diff --git a/src/main/webapp/app/exam/manage/exercise-groups/exercise-groups.component.html b/src/main/webapp/app/exam/manage/exercise-groups/exercise-groups.component.html
index 8b72a9f8f417..43cd6963ce39 100644
--- a/src/main/webapp/app/exam/manage/exercise-groups/exercise-groups.component.html
+++ b/src/main/webapp/app/exam/manage/exercise-groups/exercise-groups.component.html
@@ -242,7 +242,7 @@
{{ exerciseGroup.title }}
@for (exercise of exerciseGroup.exercises; track exercise) {
-
+
@if (course.isAtLeastEditor) {
{
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);
@@ -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 }) => {
diff --git a/src/test/playwright/support/fixtures.ts b/src/test/playwright/support/fixtures.ts
index dbf28d32cc37..24d95e944326 100644
--- a/src/test/playwright/support/fixtures.ts
+++ b/src/test/playwright/support/fixtures.ts
@@ -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
@@ -96,6 +97,7 @@ export type ArtemisPageObjects = {
courseCommunication: CourseCommunicationPage;
lectureManagement: LectureManagementPage;
lectureCreation: LectureCreationPage;
+ editExam: EditExamPage;
examCreation: ExamCreationPage;
examDetails: ExamDetailsPage;
examExerciseGroupCreation: ExamExerciseGroupCreationPage;
@@ -219,6 +221,9 @@ export const test = base.extend {
await use(new LectureCreationPage(page));
},
+ editExam: async ({ page }, use) => {
+ await use(new EditExamPage(page));
+ },
examCreation: async ({ page }, use) => {
await use(new ExamCreationPage(page));
},
diff --git a/src/test/playwright/support/pageobjects/exam/EditExamPage.ts b/src/test/playwright/support/pageobjects/exam/EditExamPage.ts
new file mode 100644
index 000000000000..69608a61522c
--- /dev/null
+++ b/src/test/playwright/support/pageobjects/exam/EditExamPage.ts
@@ -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();
+ }
+}
diff --git a/src/test/playwright/support/pageobjects/exam/ExamExerciseGroupsPage.ts b/src/test/playwright/support/pageobjects/exam/ExamExerciseGroupsPage.ts
index 4e4379f12817..9eb57e9fbc83 100644
--- a/src/test/playwright/support/pageobjects/exam/ExamExerciseGroupsPage.ts
+++ b/src/test/playwright/support/pageobjects/exam/ExamExerciseGroupsPage.ts
@@ -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`);
}
diff --git a/src/test/playwright/support/pageobjects/exam/ExamParticipationActions.ts b/src/test/playwright/support/pageobjects/exam/ExamParticipationActions.ts
index a7be0510b65a..0fee7970a6ea 100644
--- a/src/test/playwright/support/pageobjects/exam/ExamParticipationActions.ts
+++ b/src/test/playwright/support/pageobjects/exam/ExamParticipationActions.ts
@@ -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);
}
@@ -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,
+}
diff --git a/src/test/playwright/support/pageobjects/exam/ModalDialogBox.ts b/src/test/playwright/support/pageobjects/exam/ModalDialogBox.ts
index 77d7a06f2168..54de2cf3f068 100644
--- a/src/test/playwright/support/pageobjects/exam/ModalDialogBox.ts
+++ b/src/test/playwright/support/pageobjects/exam/ModalDialogBox.ts
@@ -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();
}
@@ -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();
+ }
}
diff --git a/src/test/playwright/support/pageobjects/exercises/text/TextExerciseCreationPage.ts b/src/test/playwright/support/pageobjects/exercises/text/TextExerciseCreationPage.ts
index 7d13e7ee1101..b2c934b7f3f6 100644
--- a/src/test/playwright/support/pageobjects/exercises/text/TextExerciseCreationPage.ts
+++ b/src/test/playwright/support/pageobjects/exercises/text/TextExerciseCreationPage.ts
@@ -1,4 +1,4 @@
-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';
@@ -6,6 +6,10 @@ 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;
}
@@ -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() {
@@ -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);
}
}
|