Skip to content

Commit

Permalink
Development: Add exercise update announcement e2e tests (#9784)
Browse files Browse the repository at this point in the history
  • Loading branch information
muradium authored Nov 26, 2024
1 parent 4eb3fe9 commit 34d9914
Show file tree
Hide file tree
Showing 8 changed files with 172 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ <h5 class="group-title font-weight-bold mb-0">{{ exerciseGroup.title }}</h5>
</thead>
<tbody id="exercises">
@for (exercise of exerciseGroup.exercises; track exercise) {
<tr>
<tr id="{{ 'exercise-' + exercise.id }}">
<td class="align-middle">
@if (course.isAtLeastEditor) {
<a
Expand Down
62 changes: 60 additions & 2 deletions src/test/playwright/e2e/exam/ExamParticipation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import { ExamStartEndPage } from '../../support/pageobjects/exam/ExamStartEndPag
import { Commands } from '../../support/commands';
import { ExamAPIRequests } from '../../support/requests/ExamAPIRequests';
import { ModalDialogBox } from '../../support/pageobjects/exam/ModalDialogBox';
import { ExamParticipationActions } from '../../support/pageobjects/exam/ExamParticipationActions';
import { ExamParticipationActions, TextDifferenceType } from '../../support/pageobjects/exam/ExamParticipationActions';
import { ExamNavigationBar } from '../../support/pageobjects/exam/ExamNavigationBar';
import textExerciseTemplate from '../../fixtures/exercise/text/template.json';

// Common primitives
const textFixture = 'loremIpsum.txt';
Expand Down Expand Up @@ -261,11 +263,12 @@ test.describe('Exam participation', () => {
test.describe('Exam announcements', { tag: '@slow' }, () => {
let exam: Exam;
const students = [studentOne, studentTwo];
let exercise: Exercise;

test.beforeEach('Create exam', async ({ login, examAPIRequests, examExerciseGroupCreation }) => {
await login(admin);
exam = await createExam(course, examAPIRequests);
const exercise = await examExerciseGroupCreation.addGroupWithExercise(exam, ExerciseType.TEXT, { textFixture });
exercise = await examExerciseGroupCreation.addGroupWithExercise(exam, ExerciseType.TEXT, { textFixture });
exerciseArray.push(exercise);
for (const student of students) {
await examAPIRequests.registerStudentForExam(exam, student);
Expand Down Expand Up @@ -347,6 +350,61 @@ test.describe('Exam participation', () => {
await examParticipationActions.checkExamTimeLeft('29');
}
});

test(
'Instructor changes problem statement and all participants are informed',
{ tag: '@fast' },
async ({ browser, login, navigationBar, courseManagement, examManagement, examExerciseGroups, editExam, textExerciseCreation }) => {
await login(instructor);
await navigationBar.openCourseManagement();
await courseManagement.openExamsOfCourse(course.id!);
await examManagement.openExam(exam.id!);

const studentPages = [];

for (const student of students) {
const studentContext = await browser.newContext();
const studentPage = await studentContext.newPage();
studentPages.push(studentPage);

await Commands.login(studentPage, student);
await studentPage.goto(`/courses/${course.id!}/exams/${exam.id!}`);
const examStartEnd = new ExamStartEndPage(studentPage);
await examStartEnd.startExam(false);
const examNavigation = new ExamNavigationBar(studentPage);
await examNavigation.openOrSaveExerciseByTitle(exercise.exerciseGroup!.title!);
}

await editExam.openExerciseGroups();
await examExerciseGroups.clickEditExercise(exercise.exerciseGroup!.id!, exercise.id!);

const problemStatementText = textExerciseTemplate.problemStatement;
const startOfChangesIndex = problemStatementText.lastIndexOf(' ') + 1;
const removedText = problemStatementText.slice(startOfChangesIndex);
const unchangedText = problemStatementText.slice(0, startOfChangesIndex);
const addedText = 'Changed';
await textExerciseCreation.clearProblemStatement();
await textExerciseCreation.typeProblemStatement(unchangedText + addedText);
await textExerciseCreation.create();

for (const studentPage of studentPages) {
const modalDialog = new ModalDialogBox(studentPage);
const exerciseUpdateMessage = `The problem statement of the exercise '${exercise.exerciseGroup!.title!}' was updated. Please open the exercise to see the changes.`;
await modalDialog.checkDialogType('Problem Statement Update');
await modalDialog.checkDialogMessage(exerciseUpdateMessage);
await modalDialog.checkDialogAuthor(instructor.username);
await modalDialog.pressModalButton('Navigate to exercise');
const examParticipationActions = new ExamParticipationActions(studentPage);
await examParticipationActions.checkExerciseProblemStatementDifference([
{ text: unchangedText, differenceType: TextDifferenceType.NONE },
{ text: removedText, differenceType: TextDifferenceType.DELETE },
{ text: addedText, differenceType: TextDifferenceType.ADD },
]);
await studentPage.locator('#highlightDiffButton').click();
await examParticipationActions.checkExerciseProblemStatementDifference([{ text: unchangedText + addedText, differenceType: TextDifferenceType.NONE }]);
}
},
);
});

test.afterEach('Delete course', async ({ courseManagementAPIRequests }) => {
Expand Down
5 changes: 5 additions & 0 deletions src/test/playwright/support/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import { QuizExerciseOverviewPage } from './pageobjects/exercises/quiz/QuizExerc
import { QuizExerciseParticipationPage } from './pageobjects/exercises/quiz/QuizExerciseParticipationPage';
import { ModalDialogBox } from './pageobjects/exam/ModalDialogBox';
import { ExamParticipationActions } from './pageobjects/exam/ExamParticipationActions';
import { EditExamPage } from './pageobjects/exam/EditExamPage';

/*
* Define custom types for fixtures
Expand Down Expand Up @@ -96,6 +97,7 @@ export type ArtemisPageObjects = {
courseCommunication: CourseCommunicationPage;
lectureManagement: LectureManagementPage;
lectureCreation: LectureCreationPage;
editExam: EditExamPage;
examCreation: ExamCreationPage;
examDetails: ExamDetailsPage;
examExerciseGroupCreation: ExamExerciseGroupCreationPage;
Expand Down Expand Up @@ -219,6 +221,9 @@ export const test = base.extend<ArtemisPageObjects & ArtemisCommands & ArtemisRe
lectureCreation: async ({ page }, use) => {
await use(new LectureCreationPage(page));
},
editExam: async ({ page }, use) => {
await use(new EditExamPage(page));
},
examCreation: async ({ page }, use) => {
await use(new ExamCreationPage(page));
},
Expand Down
13 changes: 13 additions & 0 deletions src/test/playwright/support/pageobjects/exam/EditExamPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Page } from '@playwright/test';

export class EditExamPage {
private readonly page: Page;

constructor(page: Page) {
this.page = page;
}

async openExerciseGroups() {
await this.page.locator(`#exercises-button-groups-table`).click();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ export class ExamExerciseGroupsPage {
await this.page.locator(`#group-${groupID} .add-programming-exercise`).click();
}

async clickEditExercise(groupID: number, exerciseID: number) {
await this.page.locator(`#group-${groupID} #exercise-${exerciseID}`).locator('.btn', { hasText: 'Edit' }).click();
}

async visitPageViaUrl(courseId: number, examId: number) {
await this.page.goto(`course-management/${courseId}/exams/${examId}/exercise-groups`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,31 @@ export class ExamParticipationActions {
await expect(exercise.locator('.exercise-title')).toContainText(title);
}

async checkExerciseProblemStatementDifference(differenceSlices: TextDifferenceSlice[]) {
const problemStatementCard = this.page.locator('.card', { hasText: 'Problem Statement' });
const problemStatementText = problemStatementCard.locator('.markdown-preview').locator('p');

if ((await problemStatementText.locator('.diffmod').count()) > 0) {
for (const slice of differenceSlices) {
switch (slice.differenceType) {
case TextDifferenceType.ADD:
await expect(problemStatementText.locator('ins').getByText(slice.text)).toBeVisible();
break;
case TextDifferenceType.DELETE:
await expect(problemStatementText.locator('del').getByText(slice.text)).toBeVisible();
break;
case TextDifferenceType.NONE:
await expect(problemStatementText).toContainText(slice.text);
break;
}
}
} else {
const firstSlice = differenceSlices[0];
expect(firstSlice.differenceType).toBe(TextDifferenceType.NONE);
await expect(problemStatementText).toHaveText(firstSlice.text);
}
}

async checkExamTitle(title: string) {
await expect(this.page.locator('#exam-title')).toContainText(title);
}
Expand Down Expand Up @@ -89,3 +114,16 @@ export class ExamParticipationActions {
await expect(gradingKeyCard.locator('tr.highlighted').locator('td', { hasText: gradeName })).toBeVisible();
}
}

export class TextDifferenceSlice {
constructor(
public text: string,
public differenceType: TextDifferenceType,
) {}
}

export enum TextDifferenceType {
NONE,
ADD,
DELETE,
}
12 changes: 12 additions & 0 deletions src/test/playwright/support/pageobjects/exam/ModalDialogBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export class ModalDialogBox {
await expect(this.getModalDialogContent().locator('.content').getByText(message)).toBeVisible();
}

async checkDialogType(type: string) {
await expect(this.getModalDialogContent().locator('.type').getByText(type)).toBeVisible();
}

async checkDialogAuthor(authorUsername: string) {
await expect(this.getModalDialogContent().locator('.author').getByText(authorUsername)).toBeVisible();
}
Expand All @@ -37,4 +41,12 @@ export class ModalDialogBox {
async closeDialog() {
await this.getModalDialogContent().locator('button').click({ force: true });
}

async pressModalButton(buttonText: string) {
let buttonLocator = this.getModalDialogContent().locator('button');
if (buttonText) {
buttonLocator = buttonLocator.filter({ hasText: buttonText });
}
await buttonLocator.click();
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { Page } from '@playwright/test';
import { Locator, Page } from '@playwright/test';
import { Dayjs } from 'dayjs';
import { enterDate } from '../../../utils';
import { TEXT_EXERCISE_BASE } from '../../../constants';

export class TextExerciseCreationPage {
private readonly page: Page;

private readonly PROBLEM_STATEMENT_SELECTOR = '#problemStatement';
private readonly EXAMPLE_SOLUTION_SELECTOR = '#exampleSolution';
private readonly ASSESSMENT_INSTRUCTIONS_SELECTOR = '#gradingInstructions';

constructor(page: Page) {
this.page = page;
}
Expand Down Expand Up @@ -33,15 +37,33 @@ export class TextExerciseCreationPage {
}

async typeProblemStatement(statement: string) {
await this.typeText('#problemStatement', statement);
const textEditor = this.getTextEditorLocator(this.PROBLEM_STATEMENT_SELECTOR);
await this.typeText(textEditor, statement);
}

async clearProblemStatement() {
const textEditor = this.getTextEditorLocator(this.PROBLEM_STATEMENT_SELECTOR);
await this.clearText(textEditor);
}

async typeExampleSolution(statement: string) {
await this.typeText('#exampleSolution', statement);
const textEditor = this.getTextEditorLocator(this.EXAMPLE_SOLUTION_SELECTOR);
await this.typeText(textEditor, statement);
}

async clearExampleSolution() {
const textEditor = this.getTextEditorLocator(this.EXAMPLE_SOLUTION_SELECTOR);
await this.clearText(textEditor);
}

async typeAssessmentInstructions(statement: string) {
await this.typeText('#gradingInstructions', statement);
const textEditor = this.getTextEditorLocator(this.ASSESSMENT_INSTRUCTIONS_SELECTOR);
await this.typeText(textEditor, statement);
}

async clearAssessmentInstructions() {
const textEditor = this.getTextEditorLocator(this.ASSESSMENT_INSTRUCTIONS_SELECTOR);
await this.clearText(textEditor);
}

async create() {
Expand All @@ -56,9 +78,18 @@ export class TextExerciseCreationPage {
return await responsePromise;
}

private async typeText(selector: string, text: string) {
const textField = this.page.locator(selector).locator('.monaco-editor');
await textField.click();
await textField.pressSequentially(text);
private getTextEditorLocator(selector: string) {
return this.page.locator(selector).locator('.monaco-editor');
}

private async clearText(textEditor: Locator) {
await textEditor.click();
await textEditor.press('Control+a');
await textEditor.press('Backspace');
}

private async typeText(textEditor: Locator, text: string) {
await textEditor.click();
await textEditor.pressSequentially(text);
}
}

0 comments on commit 34d9914

Please sign in to comment.