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); } }