From 556b6d6b0f2effc516cd637c865a32f3564efa2f Mon Sep 17 00:00:00 2001 From: Murad Talibov Date: Thu, 9 May 2024 20:18:47 +0200 Subject: [PATCH 1/8] Instructor makes an exam announcement and all participants receive it --- ...-announcement-create-button.component.html | 2 +- .../e2e/exam/ExamParticipation.spec.ts | 68 ++++++++++++++++++- .../pageobjects/exam/ExamManagementPage.ts | 23 +++++++ 3 files changed, 90 insertions(+), 3 deletions(-) diff --git a/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-announcement-dialog/exam-live-announcement-create-button.component.html b/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-announcement-dialog/exam-live-announcement-create-button.component.html index 99a6bf8f458b..245662fb970c 100644 --- a/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-announcement-dialog/exam-live-announcement-create-button.component.html +++ b/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-announcement-dialog/exam-live-announcement-create-button.component.html @@ -1,4 +1,4 @@ - diff --git a/src/test/playwright/e2e/exam/ExamParticipation.spec.ts b/src/test/playwright/e2e/exam/ExamParticipation.spec.ts index 6cc539f65011..dae4cc31c206 100644 --- a/src/test/playwright/e2e/exam/ExamParticipation.spec.ts +++ b/src/test/playwright/e2e/exam/ExamParticipation.spec.ts @@ -1,12 +1,15 @@ import { test } from '../../support/fixtures'; import { Course } from 'app/entities/course.model'; import { Exercise, ExerciseType } from '../../support/constants'; -import { admin, instructor, studentFour, studentThree, studentTwo, tutor, users } from '../../support/users'; +import { admin, instructor, studentFour, studentOne, studentThree, studentTwo, tutor, users } from '../../support/users'; import { generateUUID } from '../../support/utils'; import javaAllSuccessfulSubmission from '../../fixtures/exercise/programming/java/all_successful/submission.json'; -import dayjs from 'dayjs'; +import dayjs, { Dayjs } from 'dayjs'; import { Exam } from 'app/entities/exam.model'; import { expect } from '@playwright/test'; +import { ExamStartEndPage } from '../../support/pageobjects/exam/ExamStartEndPage'; +import { ExamManagementPage } from '../../support/pageobjects/exam/ExamManagementPage'; +import { Commands } from '../../support/commands'; // Common primitives const textFixture = 'loremIpsum.txt'; @@ -288,6 +291,67 @@ test.describe('Exam participation', () => { }); }); + test.describe('Exam announcements', () => { + let exam: Exam; + let endDate: Dayjs; + const students = [studentOne, studentTwo]; + + test.beforeEach('Create exam', async ({ login, examAPIRequests, examExerciseGroupCreation }) => { + await login(admin); + endDate = dayjs().add(1, 'day'); + const examConfig = { + course, + title: 'exam' + generateUUID(), + visibleDate: dayjs().subtract(3, 'minutes'), + startDate: dayjs().subtract(2, 'minutes'), + endDate: endDate, + examMaxPoints: 10, + numberOfExercisesInExam: 1, + }; + exam = await examAPIRequests.createExam(examConfig); + const exercise = await examExerciseGroupCreation.addGroupWithExercise(exam, ExerciseType.TEXT, { textFixture }); + exerciseArray.push(exercise); + for (const student of students) { + await examAPIRequests.registerStudentForExam(exam, student); + } + + await examAPIRequests.generateMissingIndividualExams(exam); + await examAPIRequests.prepareExerciseStartForExam(exam); + }); + + test('Instructor sends an announcement and all participants receive it', async ({ browser, login, navigationBar, courseManagement, examManagement }) => { + await login(instructor); + await navigationBar.openCourseManagement(); + await courseManagement.openExamsOfCourse(course.id!); + await examManagement.openExam(exam.id!); + + const studentPages = []; + + for (const student of [studentOne, studentTwo]) { + 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 announcement = 'Important announcement!'; + await examManagement.openAnnouncementPopup(); + const announcementTypingTime = dayjs(); + await examManagement.typeAnnouncementMessage(announcement); + await examManagement.verifyAnnouncementContent(announcementTypingTime, announcement, instructor.username); + await examManagement.sendAnnouncement(); + + for (const studentPage of studentPages) { + const examManagement = new ExamManagementPage(studentPage); + await examManagement.verifyAnnouncementContent(announcementTypingTime, announcement, instructor.username); + } + }); + }); + test.afterEach('Delete course', async ({ courseManagementAPIRequests }) => { await courseManagementAPIRequests.deleteCourse(course, admin); }); diff --git a/src/test/playwright/support/pageobjects/exam/ExamManagementPage.ts b/src/test/playwright/support/pageobjects/exam/ExamManagementPage.ts index 56c4077b0546..6cb03c7f8502 100644 --- a/src/test/playwright/support/pageobjects/exam/ExamManagementPage.ts +++ b/src/test/playwright/support/pageobjects/exam/ExamManagementPage.ts @@ -1,4 +1,5 @@ import { Page, expect } from '@playwright/test'; +import { Dayjs } from 'dayjs'; /** * A class which encapsulates UI selectors and actions for the exam management page. @@ -108,6 +109,28 @@ export class ExamManagementPage { await expect(this.page.locator('#exercise-result-score')).toHaveText(score); } + async openAnnouncementPopup() { + await this.page.locator('#announcement-create-button').click(); + } + + async typeAnnouncementMessage(message: string) { + await this.page.locator('.ace_text-input').fill(message); + } + + async verifyAnnouncementContent(announcementTime: Dayjs, message: string, authorUsername: string) { + const announcementPopup = this.page.locator('.modal-content'); + const timeFormat = 'MMM D, YYYY HH:mm'; + const announcementTimeFormatted = announcementTime.format(timeFormat); + const announcementTimeAfterMinute = announcementTime.add(1, 'minute').format(timeFormat); + await expect(announcementPopup.locator('.date').getByText(new RegExp(`(${announcementTimeFormatted}|${announcementTimeAfterMinute})`))).toBeVisible(); + await expect(announcementPopup.locator('.content').getByText(message)).toBeVisible(); + await expect(announcementPopup.locator('.author').getByText(authorUsername)).toBeVisible(); + } + + async sendAnnouncement() { + await this.page.locator('button', { hasText: 'Send Announcement' }).click(); + } + async clickEdit() { await this.page.locator('#editButton').click(); } From 08e7d09c8b26ae5f6cb353d0ff3bd1b2d19caa11 Mon Sep 17 00:00:00 2001 From: Murad Talibov Date: Fri, 10 May 2024 16:10:06 +0200 Subject: [PATCH 2/8] Adds exam working time POM functions --- .../exam-edit-working-time.component.html | 2 +- .../e2e/exam/ExamParticipation.spec.ts | 2 +- .../pageobjects/exam/ExamManagementPage.ts | 38 ++++++++++++++++--- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-edit-workingtime-dialog/exam-edit-working-time.component.html b/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-edit-workingtime-dialog/exam-edit-working-time.component.html index d50ec3b2b423..0145161f28b6 100644 --- a/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-edit-workingtime-dialog/exam-edit-working-time.component.html +++ b/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-edit-workingtime-dialog/exam-edit-working-time.component.html @@ -1,4 +1,4 @@ - diff --git a/src/test/playwright/e2e/exam/ExamParticipation.spec.ts b/src/test/playwright/e2e/exam/ExamParticipation.spec.ts index dae4cc31c206..559ebe98f770 100644 --- a/src/test/playwright/e2e/exam/ExamParticipation.spec.ts +++ b/src/test/playwright/e2e/exam/ExamParticipation.spec.ts @@ -339,7 +339,7 @@ test.describe('Exam participation', () => { } const announcement = 'Important announcement!'; - await examManagement.openAnnouncementPopup(); + await examManagement.openAnnouncementDialog(); const announcementTypingTime = dayjs(); await examManagement.typeAnnouncementMessage(announcement); await examManagement.verifyAnnouncementContent(announcementTypingTime, announcement, instructor.username); diff --git a/src/test/playwright/support/pageobjects/exam/ExamManagementPage.ts b/src/test/playwright/support/pageobjects/exam/ExamManagementPage.ts index 6cb03c7f8502..7ef7823151a7 100644 --- a/src/test/playwright/support/pageobjects/exam/ExamManagementPage.ts +++ b/src/test/playwright/support/pageobjects/exam/ExamManagementPage.ts @@ -109,7 +109,7 @@ export class ExamManagementPage { await expect(this.page.locator('#exercise-result-score')).toHaveText(score); } - async openAnnouncementPopup() { + async openAnnouncementDialog() { await this.page.locator('#announcement-create-button').click(); } @@ -118,19 +118,47 @@ export class ExamManagementPage { } async verifyAnnouncementContent(announcementTime: Dayjs, message: string, authorUsername: string) { - const announcementPopup = this.page.locator('.modal-content'); + const announcementDialog = this.page.locator('.modal-content'); const timeFormat = 'MMM D, YYYY HH:mm'; const announcementTimeFormatted = announcementTime.format(timeFormat); const announcementTimeAfterMinute = announcementTime.add(1, 'minute').format(timeFormat); - await expect(announcementPopup.locator('.date').getByText(new RegExp(`(${announcementTimeFormatted}|${announcementTimeAfterMinute})`))).toBeVisible(); - await expect(announcementPopup.locator('.content').getByText(message)).toBeVisible(); - await expect(announcementPopup.locator('.author').getByText(authorUsername)).toBeVisible(); + await expect(announcementDialog.locator('.date').getByText(new RegExp(`(${announcementTimeFormatted}|${announcementTimeAfterMinute})`))).toBeVisible(); + await expect(announcementDialog.locator('.content').getByText(message)).toBeVisible(); + await expect(announcementDialog.locator('.author').getByText(authorUsername)).toBeVisible(); } async sendAnnouncement() { await this.page.locator('button', { hasText: 'Send Announcement' }).click(); } + async openEditWorkingTimeDialog() { + await this.page.locator('#edit-working-time-button').click(); + } + + async changeExamWorkingTime(newWorkingTime: any) { + if (newWorkingTime.hours) { + await this.page.locator('#workingTimeHours').fill(newWorkingTime.hours.toString()); + } + if (newWorkingTime.minutes) { + await this.page.locator('#workingTimeMinutes').fill(newWorkingTime.minutes.toString()); + } + if (newWorkingTime.seconds) { + await this.page.locator('#workingTimeSeconds').fill(newWorkingTime.seconds.toString()); + } + } + + async verifyExamWorkingTimeChange(previousWorkingTime: any, newWorkingTime: any) { + const previousWorkingTimeString = `${previousWorkingTime.days}d ${previousWorkingTime.hours}h ${previousWorkingTime.minutes}min`; + const newWorkingTimeString = `${newWorkingTime.days}d ${newWorkingTime.hours}h ${newWorkingTime.minutes}min`; + await expect(this.page.locator('[data-testid="old-time"]').getByText(previousWorkingTimeString)).toBeVisible(); + await expect(this.page.locator('[data-testid="new-time"]').getByText(newWorkingTimeString)).toBeVisible(); + } + + async confirmWorkingTimeChange(examTitle: string) { + await this.page.locator('#confirm-entity-name').fill(examTitle); + await this.page.locator('#confirm').click(); + } + async clickEdit() { await this.page.locator('#editButton').click(); } From b56ab72125c5007f689d328e76f5fbd6c7d7dd79 Mon Sep 17 00:00:00 2001 From: Murad Talibov Date: Tue, 14 May 2024 19:27:17 +0200 Subject: [PATCH 3/8] Revert "Adds exam working time POM functions" This reverts commit 08e7d09c8b26ae5f6cb353d0ff3bd1b2d19caa11. --- .../exam-edit-working-time.component.html | 2 +- .../e2e/exam/ExamParticipation.spec.ts | 2 +- .../pageobjects/exam/ExamManagementPage.ts | 38 +++---------------- 3 files changed, 7 insertions(+), 35 deletions(-) diff --git a/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-edit-workingtime-dialog/exam-edit-working-time.component.html b/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-edit-workingtime-dialog/exam-edit-working-time.component.html index 0145161f28b6..d50ec3b2b423 100644 --- a/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-edit-workingtime-dialog/exam-edit-working-time.component.html +++ b/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-edit-workingtime-dialog/exam-edit-working-time.component.html @@ -1,4 +1,4 @@ - diff --git a/src/test/playwright/e2e/exam/ExamParticipation.spec.ts b/src/test/playwright/e2e/exam/ExamParticipation.spec.ts index 559ebe98f770..dae4cc31c206 100644 --- a/src/test/playwright/e2e/exam/ExamParticipation.spec.ts +++ b/src/test/playwright/e2e/exam/ExamParticipation.spec.ts @@ -339,7 +339,7 @@ test.describe('Exam participation', () => { } const announcement = 'Important announcement!'; - await examManagement.openAnnouncementDialog(); + await examManagement.openAnnouncementPopup(); const announcementTypingTime = dayjs(); await examManagement.typeAnnouncementMessage(announcement); await examManagement.verifyAnnouncementContent(announcementTypingTime, announcement, instructor.username); diff --git a/src/test/playwright/support/pageobjects/exam/ExamManagementPage.ts b/src/test/playwright/support/pageobjects/exam/ExamManagementPage.ts index 7ef7823151a7..6cb03c7f8502 100644 --- a/src/test/playwright/support/pageobjects/exam/ExamManagementPage.ts +++ b/src/test/playwright/support/pageobjects/exam/ExamManagementPage.ts @@ -109,7 +109,7 @@ export class ExamManagementPage { await expect(this.page.locator('#exercise-result-score')).toHaveText(score); } - async openAnnouncementDialog() { + async openAnnouncementPopup() { await this.page.locator('#announcement-create-button').click(); } @@ -118,47 +118,19 @@ export class ExamManagementPage { } async verifyAnnouncementContent(announcementTime: Dayjs, message: string, authorUsername: string) { - const announcementDialog = this.page.locator('.modal-content'); + const announcementPopup = this.page.locator('.modal-content'); const timeFormat = 'MMM D, YYYY HH:mm'; const announcementTimeFormatted = announcementTime.format(timeFormat); const announcementTimeAfterMinute = announcementTime.add(1, 'minute').format(timeFormat); - await expect(announcementDialog.locator('.date').getByText(new RegExp(`(${announcementTimeFormatted}|${announcementTimeAfterMinute})`))).toBeVisible(); - await expect(announcementDialog.locator('.content').getByText(message)).toBeVisible(); - await expect(announcementDialog.locator('.author').getByText(authorUsername)).toBeVisible(); + await expect(announcementPopup.locator('.date').getByText(new RegExp(`(${announcementTimeFormatted}|${announcementTimeAfterMinute})`))).toBeVisible(); + await expect(announcementPopup.locator('.content').getByText(message)).toBeVisible(); + await expect(announcementPopup.locator('.author').getByText(authorUsername)).toBeVisible(); } async sendAnnouncement() { await this.page.locator('button', { hasText: 'Send Announcement' }).click(); } - async openEditWorkingTimeDialog() { - await this.page.locator('#edit-working-time-button').click(); - } - - async changeExamWorkingTime(newWorkingTime: any) { - if (newWorkingTime.hours) { - await this.page.locator('#workingTimeHours').fill(newWorkingTime.hours.toString()); - } - if (newWorkingTime.minutes) { - await this.page.locator('#workingTimeMinutes').fill(newWorkingTime.minutes.toString()); - } - if (newWorkingTime.seconds) { - await this.page.locator('#workingTimeSeconds').fill(newWorkingTime.seconds.toString()); - } - } - - async verifyExamWorkingTimeChange(previousWorkingTime: any, newWorkingTime: any) { - const previousWorkingTimeString = `${previousWorkingTime.days}d ${previousWorkingTime.hours}h ${previousWorkingTime.minutes}min`; - const newWorkingTimeString = `${newWorkingTime.days}d ${newWorkingTime.hours}h ${newWorkingTime.minutes}min`; - await expect(this.page.locator('[data-testid="old-time"]').getByText(previousWorkingTimeString)).toBeVisible(); - await expect(this.page.locator('[data-testid="new-time"]').getByText(newWorkingTimeString)).toBeVisible(); - } - - async confirmWorkingTimeChange(examTitle: string) { - await this.page.locator('#confirm-entity-name').fill(examTitle); - await this.page.locator('#confirm').click(); - } - async clickEdit() { await this.page.locator('#editButton').click(); } From 5e1c72cb6238fea081b0d670901fca2bf7950aa5 Mon Sep 17 00:00:00 2001 From: Murad Talibov Date: Tue, 14 May 2024 19:29:34 +0200 Subject: [PATCH 4/8] Extract function for exam creation --- .../e2e/exam/ExamParticipation.spec.ts | 67 ++++++------------- .../pageobjects/exam/ExamManagementPage.ts | 10 +-- 2 files changed, 26 insertions(+), 51 deletions(-) diff --git a/src/test/playwright/e2e/exam/ExamParticipation.spec.ts b/src/test/playwright/e2e/exam/ExamParticipation.spec.ts index dae4cc31c206..fe30198035e6 100644 --- a/src/test/playwright/e2e/exam/ExamParticipation.spec.ts +++ b/src/test/playwright/e2e/exam/ExamParticipation.spec.ts @@ -4,12 +4,13 @@ import { Exercise, ExerciseType } from '../../support/constants'; import { admin, instructor, studentFour, studentOne, studentThree, studentTwo, tutor, users } from '../../support/users'; import { generateUUID } from '../../support/utils'; import javaAllSuccessfulSubmission from '../../fixtures/exercise/programming/java/all_successful/submission.json'; -import dayjs, { Dayjs } from 'dayjs'; +import dayjs from 'dayjs'; import { Exam } from 'app/entities/exam.model'; import { expect } from '@playwright/test'; import { ExamStartEndPage } from '../../support/pageobjects/exam/ExamStartEndPage'; import { ExamManagementPage } from '../../support/pageobjects/exam/ExamManagementPage'; import { Commands } from '../../support/commands'; +import { ExamAPIRequests } from '../../support/requests/ExamAPIRequests'; // Common primitives const textFixture = 'loremIpsum.txt'; @@ -47,16 +48,7 @@ test.describe('Exam participation', () => { test.beforeEach('Create exam', async ({ login, examAPIRequests, examExerciseGroupCreation }) => { await login(admin); - const examConfig = { - course, - title: examTitle, - visibleDate: dayjs().subtract(3, 'minutes'), - startDate: dayjs().subtract(2, 'minutes'), - endDate: dayjs().add(1, 'hour'), - examMaxPoints: 40, - numberOfExercisesInExam: 4, - }; - exam = await examAPIRequests.createExam(examConfig); + exam = await createExam(course, examAPIRequests, { title: examTitle, examMaxPoints: 40, numberOfExercisesInExam: 4 }); const textExercise = await examExerciseGroupCreation.addGroupWithExercise(exam, ExerciseType.TEXT, { textFixture }); const programmingExercise = await examExerciseGroupCreation.addGroupWithExercise(exam, ExerciseType.PROGRAMMING, { submission: javaAllSuccessfulSubmission }); const quizExercise = await examExerciseGroupCreation.addGroupWithExercise(exam, ExerciseType.QUIZ, { quizExerciseID: 0 }); @@ -139,17 +131,7 @@ test.describe('Exam participation', () => { exerciseArray = []; await login(admin); - - const examConfig = { - course, - title: examTitle, - visibleDate: dayjs().subtract(3, 'minutes'), - startDate: dayjs().subtract(2, 'minutes'), - endDate: dayjs().add(1, 'hour'), - examMaxPoints: 10, - numberOfExercisesInExam: 1, - }; - exam = await examAPIRequests.createExam(examConfig); + exam = await createExam(course, examAPIRequests, { title: examTitle }); await examExerciseGroupCreation.addGroupWithExercise(exam, ExerciseType.TEXT, { textFixture }).then((response) => { exerciseArray.push(response); }); @@ -252,17 +234,7 @@ test.describe('Exam participation', () => { exerciseArray = []; await login(admin); - - const examConfig = { - course, - title: examTitle, - visibleDate: dayjs().subtract(3, 'minutes'), - startDate: dayjs().subtract(2, 'minutes'), - endDate: dayjs().add(30, 'seconds'), - examMaxPoints: 10, - numberOfExercisesInExam: 1, - }; - exam = await examAPIRequests.createExam(examConfig); + exam = await createExam(course, examAPIRequests, { title: examTitle, endDate: dayjs().add(30, 'seconds') }); const exercise = await examExerciseGroupCreation.addGroupWithExercise(exam, ExerciseType.TEXT, { textFixture }); exerciseArray.push(exercise); @@ -293,22 +265,11 @@ test.describe('Exam participation', () => { test.describe('Exam announcements', () => { let exam: Exam; - let endDate: Dayjs; const students = [studentOne, studentTwo]; test.beforeEach('Create exam', async ({ login, examAPIRequests, examExerciseGroupCreation }) => { await login(admin); - endDate = dayjs().add(1, 'day'); - const examConfig = { - course, - title: 'exam' + generateUUID(), - visibleDate: dayjs().subtract(3, 'minutes'), - startDate: dayjs().subtract(2, 'minutes'), - endDate: endDate, - examMaxPoints: 10, - numberOfExercisesInExam: 1, - }; - exam = await examAPIRequests.createExam(examConfig); + exam = await createExam(course, examAPIRequests); const exercise = await examExerciseGroupCreation.addGroupWithExercise(exam, ExerciseType.TEXT, { textFixture }); exerciseArray.push(exercise); for (const student of students) { @@ -339,7 +300,7 @@ test.describe('Exam participation', () => { } const announcement = 'Important announcement!'; - await examManagement.openAnnouncementPopup(); + await examManagement.openAnnouncementDialog(); const announcementTypingTime = dayjs(); await examManagement.typeAnnouncementMessage(announcement); await examManagement.verifyAnnouncementContent(announcementTypingTime, announcement, instructor.username); @@ -356,3 +317,17 @@ test.describe('Exam participation', () => { await courseManagementAPIRequests.deleteCourse(course, admin); }); }); + +async function createExam(course: Course, examAPIRequests: ExamAPIRequests, customExamConfig?: any) { + const defaultExamConfig = { + course, + title: 'exam' + generateUUID(), + visibleDate: dayjs().subtract(3, 'minutes'), + startDate: dayjs().subtract(2, 'minutes'), + endDate: dayjs().add(1, 'hour'), + examMaxPoints: 10, + numberOfExercisesInExam: 1, + }; + const examConfig = { ...defaultExamConfig, ...customExamConfig }; + return await examAPIRequests.createExam(examConfig); +} diff --git a/src/test/playwright/support/pageobjects/exam/ExamManagementPage.ts b/src/test/playwright/support/pageobjects/exam/ExamManagementPage.ts index 6cb03c7f8502..9f29f61ac4c4 100644 --- a/src/test/playwright/support/pageobjects/exam/ExamManagementPage.ts +++ b/src/test/playwright/support/pageobjects/exam/ExamManagementPage.ts @@ -109,7 +109,7 @@ export class ExamManagementPage { await expect(this.page.locator('#exercise-result-score')).toHaveText(score); } - async openAnnouncementPopup() { + async openAnnouncementDialog() { await this.page.locator('#announcement-create-button').click(); } @@ -118,13 +118,13 @@ export class ExamManagementPage { } async verifyAnnouncementContent(announcementTime: Dayjs, message: string, authorUsername: string) { - const announcementPopup = this.page.locator('.modal-content'); + const announcementDialog = this.page.locator('.modal-content'); const timeFormat = 'MMM D, YYYY HH:mm'; const announcementTimeFormatted = announcementTime.format(timeFormat); const announcementTimeAfterMinute = announcementTime.add(1, 'minute').format(timeFormat); - await expect(announcementPopup.locator('.date').getByText(new RegExp(`(${announcementTimeFormatted}|${announcementTimeAfterMinute})`))).toBeVisible(); - await expect(announcementPopup.locator('.content').getByText(message)).toBeVisible(); - await expect(announcementPopup.locator('.author').getByText(authorUsername)).toBeVisible(); + await expect(announcementDialog.locator('.date').getByText(new RegExp(`(${announcementTimeFormatted}|${announcementTimeAfterMinute})`))).toBeVisible(); + await expect(announcementDialog.locator('.content').getByText(message)).toBeVisible(); + await expect(announcementDialog.locator('.author').getByText(authorUsername)).toBeVisible(); } async sendAnnouncement() { From 73515178b348e3340fe54a0aa1a0359448a0eb9a Mon Sep 17 00:00:00 2001 From: Murad Talibov Date: Wed, 15 May 2024 16:19:32 +0200 Subject: [PATCH 5/8] Adds exam working time change test --- .../exam-edit-working-time.component.html | 2 +- .../e2e/exam/ExamAssessment.spec.ts | 6 +- .../e2e/exam/ExamParticipation.spec.ts | 92 ++++++++++++++++++- .../playwright/e2e/exam/ExamResults.spec.ts | 4 +- .../test-exam/TestExamStudentExams.spec.ts | 4 +- src/test/playwright/support/fixtures.ts | 11 ++- .../pageobjects/exam/ExamManagementPage.ts | 26 ++++++ ...ticipation.ts => ExamParticipationPage.ts} | 23 ++++- .../pageobjects/exam/ModalDialogBox.ts | 40 ++++++++ 9 files changed, 193 insertions(+), 15 deletions(-) rename src/test/playwright/support/pageobjects/exam/{ExamParticipation.ts => ExamParticipationPage.ts} (85%) create mode 100644 src/test/playwright/support/pageobjects/exam/ModalDialogBox.ts diff --git a/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-edit-workingtime-dialog/exam-edit-working-time.component.html b/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-edit-workingtime-dialog/exam-edit-working-time.component.html index d50ec3b2b423..0145161f28b6 100644 --- a/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-edit-workingtime-dialog/exam-edit-working-time.component.html +++ b/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-edit-workingtime-dialog/exam-edit-working-time.component.html @@ -1,4 +1,4 @@ - diff --git a/src/test/playwright/e2e/exam/ExamAssessment.spec.ts b/src/test/playwright/e2e/exam/ExamAssessment.spec.ts index 11c74c52d208..beb706114150 100644 --- a/src/test/playwright/e2e/exam/ExamAssessment.spec.ts +++ b/src/test/playwright/e2e/exam/ExamAssessment.spec.ts @@ -10,7 +10,7 @@ import { Exam } from 'app/entities/exam.model'; import { Commands } from '../../support/commands'; import { ExamAPIRequests } from '../../support/requests/ExamAPIRequests'; import { ExamExerciseGroupCreationPage } from '../../support/pageobjects/exam/ExamExerciseGroupCreationPage'; -import { ExamParticipation } from '../../support/pageobjects/exam/ExamParticipation'; +import { ExamParticipationPage } from '../../support/pageobjects/exam/ExamParticipationPage'; import { ExamNavigationBar } from '../../support/pageobjects/exam/ExamNavigationBar'; import { ExamStartEndPage } from '../../support/pageobjects/exam/ExamStartEndPage'; import { ExamManagementPage } from '../../support/pageobjects/exam/ExamManagementPage'; @@ -308,7 +308,7 @@ export async function prepareExam(course: Course, end: dayjs.Dayjs, exerciseType const textExerciseEditor = new TextEditorPage(page); const examNavigation = new ExamNavigationBar(page); const examStartEnd = new ExamStartEndPage(page); - const examParticipation = new ExamParticipation( + const examParticipation = new ExamParticipationPage( courseList, courseOverview, examNavigation, @@ -360,7 +360,7 @@ async function makeExamSubmission( exam: Exam, exercise: Exercise, page: Page, - examParticipation: ExamParticipation, + examParticipation: ExamParticipationPage, examNavigation: ExamNavigationBar, examStartEnd: ExamStartEndPage, ) { diff --git a/src/test/playwright/e2e/exam/ExamParticipation.spec.ts b/src/test/playwright/e2e/exam/ExamParticipation.spec.ts index fe30198035e6..b0e2ec1fba20 100644 --- a/src/test/playwright/e2e/exam/ExamParticipation.spec.ts +++ b/src/test/playwright/e2e/exam/ExamParticipation.spec.ts @@ -8,9 +8,17 @@ import dayjs from 'dayjs'; import { Exam } from 'app/entities/exam.model'; import { expect } from '@playwright/test'; import { ExamStartEndPage } from '../../support/pageobjects/exam/ExamStartEndPage'; -import { ExamManagementPage } from '../../support/pageobjects/exam/ExamManagementPage'; import { Commands } from '../../support/commands'; import { ExamAPIRequests } from '../../support/requests/ExamAPIRequests'; +import { ExamParticipationPage } from '../../support/pageobjects/exam/ExamParticipationPage'; +import { CoursesPage } from '../../support/pageobjects/course/CoursesPage'; +import { CourseOverviewPage } from '../../support/pageobjects/course/CourseOverviewPage'; +import { ExamNavigationBar } from '../../support/pageobjects/exam/ExamNavigationBar'; +import { ModelingEditor } from '../../support/pageobjects/exercises/modeling/ModelingEditor'; +import { OnlineEditorPage } from '../../support/pageobjects/exercises/programming/OnlineEditorPage'; +import { MultipleChoiceQuiz } from '../../support/pageobjects/exercises/quiz/MultipleChoiceQuiz'; +import { TextEditorPage } from '../../support/pageobjects/exercises/text/TextEditorPage'; +import { ModalDialogBox } from '../../support/pageobjects/exam/ModalDialogBox'; // Common primitives const textFixture = 'loremIpsum.txt'; @@ -307,8 +315,86 @@ test.describe('Exam participation', () => { await examManagement.sendAnnouncement(); for (const studentPage of studentPages) { - const examManagement = new ExamManagementPage(studentPage); - await examManagement.verifyAnnouncementContent(announcementTypingTime, announcement, instructor.username); + const modalDialog = new ModalDialogBox(studentPage); + await modalDialog.checkDialogTime(announcementTypingTime); + await modalDialog.checkDialogMessage(announcement); + await modalDialog.checkDialogAuthor(instructor.username); + await modalDialog.closeDialog(); + } + }); + }); + + test.describe('Exam working time change', () => { + let exam: Exam; + const students = [studentOne, studentTwo]; + + 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 }); + exerciseArray.push(exercise); + for (const student of students) { + await examAPIRequests.registerStudentForExam(exam, student); + } + + await examAPIRequests.generateMissingIndividualExams(exam); + await examAPIRequests.prepareExerciseStartForExam(exam); + }); + + test('Instructor changes working time and all participants are informed', async ({ browser, login, navigationBar, courseManagement, examManagement }) => { + 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); + } + + await examManagement.openEditWorkingTimeDialog(); + await examManagement.changeExamWorkingTime({ hours: -1 }); + await examManagement.verifyExamWorkingTimeChange('1h 2min', '2min'); + await examManagement.confirmWorkingTimeChange(exam.title!); + + for (const studentPage of studentPages) { + const courseList = new CoursesPage(studentPage); + const courseOverview = new CourseOverviewPage(studentPage); + const examNavigation = new ExamNavigationBar(studentPage); + const examStartEnd = new ExamStartEndPage(studentPage); + const modelingExerciseEditor = new ModelingEditor(studentPage); + const programmingExerciseEditor = new OnlineEditorPage(studentPage); + const quizExerciseMultipleChoice = new MultipleChoiceQuiz(studentPage); + const textExerciseEditor = new TextEditorPage(studentPage); + const examParticipation = new ExamParticipationPage( + courseList, + courseOverview, + examNavigation, + examStartEnd, + modelingExerciseEditor, + programmingExerciseEditor, + quizExerciseMultipleChoice, + textExerciseEditor, + studentPage, + ); + // TODO: There are two dialogs shown on top of each other. Investigate the reason. + const modalDialog = new ModalDialogBox(studentPage); + await modalDialog.closeDialog(); + const timeChangeMessage = 'The working time of the exam has been changed.'; + await modalDialog.checkExamTimeChangeDialog('1h 2min', '2min'); + await modalDialog.checkDialogTime(dayjs()); + await modalDialog.checkDialogMessage(timeChangeMessage); + await modalDialog.checkDialogAuthor(instructor.username); + await examParticipation.checkExamTimeLeft('0min'); + await modalDialog.closeDialog(); } }); }); diff --git a/src/test/playwright/e2e/exam/ExamResults.spec.ts b/src/test/playwright/e2e/exam/ExamResults.spec.ts index 9823f62846c1..42ebc5e7bd53 100644 --- a/src/test/playwright/e2e/exam/ExamResults.spec.ts +++ b/src/test/playwright/e2e/exam/ExamResults.spec.ts @@ -14,7 +14,7 @@ import javaPartiallySuccessfulSubmission from '../../fixtures/exercise/programmi import { ExamAPIRequests } from '../../support/requests/ExamAPIRequests'; import { ExamExerciseGroupCreationPage } from '../../support/pageobjects/exam/ExamExerciseGroupCreationPage'; import { ExerciseAPIRequests } from '../../support/requests/ExerciseAPIRequests'; -import { ExamParticipation } from '../../support/pageobjects/exam/ExamParticipation'; +import { ExamParticipationPage } from '../../support/pageobjects/exam/ExamParticipationPage'; import { CourseManagementAPIRequests } from '../../support/requests/CourseManagementAPIRequests'; import { ExamNavigationBar } from '../../support/pageobjects/exam/ExamNavigationBar'; import { CourseOverviewPage } from '../../support/pageobjects/course/CourseOverviewPage'; @@ -81,7 +81,7 @@ test.describe('Exam Results', () => { const courseList = new CoursesPage(page); const courseOverview = new CourseOverviewPage(page); - const examParticipation = new ExamParticipation( + const examParticipation = new ExamParticipationPage( courseList, courseOverview, new ExamNavigationBar(page), diff --git a/src/test/playwright/e2e/exam/test-exam/TestExamStudentExams.spec.ts b/src/test/playwright/e2e/exam/test-exam/TestExamStudentExams.spec.ts index b27b11567615..c7a872dff51e 100644 --- a/src/test/playwright/e2e/exam/test-exam/TestExamStudentExams.spec.ts +++ b/src/test/playwright/e2e/exam/test-exam/TestExamStudentExams.spec.ts @@ -6,7 +6,7 @@ import { Exercise, ExerciseType } from '../../../support/constants'; import dayjs from 'dayjs'; import { test } from '../../../support/fixtures'; import { expect } from '@playwright/test'; -import { ExamParticipation } from '../../../support/pageobjects/exam/ExamParticipation'; +import { ExamParticipationPage } from '../../../support/pageobjects/exam/ExamParticipationPage'; import { ExamNavigationBar } from '../../../support/pageobjects/exam/ExamNavigationBar'; // Common primitives @@ -115,7 +115,7 @@ test.describe('Test Exam - student exams', () => { exam: Exam, toStart: boolean, toSubmit: boolean, - examParticipation: ExamParticipation, + examParticipation: ExamParticipationPage, examNavigation: ExamNavigationBar, ) { if (!toStart) { diff --git a/src/test/playwright/support/fixtures.ts b/src/test/playwright/support/fixtures.ts index 0e0e9c49969f..26d022ef53e9 100644 --- a/src/test/playwright/support/fixtures.ts +++ b/src/test/playwright/support/fixtures.ts @@ -20,7 +20,7 @@ import { ExamDetailsPage } from './pageobjects/exam/ExamDetailsPage'; import { ExamManagementPage } from './pageobjects/exam/ExamManagementPage'; import { ExamExerciseGroupCreationPage } from './pageobjects/exam/ExamExerciseGroupCreationPage'; import { ExamNavigationBar } from './pageobjects/exam/ExamNavigationBar'; -import { ExamParticipation } from './pageobjects/exam/ExamParticipation'; +import { ExamParticipationPage } from './pageobjects/exam/ExamParticipationPage'; import { ExamStartEndPage } from './pageobjects/exam/ExamStartEndPage'; import { CoursesPage } from './pageobjects/course/CoursesPage'; import { ExamAssessmentPage } from './pageobjects/assessment/ExamAssessmentPage'; @@ -62,6 +62,7 @@ import { ExamGradingPage } from './pageobjects/exam/ExamGradingPage'; import { ExamScoresPage } from './pageobjects/exam/ExamScoresPage'; import { ExamResultsPage } from './pageobjects/exam/ExamResultsPage'; import { ExerciseTeamsPage } from './pageobjects/exercises/ExerciseTeamsPage'; +import { ModalDialogBox } from './pageobjects/exam/ModalDialogBox'; /* * Define custom types for fixtures @@ -97,11 +98,12 @@ export type ArtemisPageObjects = { examGrading: ExamGradingPage; examNavigation: ExamNavigationBar; examManagement: ExamManagementPage; - examParticipation: ExamParticipation; + examParticipation: ExamParticipationPage; examResultsPage: ExamResultsPage; examScores: ExamScoresPage; examStartEnd: ExamStartEndPage; examTestRun: ExamTestRunPage; + modalDialog: ModalDialogBox; studentExamManagement: StudentExamManagementPage; fileUploadExerciseCreation: FileUploadExerciseCreationPage; fileUploadExerciseEditor: FileUploadEditorPage; @@ -229,7 +231,7 @@ export const test = base.extend { await use( - new ExamParticipation( + new ExamParticipationPage( courseList, courseOverview, examNavigation, @@ -254,6 +256,9 @@ export const test = base.extend { await use(new ExamTestRunPage(page, examStartEnd)); }, + modalDialog: async ({ page }, use) => { + await use(new ModalDialogBox(page)); + }, studentExamManagement: async ({ page }, use) => { await use(new StudentExamManagementPage(page)); }, diff --git a/src/test/playwright/support/pageobjects/exam/ExamManagementPage.ts b/src/test/playwright/support/pageobjects/exam/ExamManagementPage.ts index 9f29f61ac4c4..a2be9e7d9ed2 100644 --- a/src/test/playwright/support/pageobjects/exam/ExamManagementPage.ts +++ b/src/test/playwright/support/pageobjects/exam/ExamManagementPage.ts @@ -131,6 +131,32 @@ export class ExamManagementPage { await this.page.locator('button', { hasText: 'Send Announcement' }).click(); } + async openEditWorkingTimeDialog() { + await this.page.locator('#edit-working-time-button').click(); + } + + async changeExamWorkingTime(newWorkingTime: any) { + if (newWorkingTime.hours) { + await this.page.locator('#workingTimeHours').fill(newWorkingTime.hours.toString()); + } + if (newWorkingTime.minutes) { + await this.page.locator('#workingTimeMinutes').fill(newWorkingTime.minutes.toString()); + } + if (newWorkingTime.seconds) { + await this.page.locator('#workingTimeSeconds').fill(newWorkingTime.seconds.toString()); + } + } + + async verifyExamWorkingTimeChange(previousWorkingTime: any, newWorkingTime: any) { + await expect(this.page.locator('[data-testid="old-time"]').getByText(previousWorkingTime)).toBeVisible(); + await expect(this.page.locator('[data-testid="new-time"]').getByText(newWorkingTime)).toBeVisible(); + } + + async confirmWorkingTimeChange(examTitle: string) { + await this.page.locator('#confirm-entity-name').fill(examTitle); + await this.page.locator('#confirm').click(); + } + async clickEdit() { await this.page.locator('#editButton').click(); } diff --git a/src/test/playwright/support/pageobjects/exam/ExamParticipation.ts b/src/test/playwright/support/pageobjects/exam/ExamParticipationPage.ts similarity index 85% rename from src/test/playwright/support/pageobjects/exam/ExamParticipation.ts rename to src/test/playwright/support/pageobjects/exam/ExamParticipationPage.ts index 9ae85065182c..240fa61b089c 100644 --- a/src/test/playwright/support/pageobjects/exam/ExamParticipation.ts +++ b/src/test/playwright/support/pageobjects/exam/ExamParticipationPage.ts @@ -14,8 +14,9 @@ import { MultipleChoiceQuiz } from '../exercises/quiz/MultipleChoiceQuiz'; import { TextEditorPage } from '../exercises/text/TextEditorPage'; import { Commands } from '../../commands'; import { Fixtures } from '../../../fixtures/fixtures'; +import { Dayjs } from 'dayjs'; -export class ExamParticipation { +export class ExamParticipationPage { private readonly courseList: CoursesPage; private readonly courseOverview: CourseOverviewPage; private readonly examNavigation: ExamNavigationBar; @@ -151,6 +152,26 @@ export class ExamParticipation { await expect(this.page.locator('#your-name')).toContainText(name, { timeout: 30000 }); } + async checkExamTimeLeft(timeLeft: string) { + await expect(this.page.locator('#displayTime').getByText(timeLeft)).toBeVisible(); + } + + async checkExamTimeChangeDialog(previousWorkingTime: string, newWorkingTime: string, announcementTime: Dayjs, authorUsername: string, message: string) { + const timeChangeDialog = this.page.locator('.modal-content'); + await expect(timeChangeDialog.getByTestId('old-time').getByText(previousWorkingTime)).toBeVisible(); + await expect(timeChangeDialog.getByTestId('new-time').getByText(newWorkingTime)).toBeVisible(); + const timeFormat = 'MMM D, YYYY HH:mm'; + const announcementTimeFormatted = announcementTime.format(timeFormat); + const announcementTimeAfterMinute = announcementTime.add(1, 'minute').format(timeFormat); + await expect(timeChangeDialog.locator('.date').getByText(new RegExp(`(${announcementTimeFormatted}|${announcementTimeAfterMinute})`))).toBeVisible(); + await expect(timeChangeDialog.locator('.content').getByText(message)).toBeVisible(); + await expect(timeChangeDialog.locator('.author').getByText(authorUsername)).toBeVisible(); + } + + async closeDialog() { + await this.page.locator('button', { hasText: 'Acknowledge' }).nth(0).click({ force: true, timeout: 5000 }); + } + async handInEarly() { await this.examNavigation.handInEarly(); const response = await this.examStartEnd.finishExam(); diff --git a/src/test/playwright/support/pageobjects/exam/ModalDialogBox.ts b/src/test/playwright/support/pageobjects/exam/ModalDialogBox.ts new file mode 100644 index 000000000000..e627d21fcffb --- /dev/null +++ b/src/test/playwright/support/pageobjects/exam/ModalDialogBox.ts @@ -0,0 +1,40 @@ +import { Page, expect } from '@playwright/test'; +import { Dayjs } from 'dayjs'; + +export class ModalDialogBox { + private readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + private getModalDialogContent() { + return this.page.locator('.modal-content'); + } + + async checkDialogTime(dialogTime: Dayjs) { + const modalDialog = this.getModalDialogContent(); + const timeFormat = 'MMM D, YYYY HH:mm'; + const dialogTimeFormatted = dialogTime.format(timeFormat); + const dialogTimeAfterMinuteFormatted = dialogTime.add(1, 'minute').format(timeFormat); + await expect(modalDialog.locator('.date').getByText(new RegExp(`(${dialogTimeFormatted}|${dialogTimeAfterMinuteFormatted})`))).toBeVisible(); + } + + async checkDialogMessage(message: string) { + await expect(this.getModalDialogContent().locator('.content').getByText(message)).toBeVisible(); + } + + async checkDialogAuthor(authorUsername: string) { + await expect(this.getModalDialogContent().locator('.author').getByText(authorUsername)).toBeVisible(); + } + + async checkExamTimeChangeDialog(previousWorkingTime: string, newWorkingTime: string) { + const timeChangeDialog = this.getModalDialogContent(); + await expect(timeChangeDialog.getByTestId('old-time').getByText(previousWorkingTime)).toBeVisible(); + await expect(timeChangeDialog.getByTestId('new-time').getByText(newWorkingTime)).toBeVisible(); + } + + async closeDialog() { + await this.getModalDialogContent().locator('button').nth(0).click({ force: true }); + } +} From 957151e9c194d1b1e29d5b5d6819cb675f56ee9c Mon Sep 17 00:00:00 2001 From: Murad Talibov Date: Sat, 18 May 2024 23:34:19 +0200 Subject: [PATCH 6/8] Change TODO --- src/test/playwright/e2e/exam/ExamParticipation.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/playwright/e2e/exam/ExamParticipation.spec.ts b/src/test/playwright/e2e/exam/ExamParticipation.spec.ts index b0e2ec1fba20..a0e7c940a18a 100644 --- a/src/test/playwright/e2e/exam/ExamParticipation.spec.ts +++ b/src/test/playwright/e2e/exam/ExamParticipation.spec.ts @@ -385,7 +385,7 @@ test.describe('Exam participation', () => { textExerciseEditor, studentPage, ); - // TODO: There are two dialogs shown on top of each other. Investigate the reason. + // There are two dialogs shown on top of each other. We close one of them. const modalDialog = new ModalDialogBox(studentPage); await modalDialog.closeDialog(); const timeChangeMessage = 'The working time of the exam has been changed.'; From 2b43ec11f6ab63027acc4670c584201e02cd1c0b Mon Sep 17 00:00:00 2001 From: Murad Talibov Date: Sun, 19 May 2024 00:28:55 +0200 Subject: [PATCH 7/8] Merges announcement tests into one test suite, extracts small exam participation actions into a separate fixture --- .../e2e/exam/ExamParticipation.spec.ts | 56 ++---------- src/test/playwright/support/fixtures.ts | 6 ++ .../exam/ExamParticipationActions.ts | 91 +++++++++++++++++++ .../pageobjects/exam/ExamParticipationPage.ts | 86 +----------------- .../pageobjects/exam/ModalDialogBox.ts | 4 +- 5 files changed, 109 insertions(+), 134 deletions(-) create mode 100644 src/test/playwright/support/pageobjects/exam/ExamParticipationActions.ts diff --git a/src/test/playwright/e2e/exam/ExamParticipation.spec.ts b/src/test/playwright/e2e/exam/ExamParticipation.spec.ts index a0e7c940a18a..955016fcfe34 100644 --- a/src/test/playwright/e2e/exam/ExamParticipation.spec.ts +++ b/src/test/playwright/e2e/exam/ExamParticipation.spec.ts @@ -10,15 +10,8 @@ import { expect } from '@playwright/test'; import { ExamStartEndPage } from '../../support/pageobjects/exam/ExamStartEndPage'; import { Commands } from '../../support/commands'; import { ExamAPIRequests } from '../../support/requests/ExamAPIRequests'; -import { ExamParticipationPage } from '../../support/pageobjects/exam/ExamParticipationPage'; -import { CoursesPage } from '../../support/pageobjects/course/CoursesPage'; -import { CourseOverviewPage } from '../../support/pageobjects/course/CourseOverviewPage'; -import { ExamNavigationBar } from '../../support/pageobjects/exam/ExamNavigationBar'; -import { ModelingEditor } from '../../support/pageobjects/exercises/modeling/ModelingEditor'; -import { OnlineEditorPage } from '../../support/pageobjects/exercises/programming/OnlineEditorPage'; -import { MultipleChoiceQuiz } from '../../support/pageobjects/exercises/quiz/MultipleChoiceQuiz'; -import { TextEditorPage } from '../../support/pageobjects/exercises/text/TextEditorPage'; import { ModalDialogBox } from '../../support/pageobjects/exam/ModalDialogBox'; +import { ExamParticipationActions } from '../../support/pageobjects/exam/ExamParticipationActions'; // Common primitives const textFixture = 'loremIpsum.txt'; @@ -288,7 +281,7 @@ test.describe('Exam participation', () => { await examAPIRequests.prepareExerciseStartForExam(exam); }); - test('Instructor sends an announcement and all participants receive it', async ({ browser, login, navigationBar, courseManagement, examManagement }) => { + test('Instructor sends an announcement message and all participants receive it', async ({ browser, login, navigationBar, courseManagement, examManagement }) => { await login(instructor); await navigationBar.openCourseManagement(); await courseManagement.openExamsOfCourse(course.id!); @@ -322,24 +315,6 @@ test.describe('Exam participation', () => { await modalDialog.closeDialog(); } }); - }); - - test.describe('Exam working time change', () => { - let exam: Exam; - const students = [studentOne, studentTwo]; - - 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 }); - exerciseArray.push(exercise); - for (const student of students) { - await examAPIRequests.registerStudentForExam(exam, student); - } - - await examAPIRequests.generateMissingIndividualExams(exam); - await examAPIRequests.prepareExerciseStartForExam(exam); - }); test('Instructor changes working time and all participants are informed', async ({ browser, login, navigationBar, courseManagement, examManagement }) => { await login(instructor); @@ -366,34 +341,17 @@ test.describe('Exam participation', () => { await examManagement.confirmWorkingTimeChange(exam.title!); for (const studentPage of studentPages) { - const courseList = new CoursesPage(studentPage); - const courseOverview = new CourseOverviewPage(studentPage); - const examNavigation = new ExamNavigationBar(studentPage); - const examStartEnd = new ExamStartEndPage(studentPage); - const modelingExerciseEditor = new ModelingEditor(studentPage); - const programmingExerciseEditor = new OnlineEditorPage(studentPage); - const quizExerciseMultipleChoice = new MultipleChoiceQuiz(studentPage); - const textExerciseEditor = new TextEditorPage(studentPage); - const examParticipation = new ExamParticipationPage( - courseList, - courseOverview, - examNavigation, - examStartEnd, - modelingExerciseEditor, - programmingExerciseEditor, - quizExerciseMultipleChoice, - textExerciseEditor, - studentPage, - ); - // There are two dialogs shown on top of each other. We close one of them. + const examParticipationActions = new ExamParticipationActions(studentPage); const modalDialog = new ModalDialogBox(studentPage); - await modalDialog.closeDialog(); + // There are two dialogs shown on top of each other. We close the one + // on top for now as a workaround to avoid strict mode violation. + await modalDialog.getModalDialogContent().locator('button').last().click(); const timeChangeMessage = 'The working time of the exam has been changed.'; await modalDialog.checkExamTimeChangeDialog('1h 2min', '2min'); await modalDialog.checkDialogTime(dayjs()); await modalDialog.checkDialogMessage(timeChangeMessage); await modalDialog.checkDialogAuthor(instructor.username); - await examParticipation.checkExamTimeLeft('0min'); + await examParticipationActions.checkExamTimeLeft('0min'); await modalDialog.closeDialog(); } }); diff --git a/src/test/playwright/support/fixtures.ts b/src/test/playwright/support/fixtures.ts index 26d022ef53e9..ad2af5326744 100644 --- a/src/test/playwright/support/fixtures.ts +++ b/src/test/playwright/support/fixtures.ts @@ -63,6 +63,7 @@ import { ExamScoresPage } from './pageobjects/exam/ExamScoresPage'; import { ExamResultsPage } from './pageobjects/exam/ExamResultsPage'; import { ExerciseTeamsPage } from './pageobjects/exercises/ExerciseTeamsPage'; import { ModalDialogBox } from './pageobjects/exam/ModalDialogBox'; +import { ExamParticipationActions } from './pageobjects/exam/ExamParticipationActions'; /* * Define custom types for fixtures @@ -99,6 +100,7 @@ export type ArtemisPageObjects = { examNavigation: ExamNavigationBar; examManagement: ExamManagementPage; examParticipation: ExamParticipationPage; + examParticipationActions: ExamParticipationActions; examResultsPage: ExamResultsPage; examScores: ExamScoresPage; examStartEnd: ExamStartEndPage; @@ -244,6 +246,9 @@ export const test = base.extend { + await use(new ExamParticipationActions(page)); + }, examResultsPage: async ({ page }, use) => { await use(new ExamResultsPage(page)); }, @@ -253,6 +258,7 @@ export const test = base.extend { await use(new ExamStartEndPage(page)); }, + examTestRun: async ({ page, examStartEnd }, use) => { await use(new ExamTestRunPage(page, examStartEnd)); }, diff --git a/src/test/playwright/support/pageobjects/exam/ExamParticipationActions.ts b/src/test/playwright/support/pageobjects/exam/ExamParticipationActions.ts new file mode 100644 index 000000000000..ab7616f92a5b --- /dev/null +++ b/src/test/playwright/support/pageobjects/exam/ExamParticipationActions.ts @@ -0,0 +1,91 @@ +import { Page, expect } from '@playwright/test'; +import { Fixtures } from '../../../fixtures/fixtures'; +import { Commands } from '../../commands'; +import { getExercise } from '../../utils'; +import { Dayjs } from 'dayjs'; + +export class ExamParticipationActions { + protected readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + async selectExerciseOnOverview(index: number) { + await this.page.locator(`.exercise-table tr:nth-child(${index}) a`).click(); + } + + async clickSaveAndContinue() { + await this.page.click('#save'); + } + + async checkExerciseTitle(exerciseID: number, title: string) { + const exercise = getExercise(this.page, exerciseID); + await expect(exercise.locator('.exercise-title')).toContainText(title); + } + + async checkExamTitle(title: string) { + await expect(this.page.locator('#exam-title')).toContainText(title); + } + + async getResultScore(exerciseID?: number) { + const parentComponent = exerciseID ? getExercise(this.page, exerciseID) : this.page; + const resultScoreLocator = parentComponent.locator('#exercise-result-score'); + await Commands.reloadUntilFound(this.page, resultScoreLocator); + return resultScoreLocator; + } + + async checkResultScore(scoreText: string, exerciseID?: number) { + const scoreElement = await this.getResultScore(exerciseID); + await expect(scoreElement.getByText(new RegExp(scoreText))).toBeVisible(); + } + + async checkExamFinishedTitle(title: string) { + await expect(this.page.locator('#exam-finished-title')).toContainText(title, { timeout: 40000 }); + } + + async checkExamFullnameInputExists() { + await expect(this.page.locator('#fullname')).toBeVisible({ timeout: 30000 }); + } + + async checkYourFullname(name: string) { + await expect(this.page.locator('#your-name')).toContainText(name, { timeout: 30000 }); + } + + async checkExamTimeLeft(timeLeft: string) { + await expect(this.page.locator('#displayTime').getByText(timeLeft)).toBeVisible(); + } + + async checkExamTimeChangeDialog(previousWorkingTime: string, newWorkingTime: string, announcementTime: Dayjs, authorUsername: string, message: string) { + const timeChangeDialog = this.page.locator('.modal-content'); + await expect(timeChangeDialog.getByTestId('old-time').getByText(previousWorkingTime)).toBeVisible(); + await expect(timeChangeDialog.getByTestId('new-time').getByText(newWorkingTime)).toBeVisible(); + const timeFormat = 'MMM D, YYYY HH:mm'; + const announcementTimeFormatted = announcementTime.format(timeFormat); + const announcementTimeAfterMinute = announcementTime.add(1, 'minute').format(timeFormat); + await expect(timeChangeDialog.locator('.date').getByText(new RegExp(`(${announcementTimeFormatted}|${announcementTimeAfterMinute})`))).toBeVisible(); + await expect(timeChangeDialog.locator('.content').getByText(message)).toBeVisible(); + await expect(timeChangeDialog.locator('.author').getByText(authorUsername)).toBeVisible(); + } + + async closeDialog() { + await this.page.locator('button', { hasText: 'Acknowledge' }).nth(0).click({ force: true, timeout: 5000 }); + } + + async verifyExerciseTitleOnFinalPage(exerciseID: number, exerciseTitle: string): Promise { + const exercise = getExercise(this.page, exerciseID); + await expect(exercise.locator(`#exercise-group-title-${exerciseID}`).getByText(exerciseTitle)).toBeVisible(); + } + + async verifyTextExerciseOnFinalPage(exerciseID: number, textFixture: string): Promise { + const exercise = getExercise(this.page, exerciseID); + const submissionText = await Fixtures.get(textFixture); + await expect(exercise.locator('#text-editor')).toHaveValue(submissionText!); + } + + async verifyGradingKeyOnFinalPage(gradeName: string) { + const gradingKeyCard = this.page.locator('jhi-collapsible-card').filter({ hasText: 'Grading Key Grade Interval' }); + await gradingKeyCard.locator('button.rotate-icon').click(); + await expect(gradingKeyCard.locator('tr.highlighted').locator('td', { hasText: gradeName })).toBeVisible(); + } +} diff --git a/src/test/playwright/support/pageobjects/exam/ExamParticipationPage.ts b/src/test/playwright/support/pageobjects/exam/ExamParticipationPage.ts index 240fa61b089c..4b31b8059af4 100644 --- a/src/test/playwright/support/pageobjects/exam/ExamParticipationPage.ts +++ b/src/test/playwright/support/pageobjects/exam/ExamParticipationPage.ts @@ -3,7 +3,6 @@ import { Course } from 'app/entities/course.model'; import { Exam } from 'app/entities/exam.model'; import { AdditionalData, ExerciseType } from '../../constants'; import { UserCredentials } from '../../users'; -import { getExercise } from '../../utils'; import { OnlineEditorPage, ProgrammingExerciseSubmission } from '../exercises/programming/OnlineEditorPage'; import { CoursesPage } from '../course/CoursesPage'; import { CourseOverviewPage } from '../course/CourseOverviewPage'; @@ -14,9 +13,9 @@ import { MultipleChoiceQuiz } from '../exercises/quiz/MultipleChoiceQuiz'; import { TextEditorPage } from '../exercises/text/TextEditorPage'; import { Commands } from '../../commands'; import { Fixtures } from '../../../fixtures/fixtures'; -import { Dayjs } from 'dayjs'; +import { ExamParticipationActions } from './ExamParticipationActions'; -export class ExamParticipationPage { +export class ExamParticipationPage extends ExamParticipationActions { private readonly courseList: CoursesPage; private readonly courseOverview: CourseOverviewPage; private readonly examNavigation: ExamNavigationBar; @@ -25,7 +24,6 @@ export class ExamParticipationPage { private readonly programmingExerciseEditor: OnlineEditorPage; private readonly quizExerciseMultipleChoice: MultipleChoiceQuiz; private readonly textExerciseEditor: TextEditorPage; - private readonly page: Page; constructor( courseList: CoursesPage, @@ -38,6 +36,7 @@ export class ExamParticipationPage { textExerciseEditor: TextEditorPage, page: Page, ) { + super(page); this.courseList = courseList; this.courseOverview = courseOverview; this.examNavigation = examNavigation; @@ -46,7 +45,6 @@ export class ExamParticipationPage { this.programmingExerciseEditor = programmingExerciseEditor; this.quizExerciseMultipleChoice = quizExerciseMultipleChoice; this.textExerciseEditor = textExerciseEditor; - this.page = page; } async makeSubmission(exerciseID: number, exerciseType: ExerciseType, additionalData?: AdditionalData) { @@ -111,87 +109,9 @@ export class ExamParticipationPage { await this.examStartEnd.startExam(true); } - async selectExerciseOnOverview(index: number) { - await this.page.locator(`.exercise-table tr:nth-child(${index}) a`).click(); - } - - async clickSaveAndContinue() { - await this.page.click('#save'); - } - - async checkExerciseTitle(exerciseID: number, title: string) { - const exercise = getExercise(this.page, exerciseID); - await expect(exercise.locator('.exercise-title')).toContainText(title); - } - - async checkExamTitle(title: string) { - await expect(this.page.locator('#exam-title')).toContainText(title); - } - - async getResultScore(exerciseID?: number) { - const parentComponent = exerciseID ? getExercise(this.page, exerciseID) : this.page; - const resultScoreLocator = parentComponent.locator('#exercise-result-score'); - await Commands.reloadUntilFound(this.page, resultScoreLocator); - return resultScoreLocator; - } - - async checkResultScore(scoreText: string, exerciseID?: number) { - const scoreElement = await this.getResultScore(exerciseID); - await expect(scoreElement.getByText(new RegExp(scoreText))).toBeVisible(); - } - - async checkExamFinishedTitle(title: string) { - await expect(this.page.locator('#exam-finished-title')).toContainText(title, { timeout: 40000 }); - } - - async checkExamFullnameInputExists() { - await expect(this.page.locator('#fullname')).toBeVisible({ timeout: 30000 }); - } - - async checkYourFullname(name: string) { - await expect(this.page.locator('#your-name')).toContainText(name, { timeout: 30000 }); - } - - async checkExamTimeLeft(timeLeft: string) { - await expect(this.page.locator('#displayTime').getByText(timeLeft)).toBeVisible(); - } - - async checkExamTimeChangeDialog(previousWorkingTime: string, newWorkingTime: string, announcementTime: Dayjs, authorUsername: string, message: string) { - const timeChangeDialog = this.page.locator('.modal-content'); - await expect(timeChangeDialog.getByTestId('old-time').getByText(previousWorkingTime)).toBeVisible(); - await expect(timeChangeDialog.getByTestId('new-time').getByText(newWorkingTime)).toBeVisible(); - const timeFormat = 'MMM D, YYYY HH:mm'; - const announcementTimeFormatted = announcementTime.format(timeFormat); - const announcementTimeAfterMinute = announcementTime.add(1, 'minute').format(timeFormat); - await expect(timeChangeDialog.locator('.date').getByText(new RegExp(`(${announcementTimeFormatted}|${announcementTimeAfterMinute})`))).toBeVisible(); - await expect(timeChangeDialog.locator('.content').getByText(message)).toBeVisible(); - await expect(timeChangeDialog.locator('.author').getByText(authorUsername)).toBeVisible(); - } - - async closeDialog() { - await this.page.locator('button', { hasText: 'Acknowledge' }).nth(0).click({ force: true, timeout: 5000 }); - } - async handInEarly() { await this.examNavigation.handInEarly(); const response = await this.examStartEnd.finishExam(); expect(response.status()).toBe(200); } - - async verifyExerciseTitleOnFinalPage(exerciseID: number, exerciseTitle: string): Promise { - const exercise = getExercise(this.page, exerciseID); - await expect(exercise.locator(`#exercise-group-title-${exerciseID}`).getByText(exerciseTitle)).toBeVisible(); - } - - async verifyTextExerciseOnFinalPage(exerciseID: number, textFixture: string): Promise { - const exercise = getExercise(this.page, exerciseID); - const submissionText = await Fixtures.get(textFixture); - await expect(exercise.locator('#text-editor')).toHaveValue(submissionText!); - } - - async verifyGradingKeyOnFinalPage(gradeName: string) { - const gradingKeyCard = this.page.locator('jhi-collapsible-card').filter({ hasText: 'Grading Key Grade Interval' }); - await gradingKeyCard.locator('button.rotate-icon').click(); - await expect(gradingKeyCard.locator('tr.highlighted').locator('td', { hasText: gradeName })).toBeVisible(); - } } diff --git a/src/test/playwright/support/pageobjects/exam/ModalDialogBox.ts b/src/test/playwright/support/pageobjects/exam/ModalDialogBox.ts index e627d21fcffb..77d7a06f2168 100644 --- a/src/test/playwright/support/pageobjects/exam/ModalDialogBox.ts +++ b/src/test/playwright/support/pageobjects/exam/ModalDialogBox.ts @@ -8,7 +8,7 @@ export class ModalDialogBox { this.page = page; } - private getModalDialogContent() { + getModalDialogContent() { return this.page.locator('.modal-content'); } @@ -35,6 +35,6 @@ export class ModalDialogBox { } async closeDialog() { - await this.getModalDialogContent().locator('button').nth(0).click({ force: true }); + await this.getModalDialogContent().locator('button').click({ force: true }); } } From ffcdb3a05b5ac11e02b3966727d65f1819a2cb47 Mon Sep 17 00:00:00 2001 From: Murad Talibov Date: Thu, 23 May 2024 15:24:28 +0200 Subject: [PATCH 8/8] Avoids 2 announcements appearing on top of each other by not ending the exam --- .../playwright/e2e/exam/ExamParticipation.spec.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/test/playwright/e2e/exam/ExamParticipation.spec.ts b/src/test/playwright/e2e/exam/ExamParticipation.spec.ts index 955016fcfe34..46c8fe0e6e01 100644 --- a/src/test/playwright/e2e/exam/ExamParticipation.spec.ts +++ b/src/test/playwright/e2e/exam/ExamParticipation.spec.ts @@ -336,23 +336,21 @@ test.describe('Exam participation', () => { } await examManagement.openEditWorkingTimeDialog(); - await examManagement.changeExamWorkingTime({ hours: -1 }); - await examManagement.verifyExamWorkingTimeChange('1h 2min', '2min'); + await examManagement.changeExamWorkingTime({ minutes: -30 }); + await examManagement.verifyExamWorkingTimeChange('1h 2min', '32min'); + const workingTimeChangeTime = dayjs(); await examManagement.confirmWorkingTimeChange(exam.title!); for (const studentPage of studentPages) { const examParticipationActions = new ExamParticipationActions(studentPage); const modalDialog = new ModalDialogBox(studentPage); - // There are two dialogs shown on top of each other. We close the one - // on top for now as a workaround to avoid strict mode violation. - await modalDialog.getModalDialogContent().locator('button').last().click(); const timeChangeMessage = 'The working time of the exam has been changed.'; - await modalDialog.checkExamTimeChangeDialog('1h 2min', '2min'); - await modalDialog.checkDialogTime(dayjs()); + await modalDialog.checkExamTimeChangeDialog('1h 2min', '32min'); + await modalDialog.checkDialogTime(workingTimeChangeTime); await modalDialog.checkDialogMessage(timeChangeMessage); await modalDialog.checkDialogAuthor(instructor.username); - await examParticipationActions.checkExamTimeLeft('0min'); await modalDialog.closeDialog(); + await examParticipationActions.checkExamTimeLeft('29min'); } }); });