From 6922cf89c3cec1c28fb0da13ef7c46bc1fa0827a Mon Sep 17 00:00:00 2001 From: Murad Talibov Date: Mon, 16 Dec 2024 01:21:19 +0100 Subject: [PATCH 01/10] Mark html elements with test ids to improve testability --- .../exam-checklist.component.html | 27 ++++++++++++------- .../checklist-check.component.html | 4 +-- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-checklist.component.html b/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-checklist.component.html index d0525ddc1928..dc704cde6ea2 100644 --- a/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-checklist.component.html +++ b/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-checklist.component.html @@ -50,7 +50,10 @@

  • - +
  • @@ -63,6 +66,7 @@

  • - +
  • - +
  • - +
  • @@ -139,7 +143,7 @@

  • - +
  • @@ -167,7 +171,7 @@

  • - +
  • @@ -191,7 +195,10 @@

    @if (examChecklist) {
  • - +
  • } @@ -461,7 +468,7 @@

  • - +
  • @if (exam.publishResultsDate) { @@ -580,13 +587,13 @@

    • - +
    • - +
    • diff --git a/src/main/webapp/app/shared/components/checklist-check/checklist-check.component.html b/src/main/webapp/app/shared/components/checklist-check/checklist-check.component.html index cae4278c4ea8..bc24834155d5 100644 --- a/src/main/webapp/app/shared/components/checklist-check/checklist-check.component.html +++ b/src/main/webapp/app/shared/components/checklist-check/checklist-check.component.html @@ -1,6 +1,6 @@ @if (checkAttribute) { - + } @else { - + }   From 64c1f553130a1e07f5f035aa90128a618878f256 Mon Sep 17 00:00:00 2001 From: Murad Talibov Date: Mon, 16 Dec 2024 16:45:37 +0100 Subject: [PATCH 02/10] Adds required POM functions for checklists, small adjustments to POMs --- .../e2e/exam/ExamParticipation.spec.ts | 70 ++++++++++--------- src/test/playwright/support/fixtures.ts | 5 -- .../support/pageobjects/exam/EditExamPage.ts | 13 ---- .../pageobjects/exam/ExamDetailsPage.ts | 36 ++++++++++ .../exam/ExamExerciseGroupCreationPage.ts | 8 +-- .../support/requests/ExerciseAPIRequests.ts | 11 ++- 6 files changed, 86 insertions(+), 57 deletions(-) delete mode 100644 src/test/playwright/support/pageobjects/exam/EditExamPage.ts diff --git a/src/test/playwright/e2e/exam/ExamParticipation.spec.ts b/src/test/playwright/e2e/exam/ExamParticipation.spec.ts index f8a08642e70e..ca86e188d575 100644 --- a/src/test/playwright/e2e/exam/ExamParticipation.spec.ts +++ b/src/test/playwright/e2e/exam/ExamParticipation.spec.ts @@ -260,7 +260,7 @@ test.describe('Exam participation', () => { }); }); - test.describe('Exam announcements', { tag: '@slow' }, () => { + test.describe('Exam announcements', () => { let exam: Exam; const students = [studentOne, studentTwo]; let exercise: Exercise; @@ -278,42 +278,46 @@ test.describe('Exam participation', () => { await examAPIRequests.prepareExerciseStartForExam(exam); }); - 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!); - await examManagement.openExam(exam.id!); + test( + 'Instructor sends an announcement message and all participants receive it', + { tag: '@slow' }, + async ({ browser, login, navigationBar, courseManagement, examManagement }) => { + await login(instructor); + await navigationBar.openCourseManagement(); + await courseManagement.openExamsOfCourse(course.id!); + await examManagement.openExam(exam.id!); - const studentPages = []; + const studentPages = []; - for (const student of [studentOne, studentTwo]) { - const studentContext = await browser.newContext(); - const studentPage = await studentContext.newPage(); - studentPages.push(studentPage); + 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); - } + 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.openAnnouncementDialog(); - const announcementTypingTime = dayjs(); - await examManagement.typeAnnouncementMessage(announcement); - await examManagement.verifyAnnouncementContent(announcementTypingTime, announcement, instructor.username); - await examManagement.sendAnnouncement(); + const announcement = 'Important announcement!'; + await examManagement.openAnnouncementDialog(); + const announcementTypingTime = dayjs(); + await examManagement.typeAnnouncementMessage(announcement); + await examManagement.verifyAnnouncementContent(announcementTypingTime, announcement, instructor.username); + await examManagement.sendAnnouncement(); - for (const studentPage of studentPages) { - const modalDialog = new ModalDialogBox(studentPage); - await modalDialog.checkDialogTime(announcementTypingTime); - await modalDialog.checkDialogMessage(announcement); - await modalDialog.checkDialogAuthor(instructor.username); - await modalDialog.closeDialog(); - } - }); + for (const studentPage of studentPages) { + const modalDialog = new ModalDialogBox(studentPage); + await modalDialog.checkDialogTime(announcementTypingTime); + await modalDialog.checkDialogMessage(announcement); + await modalDialog.checkDialogAuthor(instructor.username); + await modalDialog.closeDialog(); + } + }, + ); - test('Instructor changes working time and all participants are informed', async ({ browser, login, navigationBar, courseManagement, examManagement }) => { + test('Instructor changes working time and all participants are informed', { tag: '@slow' }, async ({ browser, login, navigationBar, courseManagement, examManagement }) => { await login(instructor); await navigationBar.openCourseManagement(); await courseManagement.openExamsOfCourse(course.id!); @@ -354,7 +358,7 @@ test.describe('Exam participation', () => { test( 'Instructor changes problem statement and all participants are informed', { tag: '@fast' }, - async ({ browser, login, navigationBar, courseManagement, examManagement, examExerciseGroups, editExam, textExerciseCreation }) => { + async ({ browser, login, navigationBar, courseManagement, examManagement, examExerciseGroups, examDetails, textExerciseCreation }) => { await login(instructor); await navigationBar.openCourseManagement(); await courseManagement.openExamsOfCourse(course.id!); @@ -375,7 +379,7 @@ test.describe('Exam participation', () => { await examNavigation.openOrSaveExerciseByTitle(exercise.exerciseGroup!.title!); } - await editExam.openExerciseGroups(); + await examDetails.openExerciseGroups(); await examExerciseGroups.clickEditExercise(exercise.exerciseGroup!.id!, exercise.id!); const problemStatementText = textExerciseTemplate.problemStatement; diff --git a/src/test/playwright/support/fixtures.ts b/src/test/playwright/support/fixtures.ts index 24d95e944326..dbf28d32cc37 100644 --- a/src/test/playwright/support/fixtures.ts +++ b/src/test/playwright/support/fixtures.ts @@ -67,7 +67,6 @@ 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 @@ -97,7 +96,6 @@ export type ArtemisPageObjects = { courseCommunication: CourseCommunicationPage; lectureManagement: LectureManagementPage; lectureCreation: LectureCreationPage; - editExam: EditExamPage; examCreation: ExamCreationPage; examDetails: ExamDetailsPage; examExerciseGroupCreation: ExamExerciseGroupCreationPage; @@ -221,9 +219,6 @@ 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 deleted file mode 100644 index 69608a61522c..000000000000 --- a/src/test/playwright/support/pageobjects/exam/EditExamPage.ts +++ /dev/null @@ -1,13 +0,0 @@ -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/ExamDetailsPage.ts b/src/test/playwright/support/pageobjects/exam/ExamDetailsPage.ts index 29bc3b8aa8a0..b770a1c53a9b 100644 --- a/src/test/playwright/support/pageobjects/exam/ExamDetailsPage.ts +++ b/src/test/playwright/support/pageobjects/exam/ExamDetailsPage.ts @@ -10,6 +10,28 @@ export class ExamDetailsPage { this.page = page; } + async openExerciseGroups() { + await this.page.locator(`#exercises-button-groups`).click(); + } + + async checkItemChecked(checklistItem: ExamChecklistItem) { + await expect( + this.getChecklistItemLocator(checklistItem).getByTestId('check-icon-checked'), + `Checklist item for \"${checklistItem}\" is not checked or not found`, + ).toBeVisible(); + } + + async checkItemUnchecked(checklistItem: ExamChecklistItem) { + await expect( + this.getChecklistItemLocator(checklistItem).getByTestId('check-icon-unchecked'), + `Checklist item for \"${checklistItem}\" is not unchecked or not found`, + ).toBeVisible(); + } + + private getChecklistItemLocator(checklistItem: ExamChecklistItem) { + return this.page.getByTestId(checklistItem); + } + /** * Deletes this exam. * @param examTitle the exam title to confirm the deletion @@ -23,3 +45,17 @@ export class ExamDetailsPage { await deleteButton.click(); } } + +export enum ExamChecklistItem { + LEAST_ONE_EXERCISE_GROUP = 'check-least-one-exercise-group', + NUMBER_OF_EXERCISE_GROUPS = 'check-number-of-exercise-groups', + EACH_EXERCISE_GROUP_HAS_EXERCISES = 'check-each-exercise-group-has-exercises', + POINTS_IN_EXERCISE_GROUPS_EQUAL = 'check-points-in-exercise-groups-equal', + TOTAL_POINTS_POSSIBLE = 'check-total-points-possible', + LEAST_ONE_STUDENT = 'check-least-one-student', + ALL_EXAMS_GENERATED = 'check-all-exams-generated', + ALL_EXERCISES_PREPARED = 'check-all-exercises-prepared', + PUBLISHING_DATE_SET = 'check-publishing-date-set', + START_DATE_REVIEW_SET = 'check-start-date-review-set', + END_DATE_REVIEW_SET = 'check-end-date-review-set', +} diff --git a/src/test/playwright/support/pageobjects/exam/ExamExerciseGroupCreationPage.ts b/src/test/playwright/support/pageobjects/exam/ExamExerciseGroupCreationPage.ts index 435202bf7971..793a36852b9b 100644 --- a/src/test/playwright/support/pageobjects/exam/ExamExerciseGroupCreationPage.ts +++ b/src/test/playwright/support/pageobjects/exam/ExamExerciseGroupCreationPage.ts @@ -44,8 +44,8 @@ export class ExamExerciseGroupCreationPage { await responsePromise; } - async addGroupWithExercise(exam: Exam, exerciseType: ExerciseType, additionalData: AdditionalData = {}): Promise { - const response = await this.handleAddGroupWithExercise(exam, 'Exercise ' + generateUUID(), exerciseType, additionalData); + async addGroupWithExercise(exam: Exam, exerciseType: ExerciseType, additionalData: AdditionalData = {}, exerciseTemplate?: any): Promise { + const response = await this.handleAddGroupWithExercise(exam, 'Exercise ' + generateUUID(), exerciseType, additionalData, exerciseTemplate); let exercise = { ...response!, additionalData }; if (exerciseType == ExerciseType.QUIZ) { const quiz = response as QuizExercise; @@ -61,11 +61,11 @@ export class ExamExerciseGroupCreationPage { return exercise; } - async handleAddGroupWithExercise(exam: Exam, title: string, exerciseType: ExerciseType, additionalData: AdditionalData): Promise { + async handleAddGroupWithExercise(exam: Exam, title: string, exerciseType: ExerciseType, additionalData: AdditionalData, exerciseTemplate?: any): Promise { const exerciseGroup = await this.examAPIRequests.addExerciseGroupForExam(exam); switch (exerciseType) { case ExerciseType.TEXT: - return await this.exerciseAPIRequests.createTextExercise({ exerciseGroup }, title); + return await this.exerciseAPIRequests.createTextExercise({ exerciseGroup }, title, exerciseTemplate); case ExerciseType.MODELING: return await this.exerciseAPIRequests.createModelingExercise({ exerciseGroup }, title); case ExerciseType.QUIZ: diff --git a/src/test/playwright/support/requests/ExerciseAPIRequests.ts b/src/test/playwright/support/requests/ExerciseAPIRequests.ts index 20d55b216fa8..d8127be552c2 100644 --- a/src/test/playwright/support/requests/ExerciseAPIRequests.ts +++ b/src/test/playwright/support/requests/ExerciseAPIRequests.ts @@ -217,10 +217,16 @@ export class ExerciseAPIRequests { * * @param body - An object containing either the course or exercise group the exercise will be added to. * @param title - The title for the text exercise (optional, default: auto-generated). + * @param exerciseTemplate - The template for the text exercise + * (optional, default: textExerciseTemplate - default template). */ - async createTextExercise(body: { course: Course } | { exerciseGroup: ExerciseGroup }, title = 'Text ' + generateUUID()): Promise { + async createTextExercise( + body: { course: Course } | { exerciseGroup: ExerciseGroup }, + title = 'Text ' + generateUUID(), + exerciseTemplate: any = textExerciseTemplate, + ): Promise { const template = { - ...textExerciseTemplate, + ...exerciseTemplate, title, channelName: 'exercise-' + titleLowercase(title), }; @@ -272,6 +278,7 @@ export class ExerciseAPIRequests { * * @param exerciseId - The ID of the text exercise for which the submission is made. * @param text - The text content of the submission. + * @param createNewSubmission - Whether to create a new submission or update an existing one (optional, default: true). */ async makeTextExerciseSubmission(exerciseId: number, text: string, createNewSubmission = true) { const url = `${EXERCISE_BASE}/${exerciseId}/text-submissions`; From 88fb9a25dd5ea0cca9f4e8dba516a6a87b6303b5 Mon Sep 17 00:00:00 2001 From: Murad Talibov Date: Mon, 16 Dec 2024 21:58:50 +0100 Subject: [PATCH 03/10] Tests for exam exercise setup, student registration, exam generation and dates set checks --- .../exam-checklist.component.html | 6 +- .../e2e/exam/ExamChecklists.spec.ts | 233 ++++++++++++++++++ .../pageobjects/exam/ExamCreationPage.ts | 21 ++ .../pageobjects/exam/ExamDetailsPage.ts | 20 ++ .../exam/ExamExerciseGroupCreationPage.ts | 35 ++- .../exam/StudentExamManagementPage.ts | 7 +- .../support/requests/ExamAPIRequests.ts | 8 +- 7 files changed, 320 insertions(+), 10 deletions(-) create mode 100644 src/test/playwright/e2e/exam/ExamChecklists.spec.ts diff --git a/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-checklist.component.html b/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-checklist.component.html index dc704cde6ea2..2af08af92ea8 100644 --- a/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-checklist.component.html +++ b/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-checklist.component.html @@ -154,7 +154,7 @@

      + @@ -178,7 +178,7 @@

      + @@ -205,7 +205,7 @@

      + diff --git a/src/test/playwright/e2e/exam/ExamChecklists.spec.ts b/src/test/playwright/e2e/exam/ExamChecklists.spec.ts new file mode 100644 index 000000000000..0e1ae7c6cb9d --- /dev/null +++ b/src/test/playwright/e2e/exam/ExamChecklists.spec.ts @@ -0,0 +1,233 @@ +import { test } from '../../support/fixtures'; +import { admin, instructor, studentOne } from '../../support/users'; +import { Course } from 'app/entities/course.model'; +import { Exam } from 'app/entities/exam/exam.model'; +import { generateUUID } from '../../support/utils'; +import dayjs from 'dayjs'; +import { ExamChecklistItem } from '../../support/pageobjects/exam/ExamDetailsPage'; +import { ExerciseType } from '../../support/constants'; +import textExerciseTemplate from '../../fixtures/exercise/text/template.json'; +import { ExamExerciseGroupCreationPage } from '../../support/pageobjects/exam/ExamExerciseGroupCreationPage'; +import { ExamExerciseGroupsPage } from '../../support/pageobjects/exam/ExamExerciseGroupsPage'; +import { Page } from '@playwright/test'; + +test.describe('Exam Checklists', async () => { + let course: Course; + let exam: Exam; + let examStartDate: dayjs.Dayjs; + let examEndDate: dayjs.Dayjs; + + const NUMBER_OF_EXERCISES = 2; + const EXAM_MAX_POINTS = NUMBER_OF_EXERCISES * 10; + + test.beforeEach('Create course', async ({ login, courseManagementAPIRequests }) => { + await login(admin); + course = await courseManagementAPIRequests.createCourse({ customizeGroups: true }); + }); + + test.beforeEach('Create exam', async ({ login, examAPIRequests, courseManagementAPIRequests }) => { + await login(admin); + examStartDate = dayjs().add(1, 'hour'); + examEndDate = dayjs().add(2, 'hour'); + const examConfig = { + course, + title: 'exam' + generateUUID(), + visibleDate: dayjs().subtract(3, 'minutes'), + startDate: examStartDate, + endDate: examEndDate, + examMaxPoints: EXAM_MAX_POINTS, + numberOfExercisesInExam: NUMBER_OF_EXERCISES, + }; + exam = await examAPIRequests.createExam(examConfig); + await courseManagementAPIRequests.addStudentToCourse(course, studentOne); + }); + + test.describe('Exercise group checks', { tag: '@fast' }, () => { + test('Instructor adds an exercise group and at least one exercise group check is marked', async ({ + page, + login, + examDetails, + examExerciseGroups, + examExerciseGroupCreation, + }) => { + await login(instructor); + await navigateToExamDetailsPage(page, course, exam); + await examDetails.checkItemUnchecked(ExamChecklistItem.LEAST_ONE_EXERCISE_GROUP); + await examDetails.openExerciseGroups(); + await examExerciseGroups.clickAddExerciseGroup(); + await examExerciseGroupCreation.typeTitle('Group 1'); + await examExerciseGroupCreation.clickSave(); + await navigateToExamDetailsPage(page, course, exam); + await examDetails.checkItemChecked(ExamChecklistItem.LEAST_ONE_EXERCISE_GROUP); + }); + + test('Instructor adds exercise groups and the number of exercise groups check is correctly reacting to changes', async ({ + page, + login, + examDetails, + examExerciseGroups, + examExerciseGroupCreation, + }) => { + await login(instructor); + await navigateToExamDetailsPage(page, course, exam); + await examDetails.checkItemUnchecked(ExamChecklistItem.NUMBER_OF_EXERCISE_GROUPS); + await examDetails.openExerciseGroups(); + for (let i = 0; i < NUMBER_OF_EXERCISES; i++) { + await addExamExerciseGroup(examExerciseGroups, examExerciseGroupCreation); + } + await navigateToExamDetailsPage(page, course, exam); + await examDetails.openExerciseGroups(); + await addExamExerciseGroup(examExerciseGroups, examExerciseGroupCreation, false); + await navigateToExamDetailsPage(page, course, exam); + await examDetails.checkItemChecked(ExamChecklistItem.NUMBER_OF_EXERCISE_GROUPS); + await examDetails.openExerciseGroups(); + await addExamExerciseGroup(examExerciseGroups, examExerciseGroupCreation); + await navigateToExamDetailsPage(page, course, exam); + await examDetails.checkItemUnchecked(ExamChecklistItem.NUMBER_OF_EXERCISE_GROUPS); + }); + + test('Instructor adds exercise groups and each exercise group has exercises check is correctly reacting to changes', async ({ + page, + login, + examDetails, + examExerciseGroups, + examExerciseGroupCreation, + }) => { + await login(instructor); + await navigateToExamDetailsPage(page, course, exam); + await examDetails.checkItemUnchecked(ExamChecklistItem.EACH_EXERCISE_GROUP_HAS_EXERCISES); + await examExerciseGroupCreation.addGroupWithExercise(exam, ExerciseType.TEXT); + await page.reload(); + await examDetails.checkItemChecked(ExamChecklistItem.EACH_EXERCISE_GROUP_HAS_EXERCISES); + await examDetails.openExerciseGroups(); + await examExerciseGroups.clickAddExerciseGroup(); + await examExerciseGroupCreation.typeTitle('Empty group'); + await examExerciseGroupCreation.clickSave(); + await navigateToExamDetailsPage(page, course, exam); + await examDetails.checkItemUnchecked(ExamChecklistItem.EACH_EXERCISE_GROUP_HAS_EXERCISES); + }); + + test('Instructor adds exercise groups and points in exercise groups equal check is correctly reacting to changes', async ({ + page, + login, + examDetails, + examAPIRequests, + exerciseAPIRequests, + }) => { + await login(instructor); + await navigateToExamDetailsPage(page, course, exam); + await examDetails.checkItemUnchecked(ExamChecklistItem.POINTS_IN_EXERCISE_GROUPS_EQUAL); + const exerciseGroup = await examAPIRequests.addExerciseGroupForExam(exam); + await exerciseAPIRequests.createTextExercise({ exerciseGroup }, 'Exercise ' + generateUUID(), textExerciseTemplate); + await page.reload(); + await examDetails.checkItemChecked(ExamChecklistItem.POINTS_IN_EXERCISE_GROUPS_EQUAL); + const maxPointsOfFirstExercise = textExerciseTemplate.maxPoints; + const exerciseTemplate = textExerciseTemplate; + exerciseTemplate.maxPoints = maxPointsOfFirstExercise - 1; + await exerciseAPIRequests.createTextExercise({ exerciseGroup }, 'Exercise ' + generateUUID(), exerciseTemplate); + await page.reload(); + await examDetails.checkItemUnchecked(ExamChecklistItem.POINTS_IN_EXERCISE_GROUPS_EQUAL); + }); + + test('Instructor adds exercise groups and total points possible check is correctly reacting to changes', async ({ + page, + login, + examDetails, + examExerciseGroupCreation, + }) => { + await login(instructor); + await navigateToExamDetailsPage(page, course, exam); + await examDetails.checkItemUnchecked(ExamChecklistItem.TOTAL_POINTS_POSSIBLE); + await examExerciseGroupCreation.addGroupWithExercise(exam, ExerciseType.TEXT); + await page.reload(); + await examDetails.checkItemUnchecked(ExamChecklistItem.TOTAL_POINTS_POSSIBLE); + await examExerciseGroupCreation.addGroupWithExercise(exam, ExerciseType.TEXT, {}, false); + await page.reload(); + await examDetails.checkItemChecked(ExamChecklistItem.TOTAL_POINTS_POSSIBLE); + await examExerciseGroupCreation.addGroupWithExercise(exam, ExerciseType.TEXT); + await page.reload(); + await examDetails.checkItemChecked(ExamChecklistItem.TOTAL_POINTS_POSSIBLE); + await examExerciseGroupCreation.addGroupWithExercise(exam, ExerciseType.TEXT); + await page.reload(); + await examDetails.checkItemUnchecked(ExamChecklistItem.TOTAL_POINTS_POSSIBLE); + }); + }); + + test('Instructor registers a student to exam and at least one student check is marked', async ({ page, login, examDetails, studentExamManagement }) => { + await login(instructor); + await navigateToExamDetailsPage(page, course, exam); + await examDetails.checkItemUnchecked(ExamChecklistItem.LEAST_ONE_STUDENT); + await examDetails.clickStudentsToRegister(); + await studentExamManagement.clickRegisterCourseStudents(); + await navigateToExamDetailsPage(page, course, exam); + await examDetails.checkItemChecked(ExamChecklistItem.LEAST_ONE_STUDENT); + }); + + test.describe('Individual exam generation and exam preparation checks', { tag: '@fast' }, () => { + test.beforeEach('Add exercise groups and register exam students', async ({ login, examExerciseGroupCreation, examAPIRequests }) => { + await login(admin); + for (let i = 0; i < NUMBER_OF_EXERCISES; i++) { + await examExerciseGroupCreation.addGroupWithExercise(exam, ExerciseType.TEXT); + } + await examAPIRequests.registerAllCourseStudentsForExam(exam); + }); + + test('Instructor generates individual exams, prepares exercises for start and corresponding checks are marked', async ({ + page, + login, + examDetails, + studentExamManagement, + }) => { + await login(instructor); + await navigateToExamDetailsPage(page, course, exam); + await examDetails.checkItemUnchecked(ExamChecklistItem.ALL_EXAMS_GENERATED); + await examDetails.checkItemUnchecked(ExamChecklistItem.ALL_EXERCISES_PREPARED); + await examDetails.clickStudentExamsToGenerate(); + await studentExamManagement.clickGenerateStudentExams(); + await navigateToExamDetailsPage(page, course, exam); + await examDetails.checkItemChecked(ExamChecklistItem.ALL_EXAMS_GENERATED); + await examDetails.checkItemUnchecked(ExamChecklistItem.ALL_EXERCISES_PREPARED); + await examDetails.clickStudentExamsToPrepareStart(); + await studentExamManagement.clickPrepareExerciseStart(); + await navigateToExamDetailsPage(page, course, exam); + await examDetails.checkItemChecked(ExamChecklistItem.ALL_EXAMS_GENERATED); + await examDetails.checkItemChecked(ExamChecklistItem.ALL_EXERCISES_PREPARED); + }); + }); + + test('Instructor sets the publish results and review dates and the corresponding checks are marked', async ({ page, login, examDetails, examCreation }) => { + await login(instructor); + await navigateToExamDetailsPage(page, course, exam); + await examDetails.checkItemUnchecked(ExamChecklistItem.PUBLISHING_DATE_SET); + await examDetails.checkItemUnchecked(ExamChecklistItem.START_DATE_REVIEW_SET); + await examDetails.checkItemUnchecked(ExamChecklistItem.END_DATE_REVIEW_SET); + await examDetails.clickEditExamForPublishDate(); + await examCreation.setPublishResultsDate(examEndDate.add(1, 'hour')); + await examCreation.update(); + await page.waitForURL(`**/exams/${exam.id}`); + await examDetails.checkItemChecked(ExamChecklistItem.PUBLISHING_DATE_SET); + await examDetails.checkItemUnchecked(ExamChecklistItem.START_DATE_REVIEW_SET); + await examDetails.checkItemUnchecked(ExamChecklistItem.END_DATE_REVIEW_SET); + await examDetails.clickEditExamForReviewDate(); + await examCreation.setStudentReviewStartDate(examEndDate.add(2, 'hour')); + await examCreation.setStudentReviewEndDate(examEndDate.add(1, 'day')); + await examCreation.update(); + await page.waitForURL(`**/exams/${exam.id}`); + await examDetails.checkItemChecked(ExamChecklistItem.PUBLISHING_DATE_SET); + await examDetails.checkItemChecked(ExamChecklistItem.START_DATE_REVIEW_SET); + await examDetails.checkItemChecked(ExamChecklistItem.END_DATE_REVIEW_SET); + }); +}); + +async function navigateToExamDetailsPage(page: Page, course: Course, exam: Exam) { + await page.goto(`/course-management/${course.id}/exams/${exam.id}`); +} + +async function addExamExerciseGroup(examExerciseGroups: ExamExerciseGroupsPage, examExerciseGroupCreation: ExamExerciseGroupCreationPage, isMandatory?: boolean) { + await examExerciseGroups.clickAddExerciseGroup(); + await examExerciseGroupCreation.typeTitle(`Group ${generateUUID()}`); + if (isMandatory !== undefined) { + await examExerciseGroupCreation.setMandatoryBox(isMandatory); + } + await examExerciseGroupCreation.clickSave(); +} diff --git a/src/test/playwright/support/pageobjects/exam/ExamCreationPage.ts b/src/test/playwright/support/pageobjects/exam/ExamCreationPage.ts index e710822e6723..8fe6ad86c5d3 100644 --- a/src/test/playwright/support/pageobjects/exam/ExamCreationPage.ts +++ b/src/test/playwright/support/pageobjects/exam/ExamCreationPage.ts @@ -51,6 +51,27 @@ export class ExamCreationPage { await enterDate(this.page, '#endDate', date); } + /** + * @param date the date when the exam results will be published + */ + async setPublishResultsDate(date: dayjs.Dayjs) { + await enterDate(this.page, '#publishResultsDate', date); + } + + /** + * @param date the date when the exam student review starts + */ + async setStudentReviewStartDate(date: dayjs.Dayjs) { + await enterDate(this.page, '#examStudentReviewStart', date); + } + + /** + * @param date the date when the exam student review ends + */ + async setStudentReviewEndDate(date: dayjs.Dayjs) { + await enterDate(this.page, '#examStudentReviewEnd', date); + } + /** * @param time the exam working time */ diff --git a/src/test/playwright/support/pageobjects/exam/ExamDetailsPage.ts b/src/test/playwright/support/pageobjects/exam/ExamDetailsPage.ts index b770a1c53a9b..4f103e1b5f55 100644 --- a/src/test/playwright/support/pageobjects/exam/ExamDetailsPage.ts +++ b/src/test/playwright/support/pageobjects/exam/ExamDetailsPage.ts @@ -32,6 +32,26 @@ export class ExamDetailsPage { return this.page.getByTestId(checklistItem); } + async clickStudentsToRegister() { + await this.page.getByTestId('students-button-register').click(); + } + + async clickStudentExamsToGenerate() { + await this.page.getByTestId('student-exams-button-generate').click(); + } + + async clickStudentExamsToPrepareStart() { + await this.page.getByTestId('student-exams-button-prepare-start').click(); + } + + async clickEditExamForPublishDate() { + await this.page.locator('#editButton_publish').click(); + } + + async clickEditExamForReviewDate() { + await this.page.locator('#editButton_review').click(); + } + /** * Deletes this exam. * @param examTitle the exam title to confirm the deletion diff --git a/src/test/playwright/support/pageobjects/exam/ExamExerciseGroupCreationPage.ts b/src/test/playwright/support/pageobjects/exam/ExamExerciseGroupCreationPage.ts index 793a36852b9b..8b9e504e9380 100644 --- a/src/test/playwright/support/pageobjects/exam/ExamExerciseGroupCreationPage.ts +++ b/src/test/playwright/support/pageobjects/exam/ExamExerciseGroupCreationPage.ts @@ -27,8 +27,20 @@ export class ExamExerciseGroupCreationPage { await titleField.fill(title); } + async setMandatoryBox(checked: boolean) { + if (checked) { + await this.getMandatoryBoxLocator().check(); + } else { + await this.getMandatoryBoxLocator().uncheck(); + } + } + async isMandatoryBoxShouldBeChecked() { - await this.page.locator('#isMandatory').isChecked(); + await this.getMandatoryBoxLocator().isChecked(); + } + + private getMandatoryBoxLocator() { + return this.page.locator('#isMandatory'); } async clickSave(): Promise { @@ -44,8 +56,14 @@ export class ExamExerciseGroupCreationPage { await responsePromise; } - async addGroupWithExercise(exam: Exam, exerciseType: ExerciseType, additionalData: AdditionalData = {}, exerciseTemplate?: any): Promise { - const response = await this.handleAddGroupWithExercise(exam, 'Exercise ' + generateUUID(), exerciseType, additionalData, exerciseTemplate); + async addGroupWithExercise( + exam: Exam, + exerciseType: ExerciseType, + additionalData: AdditionalData = {}, + isMandatory?: boolean, + exerciseTemplate?: any, + ): Promise { + const response = await this.handleAddGroupWithExercise(exam, 'Exercise ' + generateUUID(), exerciseType, additionalData, isMandatory, exerciseTemplate); let exercise = { ...response!, additionalData }; if (exerciseType == ExerciseType.QUIZ) { const quiz = response as QuizExercise; @@ -61,8 +79,15 @@ export class ExamExerciseGroupCreationPage { return exercise; } - async handleAddGroupWithExercise(exam: Exam, title: string, exerciseType: ExerciseType, additionalData: AdditionalData, exerciseTemplate?: any): Promise { - const exerciseGroup = await this.examAPIRequests.addExerciseGroupForExam(exam); + async handleAddGroupWithExercise( + exam: Exam, + title: string, + exerciseType: ExerciseType, + additionalData: AdditionalData, + isMandatory?: boolean, + exerciseTemplate?: any, + ): Promise { + const exerciseGroup = await this.examAPIRequests.addExerciseGroupForExam(exam, 'Group ' + generateUUID(), isMandatory); switch (exerciseType) { case ExerciseType.TEXT: return await this.exerciseAPIRequests.createTextExercise({ exerciseGroup }, title, exerciseTemplate); diff --git a/src/test/playwright/support/pageobjects/exam/StudentExamManagementPage.ts b/src/test/playwright/support/pageobjects/exam/StudentExamManagementPage.ts index 593fc5451872..af3c655bde51 100644 --- a/src/test/playwright/support/pageobjects/exam/StudentExamManagementPage.ts +++ b/src/test/playwright/support/pageobjects/exam/StudentExamManagementPage.ts @@ -12,7 +12,8 @@ export class StudentExamManagementPage { async clickGenerateStudentExams() { const responsePromise = this.page.waitForResponse(`${COURSE_BASE}/*/exams/*/generate-student-exams`); await this.page.click('#generateStudentExamsButton'); - await responsePromise; + const response = await responsePromise; + console.log('Generate student exams response:', await response.json()); } async clickRegisterCourseStudents() { @@ -21,6 +22,10 @@ export class StudentExamManagementPage { return await responsePromise; } + async clickPrepareExerciseStart() { + await this.page.click('#startExercisesButton'); + } + getGenerateMissingStudentExamsButton() { return this.page.locator('#generateMissingStudentExamsButton'); } diff --git a/src/test/playwright/support/requests/ExamAPIRequests.ts b/src/test/playwright/support/requests/ExamAPIRequests.ts index 34ffccb9a8fe..aab475187848 100644 --- a/src/test/playwright/support/requests/ExamAPIRequests.ts +++ b/src/test/playwright/support/requests/ExamAPIRequests.ts @@ -113,12 +113,18 @@ export class ExamAPIRequests { /** * Register the student for the exam - * @param exam the exam object */ async registerStudentForExam(exam: Exam, student: UserCredentials) { await this.page.request.post(`${COURSE_BASE}/${exam.course!.id}/exams/${exam.id}/students/${student.username}`); } + /** + * Register all course students for the exam + */ + async registerAllCourseStudentsForExam(exam: Exam) { + await this.page.request.post(`${COURSE_BASE}/${exam.course!.id}/exams/${exam.id}/register-course-students`); + } + /** * Creates an exam test run with the provided settings. * @param exam the exam object From 83fb7f9a19e213c40ec444f1c6ec57b3c6568b91 Mon Sep 17 00:00:00 2001 From: Murad Talibov Date: Wed, 18 Dec 2024 17:34:03 +0100 Subject: [PATCH 04/10] Fix potential race conditions and avoid text exercise fixture modifications --- .../e2e/exam/ExamChecklists.spec.ts | 59 +++++++++++-------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/src/test/playwright/e2e/exam/ExamChecklists.spec.ts b/src/test/playwright/e2e/exam/ExamChecklists.spec.ts index 0e1ae7c6cb9d..41faec5b334b 100644 --- a/src/test/playwright/e2e/exam/ExamChecklists.spec.ts +++ b/src/test/playwright/e2e/exam/ExamChecklists.spec.ts @@ -10,35 +10,15 @@ import textExerciseTemplate from '../../fixtures/exercise/text/template.json'; import { ExamExerciseGroupCreationPage } from '../../support/pageobjects/exam/ExamExerciseGroupCreationPage'; import { ExamExerciseGroupsPage } from '../../support/pageobjects/exam/ExamExerciseGroupsPage'; import { Page } from '@playwright/test'; +import { Commands } from '../../support/commands'; +import { ExamAPIRequests } from '../../support/requests/ExamAPIRequests'; test.describe('Exam Checklists', async () => { let course: Course; - let exam: Exam; - let examStartDate: dayjs.Dayjs; - let examEndDate: dayjs.Dayjs; - - const NUMBER_OF_EXERCISES = 2; - const EXAM_MAX_POINTS = NUMBER_OF_EXERCISES * 10; test.beforeEach('Create course', async ({ login, courseManagementAPIRequests }) => { await login(admin); course = await courseManagementAPIRequests.createCourse({ customizeGroups: true }); - }); - - test.beforeEach('Create exam', async ({ login, examAPIRequests, courseManagementAPIRequests }) => { - await login(admin); - examStartDate = dayjs().add(1, 'hour'); - examEndDate = dayjs().add(2, 'hour'); - const examConfig = { - course, - title: 'exam' + generateUUID(), - visibleDate: dayjs().subtract(3, 'minutes'), - startDate: examStartDate, - endDate: examEndDate, - examMaxPoints: EXAM_MAX_POINTS, - numberOfExercisesInExam: NUMBER_OF_EXERCISES, - }; - exam = await examAPIRequests.createExam(examConfig); await courseManagementAPIRequests.addStudentToCourse(course, studentOne); }); @@ -50,6 +30,7 @@ test.describe('Exam Checklists', async () => { examExerciseGroups, examExerciseGroupCreation, }) => { + const exam = await createExam(course, page); await login(instructor); await navigateToExamDetailsPage(page, course, exam); await examDetails.checkItemUnchecked(ExamChecklistItem.LEAST_ONE_EXERCISE_GROUP); @@ -68,11 +49,12 @@ test.describe('Exam Checklists', async () => { examExerciseGroups, examExerciseGroupCreation, }) => { + const exam = await createExam(course, page); await login(instructor); await navigateToExamDetailsPage(page, course, exam); await examDetails.checkItemUnchecked(ExamChecklistItem.NUMBER_OF_EXERCISE_GROUPS); await examDetails.openExerciseGroups(); - for (let i = 0; i < NUMBER_OF_EXERCISES; i++) { + for (let i = 0; i < exam.numberOfExercisesInExam!; i++) { await addExamExerciseGroup(examExerciseGroups, examExerciseGroupCreation); } await navigateToExamDetailsPage(page, course, exam); @@ -93,6 +75,7 @@ test.describe('Exam Checklists', async () => { examExerciseGroups, examExerciseGroupCreation, }) => { + const exam = await createExam(course, page); await login(instructor); await navigateToExamDetailsPage(page, course, exam); await examDetails.checkItemUnchecked(ExamChecklistItem.EACH_EXERCISE_GROUP_HAS_EXERCISES); @@ -114,6 +97,7 @@ test.describe('Exam Checklists', async () => { examAPIRequests, exerciseAPIRequests, }) => { + const exam = await createExam(course, page); await login(instructor); await navigateToExamDetailsPage(page, course, exam); await examDetails.checkItemUnchecked(ExamChecklistItem.POINTS_IN_EXERCISE_GROUPS_EQUAL); @@ -122,7 +106,7 @@ test.describe('Exam Checklists', async () => { await page.reload(); await examDetails.checkItemChecked(ExamChecklistItem.POINTS_IN_EXERCISE_GROUPS_EQUAL); const maxPointsOfFirstExercise = textExerciseTemplate.maxPoints; - const exerciseTemplate = textExerciseTemplate; + const exerciseTemplate = { ...textExerciseTemplate }; exerciseTemplate.maxPoints = maxPointsOfFirstExercise - 1; await exerciseAPIRequests.createTextExercise({ exerciseGroup }, 'Exercise ' + generateUUID(), exerciseTemplate); await page.reload(); @@ -135,6 +119,7 @@ test.describe('Exam Checklists', async () => { examDetails, examExerciseGroupCreation, }) => { + const exam = await createExam(course, page); await login(instructor); await navigateToExamDetailsPage(page, course, exam); await examDetails.checkItemUnchecked(ExamChecklistItem.TOTAL_POINTS_POSSIBLE); @@ -154,6 +139,7 @@ test.describe('Exam Checklists', async () => { }); test('Instructor registers a student to exam and at least one student check is marked', async ({ page, login, examDetails, studentExamManagement }) => { + const exam = await createExam(course, page); await login(instructor); await navigateToExamDetailsPage(page, course, exam); await examDetails.checkItemUnchecked(ExamChecklistItem.LEAST_ONE_STUDENT); @@ -164,9 +150,15 @@ test.describe('Exam Checklists', async () => { }); test.describe('Individual exam generation and exam preparation checks', { tag: '@fast' }, () => { + let exam: Exam; + + test.beforeEach('Create exam', async ({ page }) => { + exam = await createExam(course, page); + }); + test.beforeEach('Add exercise groups and register exam students', async ({ login, examExerciseGroupCreation, examAPIRequests }) => { await login(admin); - for (let i = 0; i < NUMBER_OF_EXERCISES; i++) { + for (let i = 0; i < exam.numberOfExercisesInExam!; i++) { await examExerciseGroupCreation.addGroupWithExercise(exam, ExerciseType.TEXT); } await examAPIRequests.registerAllCourseStudentsForExam(exam); @@ -196,12 +188,14 @@ test.describe('Exam Checklists', async () => { }); test('Instructor sets the publish results and review dates and the corresponding checks are marked', async ({ page, login, examDetails, examCreation }) => { + const exam = await createExam(course, page); await login(instructor); await navigateToExamDetailsPage(page, course, exam); await examDetails.checkItemUnchecked(ExamChecklistItem.PUBLISHING_DATE_SET); await examDetails.checkItemUnchecked(ExamChecklistItem.START_DATE_REVIEW_SET); await examDetails.checkItemUnchecked(ExamChecklistItem.END_DATE_REVIEW_SET); await examDetails.clickEditExamForPublishDate(); + const examEndDate = dayjs(exam.endDate! as dayjs.Dayjs); await examCreation.setPublishResultsDate(examEndDate.add(1, 'hour')); await examCreation.update(); await page.waitForURL(`**/exams/${exam.id}`); @@ -219,6 +213,21 @@ test.describe('Exam Checklists', async () => { }); }); +async function createExam(course: Course, page: Page) { + const NUMBER_OF_EXERCISES = 2; + const EXAM_MAX_POINTS = NUMBER_OF_EXERCISES * 10; + + await Commands.login(page, admin); + const examConfig = { + course, + examMaxPoints: EXAM_MAX_POINTS, + numberOfExercisesInExam: NUMBER_OF_EXERCISES, + }; + + const examAPIRequests = new ExamAPIRequests(page); + return await examAPIRequests.createExam(examConfig); +} + async function navigateToExamDetailsPage(page: Page, course: Course, exam: Exam) { await page.goto(`/course-management/${course.id}/exams/${exam.id}`); } From 55879fbe9897d2a3a1b859d00169a617f5af5b51 Mon Sep 17 00:00:00 2001 From: Murad Talibov Date: Thu, 19 Dec 2024 16:18:18 +0100 Subject: [PATCH 05/10] Adds tests about checks for missing assessments, unassessed quizzes and submissions --- .../exam-checklist.component.html | 15 ++++- .../e2e/exam/ExamAssessment.spec.ts | 2 +- .../e2e/exam/ExamChecklists.spec.ts | 65 ++++++++++++++++++- .../pageobjects/exam/ExamDetailsPage.ts | 11 ++++ .../support/requests/ExamAPIRequests.ts | 13 ++++ 5 files changed, 101 insertions(+), 5 deletions(-) diff --git a/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-checklist.component.html b/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-checklist.component.html index 2af08af92ea8..8e773545e824 100644 --- a/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-checklist.component.html +++ b/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-checklist.component.html @@ -494,7 +494,10 @@

      - +

      @if (isEvaluatingQuizExercises) { } @else { - + } @@ -543,7 +549,10 @@

      @if (isAssessingUnsubmittedExams) { } @else { - + } diff --git a/src/test/playwright/e2e/exam/ExamAssessment.spec.ts b/src/test/playwright/e2e/exam/ExamAssessment.spec.ts index 4881c7337718..6139d8f26cf6 100644 --- a/src/test/playwright/e2e/exam/ExamAssessment.spec.ts +++ b/src/test/playwright/e2e/exam/ExamAssessment.spec.ts @@ -370,7 +370,7 @@ async function makeExamSubmission( await examStartEnd.finishExam(); } -async function startAssessing( +export async function startAssessing( courseID: number, examID: number, timeout: number, diff --git a/src/test/playwright/e2e/exam/ExamChecklists.spec.ts b/src/test/playwright/e2e/exam/ExamChecklists.spec.ts index 41faec5b334b..ace451156933 100644 --- a/src/test/playwright/e2e/exam/ExamChecklists.spec.ts +++ b/src/test/playwright/e2e/exam/ExamChecklists.spec.ts @@ -12,6 +12,7 @@ import { ExamExerciseGroupsPage } from '../../support/pageobjects/exam/ExamExerc import { Page } from '@playwright/test'; import { Commands } from '../../support/commands'; import { ExamAPIRequests } from '../../support/requests/ExamAPIRequests'; +import { prepareExam, startAssessing } from './ExamAssessment.spec'; test.describe('Exam Checklists', async () => { let course: Course; @@ -211,14 +212,76 @@ test.describe('Exam Checklists', async () => { await examDetails.checkItemChecked(ExamChecklistItem.START_DATE_REVIEW_SET); await examDetails.checkItemChecked(ExamChecklistItem.END_DATE_REVIEW_SET); }); + + test( + 'Student makes a submission and missing assessment check is marked for instructor after assessment', + { tag: '@slow' }, + async ({ page, login, examDetails, examManagement, courseAssessment, exerciseAssessment, textExerciseAssessment, examAPIRequests }) => { + const exam = await prepareExam(course, dayjs().add(1, 'day'), ExerciseType.TEXT, page); + await login(instructor); + await examAPIRequests.finishExam(exam); + await navigateToExamDetailsPage(page, course, exam); + await examDetails.checkItemUnchecked(ExamChecklistItem.UNFINISHED_ASSESSMENTS); + await startAssessing(course.id!, exam.id!, 60000, examManagement, courseAssessment, exerciseAssessment); + await textExerciseAssessment.addNewFeedback(5, 'OK'); + await textExerciseAssessment.submit(); + await navigateToExamDetailsPage(page, course, exam); + await examDetails.checkItemChecked(ExamChecklistItem.UNFINISHED_ASSESSMENTS); + }, + ); + + test.skip( + 'Student makes a quiz submission and unassessed quizzes check is marked for instructor after assessment', + { tag: '@slow' }, + async ({ page, login, examDetails, examAPIRequests }) => { + const exam = await prepareExam(course, dayjs().add(1, 'day'), ExerciseType.QUIZ, page); + await login(instructor); + await examAPIRequests.finishExam(exam); + await navigateToExamDetailsPage(page, course, exam); + await examDetails.checkItemUnchecked(ExamChecklistItem.UNASSESSED_QUIZZES); + await examDetails.clickEvaluateQuizExercises(); + await examDetails.checkItemChecked(ExamChecklistItem.UNASSESSED_QUIZZES); + }, + ); + + test.skip( + 'Student does not submit the exam on time and corresponding check is marked', + { tag: '@slow' }, + async ({ page, login, examDetails, examAPIRequests, examExerciseGroupCreation, examParticipation }) => { + const examConfig = { + startDate: dayjs(), + endDate: dayjs().add(1, 'day'), + publishResultsDate: dayjs().add(2, 'day'), + }; + const exam = await createExam(course, page, examConfig); + for (let i = 0; i < exam.numberOfExercisesInExam!; i++) { + await examExerciseGroupCreation.addGroupWithExercise(exam, ExerciseType.TEXT, { textFixture: 'loremIpsum-short.txt' }); + } + await examAPIRequests.registerStudentForExam(exam, studentOne); + await examAPIRequests.generateMissingIndividualExams(exam); + await examAPIRequests.prepareExerciseStartForExam(exam); + await examParticipation.startParticipation(studentOne, course, exam); + await login(instructor); + await examAPIRequests.finishExam(exam); + await navigateToExamDetailsPage(page, course, exam); + await examDetails.checkItemUnchecked(ExamChecklistItem.UNSUBMITTED_EXERCISES); + await examDetails.clickAssessUnsubmittedParticipations(); + await examDetails.checkItemChecked(ExamChecklistItem.UNSUBMITTED_EXERCISES); + }, + ); + + test.afterEach('Delete course', async ({ courseManagementAPIRequests }) => { + await courseManagementAPIRequests.deleteCourse(course, admin); + }); }); -async function createExam(course: Course, page: Page) { +async function createExam(course: Course, page: Page, customConfig?: any) { const NUMBER_OF_EXERCISES = 2; const EXAM_MAX_POINTS = NUMBER_OF_EXERCISES * 10; await Commands.login(page, admin); const examConfig = { + ...customConfig, course, examMaxPoints: EXAM_MAX_POINTS, numberOfExercisesInExam: NUMBER_OF_EXERCISES, diff --git a/src/test/playwright/support/pageobjects/exam/ExamDetailsPage.ts b/src/test/playwright/support/pageobjects/exam/ExamDetailsPage.ts index 4f103e1b5f55..33e4ef66329e 100644 --- a/src/test/playwright/support/pageobjects/exam/ExamDetailsPage.ts +++ b/src/test/playwright/support/pageobjects/exam/ExamDetailsPage.ts @@ -52,6 +52,14 @@ export class ExamDetailsPage { await this.page.locator('#editButton_review').click(); } + async clickEvaluateQuizExercises() { + await this.page.locator('#evaluateQuizExercisesButton').click(); + } + + async clickAssessUnsubmittedParticipations() { + await this.page.locator('#assessUnsubmittedExamModelingAndTextParticipationsButton').click(); + } + /** * Deletes this exam. * @param examTitle the exam title to confirm the deletion @@ -78,4 +86,7 @@ export enum ExamChecklistItem { PUBLISHING_DATE_SET = 'check-publishing-date-set', START_DATE_REVIEW_SET = 'check-start-date-review-set', END_DATE_REVIEW_SET = 'check-end-date-review-set', + UNFINISHED_ASSESSMENTS = 'check-unfinished-assessments', + UNASSESSED_QUIZZES = 'check-unassessed-quizzes', + UNSUBMITTED_EXERCISES = 'check-unsubmitted-exercises', } diff --git a/src/test/playwright/support/requests/ExamAPIRequests.ts b/src/test/playwright/support/requests/ExamAPIRequests.ts index aab475187848..899e1f396e93 100644 --- a/src/test/playwright/support/requests/ExamAPIRequests.ts +++ b/src/test/playwright/support/requests/ExamAPIRequests.ts @@ -214,4 +214,17 @@ export class ExamAPIRequests { const response = await this.page.request.get(`${COURSE_BASE}/${exam.course!.id}/exams/${exam.id}/student-exams/${studentExam.id}/grade-summary`); return await response.json(); } + + /** + * Determines the time left until the exam ends and finishes the exam by subtracting it from the working time. + */ + async finishExam(exam: Exam) { + const examEndDate = dayjs(exam.endDate! as dayjs.Dayjs); + // Determine the time left until the exam ends and add extra minute + // to make sure the exam is finished after subtracting it from the working time + const examTimeLeftInSeconds = examEndDate.diff(dayjs(), 'seconds') + 60; + if (examTimeLeftInSeconds > 0) { + await this.page.request.patch(`${COURSE_BASE}/${exam.course!.id}/exams/${exam.id}/working-time`, { data: -examTimeLeftInSeconds }); + } + } } From c20598c62ec1162b595a5459c98c518f3d1e382f Mon Sep 17 00:00:00 2001 From: Murad Talibov Date: Thu, 19 Dec 2024 17:22:07 +0100 Subject: [PATCH 06/10] Move exam utility functions outside the test file to make it reusable --- .../e2e/exam/ExamAssessment.spec.ts | 124 +---------------- .../e2e/exam/ExamChecklists.spec.ts | 3 +- .../exam/StudentExamManagementPage.ts | 3 +- src/test/playwright/support/utils.ts | 127 +++++++++++++++++- 4 files changed, 134 insertions(+), 123 deletions(-) diff --git a/src/test/playwright/e2e/exam/ExamAssessment.spec.ts b/src/test/playwright/e2e/exam/ExamAssessment.spec.ts index 6139d8f26cf6..dda83bae8047 100644 --- a/src/test/playwright/e2e/exam/ExamAssessment.spec.ts +++ b/src/test/playwright/e2e/exam/ExamAssessment.spec.ts @@ -1,33 +1,19 @@ import dayjs, { Dayjs } from 'dayjs'; -import { Exercise, ExerciseType, ProgrammingExerciseAssessmentType } from '../../support/constants'; +import { Exercise, ExerciseType } from '../../support/constants'; import { admin, instructor, studentFour, studentOne, studentThree, studentTwo, tutor, users } from '../../support/users'; import { Page, expect } from '@playwright/test'; -import javaPartiallySuccessful from '../../fixtures/exercise/programming/java/partially_successful/submission.json'; - import { Course } from 'app/entities/course.model'; import { Exam } from 'app/entities/exam/exam.model'; import { Commands } from '../../support/commands'; -import { ExamAPIRequests } from '../../support/requests/ExamAPIRequests'; -import { ExamExerciseGroupCreationPage } from '../../support/pageobjects/exam/ExamExerciseGroupCreationPage'; -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'; import { CourseAssessmentDashboardPage } from '../../support/pageobjects/assessment/CourseAssessmentDashboardPage'; import { ExerciseAssessmentDashboardPage } from '../../support/pageobjects/assessment/ExerciseAssessmentDashboardPage'; import { StudentAssessmentPage } from '../../support/pageobjects/assessment/StudentAssessmentPage'; import { ExamAssessmentPage } from '../../support/pageobjects/assessment/ExamAssessmentPage'; import { test } from '../../support/fixtures'; -import { ExerciseAPIRequests } from '../../support/requests/ExerciseAPIRequests'; -import { CoursesPage } from '../../support/pageobjects/course/CoursesPage'; -import { CourseOverviewPage } from '../../support/pageobjects/course/CourseOverviewPage'; -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 { CourseManagementAPIRequests } from '../../support/requests/CourseManagementAPIRequests'; -import { generateUUID, newBrowserPage } from '../../support/utils'; +import { generateUUID, newBrowserPage, prepareExam, startAssessing } from '../../support/utils'; import examStatisticsSample from '../../fixtures/exam/statistics.json'; import { ExamScoresPage } from '../../support/pageobjects/exam/ExamScoresPage'; @@ -60,7 +46,7 @@ test.describe('Exam assessment', () => { test.beforeAll('Prepare exam', async ({ browser }) => { examEnd = dayjs().add(2, 'minutes'); const page = await newBrowserPage(browser); - await prepareExam(course, examEnd, ExerciseType.PROGRAMMING, page); + exam = await prepareExam(course, examEnd, ExerciseType.PROGRAMMING, page); }); test('Assess a programming exercise submission (MANUAL)', async ({ login, examManagement, examAssessment, examParticipation, courseAssessment, exerciseAssessment }) => { @@ -84,7 +70,7 @@ test.describe('Exam assessment', () => { test.beforeAll('Prepare exam', async ({ browser }) => { examEnd = dayjs().add(45, 'seconds'); const page = await newBrowserPage(browser); - await prepareExam(course, examEnd, ExerciseType.MODELING, page); + exam = await prepareExam(course, examEnd, ExerciseType.MODELING, page); }); test('Assess a modeling exercise submission', async ({ @@ -122,7 +108,7 @@ test.describe('Exam assessment', () => { test.beforeAll('Prepare exam', async ({ browser }) => { examEnd = dayjs().add(20, 'seconds'); const page = await newBrowserPage(browser); - await prepareExam(course, examEnd, ExerciseType.TEXT, page, 2); + exam = await prepareExam(course, examEnd, ExerciseType.TEXT, page, 2); }); test('Assess a text exercise submission', async ({ login, examManagement, examAssessment, examParticipation, courseAssessment, exerciseAssessment }) => { @@ -159,7 +145,7 @@ test.describe('Exam assessment', () => { examEnd = dayjs().add(30, 'seconds'); resultDate = examEnd.add(5, 'seconds'); const page = await newBrowserPage(browser); - await prepareExam(course, examEnd, ExerciseType.QUIZ, page); + exam = await prepareExam(course, examEnd, ExerciseType.QUIZ, page); }); test('Assesses quiz automatically', async ({ page, login, examManagement, courseAssessment, examParticipation }) => { @@ -294,104 +280,6 @@ test.afterAll('Delete course', async ({ browser }) => { await courseManagementAPIRequests.deleteCourse(course, admin); }); -export async function prepareExam(course: Course, end: dayjs.Dayjs, exerciseType: ExerciseType, page: Page, numberOfCorrectionRounds: number = 1): Promise { - const examAPIRequests = new ExamAPIRequests(page); - const exerciseAPIRequests = new ExerciseAPIRequests(page); - const examExerciseGroupCreation = new ExamExerciseGroupCreationPage(page, examAPIRequests, exerciseAPIRequests); - const courseList = new CoursesPage(page); - const courseOverview = new CourseOverviewPage(page); - const modelingExerciseEditor = new ModelingEditor(page); - const programmingExerciseEditor = new OnlineEditorPage(page); - const quizExerciseMultipleChoice = new MultipleChoiceQuiz(page); - const textExerciseEditor = new TextEditorPage(page); - const examNavigation = new ExamNavigationBar(page); - const examStartEnd = new ExamStartEndPage(page); - const examParticipation = new ExamParticipationPage( - courseList, - courseOverview, - examNavigation, - examStartEnd, - modelingExerciseEditor, - programmingExerciseEditor, - quizExerciseMultipleChoice, - textExerciseEditor, - page, - ); - - await Commands.login(page, admin); - const resultDate = end.add(1, 'second'); - const examConfig = { - course, - startDate: dayjs(), - endDate: end, - numberOfCorrectionRoundsInExam: numberOfCorrectionRounds, - examStudentReviewStart: resultDate, - examStudentReviewEnd: resultDate.add(1, 'minute'), - publishResultsDate: resultDate, - gracePeriod: 10, - }; - exam = await examAPIRequests.createExam(examConfig); - let additionalData = {}; - switch (exerciseType) { - case ExerciseType.PROGRAMMING: - additionalData = { submission: javaPartiallySuccessful, progExerciseAssessmentType: ProgrammingExerciseAssessmentType.SEMI_AUTOMATIC }; - break; - case ExerciseType.TEXT: - additionalData = { textFixture: 'loremIpsum-short.txt' }; - break; - case ExerciseType.QUIZ: - additionalData = { quizExerciseID: 0 }; - break; - } - - const exercise = await examExerciseGroupCreation.addGroupWithExercise(exam, exerciseType, additionalData); - await examAPIRequests.registerStudentForExam(exam, studentOne); - await examAPIRequests.generateMissingIndividualExams(exam); - await examAPIRequests.prepareExerciseStartForExam(exam); - exercise.additionalData = additionalData; - await makeExamSubmission(course, exam, exercise, page, examParticipation, examNavigation, examStartEnd); - return exam; -} - -async function makeExamSubmission( - course: Course, - exam: Exam, - exercise: Exercise, - page: Page, - examParticipation: ExamParticipationPage, - examNavigation: ExamNavigationBar, - examStartEnd: ExamStartEndPage, -) { - await examParticipation.startParticipation(studentOne, course, exam); - await examNavigation.openOrSaveExerciseByTitle(exercise.exerciseGroup!.title!); - await examParticipation.makeSubmission(exercise.id!, exercise.type!, exercise.additionalData); - await page.waitForTimeout(2000); - await examNavigation.handInEarly(); - await examStartEnd.finishExam(); -} - -export async function startAssessing( - courseID: number, - examID: number, - timeout: number, - examManagement: ExamManagementPage, - courseAssessment: CourseAssessmentDashboardPage, - exerciseAssessment: ExerciseAssessmentDashboardPage, - toggleSecondRound: boolean = false, - isFirstTimeAssessing: boolean = true, -) { - await examManagement.openAssessmentDashboard(courseID, examID, timeout); - await courseAssessment.clickExerciseDashboardButton(); - if (toggleSecondRound) { - await exerciseAssessment.toggleSecondCorrectionRound(); - } - if (isFirstTimeAssessing) { - await exerciseAssessment.clickHaveReadInstructionsButton(); - } - await exerciseAssessment.clickStartNewAssessment(); - exerciseAssessment.getLockedMessage(); -} - async function handleComplaint( course: Course, exam: Exam, diff --git a/src/test/playwright/e2e/exam/ExamChecklists.spec.ts b/src/test/playwright/e2e/exam/ExamChecklists.spec.ts index ace451156933..efe5f86b5330 100644 --- a/src/test/playwright/e2e/exam/ExamChecklists.spec.ts +++ b/src/test/playwright/e2e/exam/ExamChecklists.spec.ts @@ -2,7 +2,7 @@ import { test } from '../../support/fixtures'; import { admin, instructor, studentOne } from '../../support/users'; import { Course } from 'app/entities/course.model'; import { Exam } from 'app/entities/exam/exam.model'; -import { generateUUID } from '../../support/utils'; +import { generateUUID, prepareExam, startAssessing } from '../../support/utils'; import dayjs from 'dayjs'; import { ExamChecklistItem } from '../../support/pageobjects/exam/ExamDetailsPage'; import { ExerciseType } from '../../support/constants'; @@ -12,7 +12,6 @@ import { ExamExerciseGroupsPage } from '../../support/pageobjects/exam/ExamExerc import { Page } from '@playwright/test'; import { Commands } from '../../support/commands'; import { ExamAPIRequests } from '../../support/requests/ExamAPIRequests'; -import { prepareExam, startAssessing } from './ExamAssessment.spec'; test.describe('Exam Checklists', async () => { let course: Course; diff --git a/src/test/playwright/support/pageobjects/exam/StudentExamManagementPage.ts b/src/test/playwright/support/pageobjects/exam/StudentExamManagementPage.ts index af3c655bde51..9dee95c1e573 100644 --- a/src/test/playwright/support/pageobjects/exam/StudentExamManagementPage.ts +++ b/src/test/playwright/support/pageobjects/exam/StudentExamManagementPage.ts @@ -12,8 +12,7 @@ export class StudentExamManagementPage { async clickGenerateStudentExams() { const responsePromise = this.page.waitForResponse(`${COURSE_BASE}/*/exams/*/generate-student-exams`); await this.page.click('#generateStudentExamsButton'); - const response = await responsePromise; - console.log('Generate student exams response:', await response.json()); + await responsePromise; } async clickRegisterCourseStudents() { diff --git a/src/test/playwright/support/utils.ts b/src/test/playwright/support/utils.ts index bc464f447717..893e0d4d5adf 100644 --- a/src/test/playwright/support/utils.ts +++ b/src/test/playwright/support/utils.ts @@ -1,10 +1,30 @@ import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; import { v4 as uuidv4 } from 'uuid'; -import { TIME_FORMAT } from './constants'; +import { Exercise, ExerciseType, ProgrammingExerciseAssessmentType, TIME_FORMAT } from './constants'; import * as fs from 'fs'; import { dirname } from 'path'; import { Browser, Locator, Page, expect } from '@playwright/test'; +import { Course } from 'app/entities/course.model'; +import { Exam } from 'app/entities/exam/exam.model'; +import { ExamAPIRequests } from './requests/ExamAPIRequests'; +import { ExerciseAPIRequests } from './requests/ExerciseAPIRequests'; +import { ExamExerciseGroupCreationPage } from './pageobjects/exam/ExamExerciseGroupCreationPage'; +import { CoursesPage } from './pageobjects/course/CoursesPage'; +import { CourseOverviewPage } from './pageobjects/course/CourseOverviewPage'; +import { ModelingEditor } from './pageobjects/exercises/modeling/ModelingEditor'; +import { OnlineEditorPage } from './pageobjects/exercises/programming/OnlineEditorPage'; +import { MultipleChoiceQuiz } from './pageobjects/exercises/quiz/MultipleChoiceQuiz'; +import { TextEditorPage } from './pageobjects/exercises/text/TextEditorPage'; +import { ExamNavigationBar } from './pageobjects/exam/ExamNavigationBar'; +import { ExamStartEndPage } from './pageobjects/exam/ExamStartEndPage'; +import { ExamParticipationPage } from './pageobjects/exam/ExamParticipationPage'; +import { Commands } from './commands'; +import { admin, studentOne } from './users'; +import javaPartiallySuccessful from '../fixtures/exercise/programming/java/partially_successful/submission.json'; +import { ExamManagementPage } from './pageobjects/exam/ExamManagementPage'; +import { CourseAssessmentDashboardPage } from './pageobjects/assessment/CourseAssessmentDashboardPage'; +import { ExerciseAssessmentDashboardPage } from './pageobjects/assessment/ExerciseAssessmentDashboardPage'; // Add utc plugin to use the utc timezone dayjs.extend(utc); @@ -170,3 +190,108 @@ export async function drag(page: Page, draggable: Locator, droppable: Locator) { }); await page.mouse.up(); } + +/* + * Exam utility functions + */ + +export async function prepareExam(course: Course, end: dayjs.Dayjs, exerciseType: ExerciseType, page: Page, numberOfCorrectionRounds: number = 1): Promise { + const examAPIRequests = new ExamAPIRequests(page); + const exerciseAPIRequests = new ExerciseAPIRequests(page); + const examExerciseGroupCreation = new ExamExerciseGroupCreationPage(page, examAPIRequests, exerciseAPIRequests); + const courseList = new CoursesPage(page); + const courseOverview = new CourseOverviewPage(page); + const modelingExerciseEditor = new ModelingEditor(page); + const programmingExerciseEditor = new OnlineEditorPage(page); + const quizExerciseMultipleChoice = new MultipleChoiceQuiz(page); + const textExerciseEditor = new TextEditorPage(page); + const examNavigation = new ExamNavigationBar(page); + const examStartEnd = new ExamStartEndPage(page); + const examParticipation = new ExamParticipationPage( + courseList, + courseOverview, + examNavigation, + examStartEnd, + modelingExerciseEditor, + programmingExerciseEditor, + quizExerciseMultipleChoice, + textExerciseEditor, + page, + ); + + await Commands.login(page, admin); + const resultDate = end.add(1, 'second'); + const examConfig = { + course, + startDate: dayjs(), + endDate: end, + numberOfCorrectionRoundsInExam: numberOfCorrectionRounds, + examStudentReviewStart: resultDate, + examStudentReviewEnd: resultDate.add(1, 'minute'), + publishResultsDate: resultDate, + gracePeriod: 10, + }; + const exam = await examAPIRequests.createExam(examConfig); + let additionalData = {}; + switch (exerciseType) { + case ExerciseType.PROGRAMMING: + additionalData = { + submission: javaPartiallySuccessful, + progExerciseAssessmentType: ProgrammingExerciseAssessmentType.SEMI_AUTOMATIC, + }; + break; + case ExerciseType.TEXT: + additionalData = { textFixture: 'loremIpsum-short.txt' }; + break; + case ExerciseType.QUIZ: + additionalData = { quizExerciseID: 0 }; + break; + } + + const exercise = await examExerciseGroupCreation.addGroupWithExercise(exam, exerciseType, additionalData); + await examAPIRequests.registerStudentForExam(exam, studentOne); + await examAPIRequests.generateMissingIndividualExams(exam); + await examAPIRequests.prepareExerciseStartForExam(exam); + exercise.additionalData = additionalData; + await makeExamSubmission(course, exam, exercise, page, examParticipation, examNavigation, examStartEnd); + return exam; +} + +export async function makeExamSubmission( + course: Course, + exam: Exam, + exercise: Exercise, + page: Page, + examParticipation: ExamParticipationPage, + examNavigation: ExamNavigationBar, + examStartEnd: ExamStartEndPage, +) { + await examParticipation.startParticipation(studentOne, course, exam); + await examNavigation.openOrSaveExerciseByTitle(exercise.exerciseGroup!.title!); + await examParticipation.makeSubmission(exercise.id!, exercise.type!, exercise.additionalData); + await page.waitForTimeout(2000); + await examNavigation.handInEarly(); + await examStartEnd.finishExam(); +} + +export async function startAssessing( + courseID: number, + examID: number, + timeout: number, + examManagement: ExamManagementPage, + courseAssessment: CourseAssessmentDashboardPage, + exerciseAssessment: ExerciseAssessmentDashboardPage, + toggleSecondRound: boolean = false, + isFirstTimeAssessing: boolean = true, +) { + await examManagement.openAssessmentDashboard(courseID, examID, timeout); + await courseAssessment.clickExerciseDashboardButton(); + if (toggleSecondRound) { + await exerciseAssessment.toggleSecondCorrectionRound(); + } + if (isFirstTimeAssessing) { + await exerciseAssessment.clickHaveReadInstructionsButton(); + } + await exerciseAssessment.clickStartNewAssessment(); + exerciseAssessment.getLockedMessage(); +} From decd7b13977510801c24a87f2b31514e5d229b51 Mon Sep 17 00:00:00 2001 From: Murad Talibov Date: Thu, 19 Dec 2024 18:51:45 +0100 Subject: [PATCH 07/10] Add tags to the tests missing it --- src/test/playwright/e2e/exam/ExamChecklists.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/playwright/e2e/exam/ExamChecklists.spec.ts b/src/test/playwright/e2e/exam/ExamChecklists.spec.ts index efe5f86b5330..e6c66d153094 100644 --- a/src/test/playwright/e2e/exam/ExamChecklists.spec.ts +++ b/src/test/playwright/e2e/exam/ExamChecklists.spec.ts @@ -138,7 +138,7 @@ test.describe('Exam Checklists', async () => { }); }); - test('Instructor registers a student to exam and at least one student check is marked', async ({ page, login, examDetails, studentExamManagement }) => { + test('Instructor registers a student to exam and at least one student check is marked', { tag: '@fast' }, async ({ page, login, examDetails, studentExamManagement }) => { const exam = await createExam(course, page); await login(instructor); await navigateToExamDetailsPage(page, course, exam); @@ -187,7 +187,7 @@ test.describe('Exam Checklists', async () => { }); }); - test('Instructor sets the publish results and review dates and the corresponding checks are marked', async ({ page, login, examDetails, examCreation }) => { + test('Instructor sets the publish results and review dates and the corresponding checks are marked', { tag: '@fast' }, async ({ page, login, examDetails, examCreation }) => { const exam = await createExam(course, page); await login(instructor); await navigateToExamDetailsPage(page, course, exam); From 11e21e5b780e50b68db8e0badf287bcaa51873f6 Mon Sep 17 00:00:00 2001 From: Murad Talibov Date: Fri, 20 Dec 2024 01:59:15 +0100 Subject: [PATCH 08/10] Solve potential race condition on exam creation/deletion tests --- src/test/playwright/e2e/exam/ExamCreationDeletion.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/playwright/e2e/exam/ExamCreationDeletion.spec.ts b/src/test/playwright/e2e/exam/ExamCreationDeletion.spec.ts index a17426823b20..00761169653b 100644 --- a/src/test/playwright/e2e/exam/ExamCreationDeletion.spec.ts +++ b/src/test/playwright/e2e/exam/ExamCreationDeletion.spec.ts @@ -4,7 +4,6 @@ import { Course } from 'app/entities/course.model'; import { dayjsToString, generateUUID, trimDate } from '../../support/utils'; import dayjs from 'dayjs'; import { expect } from '@playwright/test'; -import { Exam } from 'app/entities/exam/exam.model'; /* * Common primitives @@ -39,7 +38,6 @@ const dateFormat = 'MMM D, YYYY HH:mm'; test.describe('Exam creation/deletion', { tag: '@fast' }, () => { let course: Course; - let examId: number; test.beforeEach(async ({ login, courseManagementAPIRequests }) => { await login(admin); @@ -64,8 +62,6 @@ test.describe('Exam creation/deletion', { tag: '@fast' }, () => { await examCreation.setConfirmationEndText(examData.confirmationEndText); const response = await examCreation.submit(); - const exam: Exam = await response.json(); - examId = exam.id!; expect(response.status()).toBe(201); await expect(examManagement.getExamTitle()).toContainText(examData.title); @@ -82,6 +78,8 @@ test.describe('Exam creation/deletion', { tag: '@fast' }, () => { }); test.describe('Exam deletion', () => { + let examId: number; + test.beforeEach(async ({ examAPIRequests }) => { examData.title = 'exam' + generateUUID(); const examConfig = { @@ -102,6 +100,8 @@ test.describe('Exam creation/deletion', { tag: '@fast' }, () => { }); test.describe('Edits an exam', () => { + let examId: number; + test.beforeEach(async ({ examAPIRequests }) => { examData.title = 'exam' + generateUUID(); const examConfig = { From e96a6d83a007fafe1352b732aa5a00b9767233cc Mon Sep 17 00:00:00 2001 From: Murad Talibov Date: Fri, 20 Dec 2024 11:10:35 +0100 Subject: [PATCH 09/10] Improve exam creation/deletion tests for better isolation and precise exam date differences --- .../e2e/exam/ExamCreationDeletion.spec.ts | 109 +++++++++--------- 1 file changed, 54 insertions(+), 55 deletions(-) diff --git a/src/test/playwright/e2e/exam/ExamCreationDeletion.spec.ts b/src/test/playwright/e2e/exam/ExamCreationDeletion.spec.ts index 00761169653b..fb0e69ef5c92 100644 --- a/src/test/playwright/e2e/exam/ExamCreationDeletion.spec.ts +++ b/src/test/playwright/e2e/exam/ExamCreationDeletion.spec.ts @@ -4,36 +4,11 @@ import { Course } from 'app/entities/course.model'; import { dayjsToString, generateUUID, trimDate } from '../../support/utils'; import dayjs from 'dayjs'; import { expect } from '@playwright/test'; +import { Exam } from 'app/entities/exam/exam.model'; /* * Common primitives */ -const examData = { - title: 'exam' + generateUUID(), - visibleDate: dayjs(), - startDate: dayjs().add(1, 'hour'), - endDate: dayjs().add(2, 'hour'), - numberOfExercisesInExam: 4, - examMaxPoints: 40, - startText: 'Exam start text', - endText: 'Exam end text', - confirmationStartText: 'Exam confirmation start text', - confirmationEndText: 'Exam confirmation end text', -}; - -const editedExamData = { - title: 'exam' + generateUUID(), - visibleDate: dayjs(), - startDate: dayjs().add(1, 'hour'), - endDate: dayjs().add(5, 'hour'), - numberOfExercisesInExam: 3, - examMaxPoints: 30, - startText: 'Edited exam start text', - endText: 'Edited exam end text', - confirmationStartText: 'Edited exam confirmation start text', - confirmationEndText: 'Edited exam confirmation end text', -}; - const dateFormat = 'MMM D, YYYY HH:mm'; test.describe('Exam creation/deletion', { tag: '@fast' }, () => { @@ -45,6 +20,19 @@ test.describe('Exam creation/deletion', { tag: '@fast' }, () => { }); test('Creates an exam', async ({ navigationBar, courseManagement, examManagement, examCreation }) => { + const examData = { + title: 'exam' + generateUUID(), + visibleDate: dayjs(), + startDate: dayjs().add(1, 'hour'), + endDate: dayjs().add(2, 'hour'), + numberOfExercisesInExam: 4, + examMaxPoints: 40, + startText: 'Exam start text', + endText: 'Exam end text', + confirmationStartText: 'Exam confirmation start text', + confirmationEndText: 'Exam confirmation end text', + }; + await navigationBar.openCourseManagement(); await courseManagement.openExamsOfCourse(course.id!); @@ -78,48 +66,60 @@ test.describe('Exam creation/deletion', { tag: '@fast' }, () => { }); test.describe('Exam deletion', () => { - let examId: number; + let exam: Exam; test.beforeEach(async ({ examAPIRequests }) => { - examData.title = 'exam' + generateUUID(); const examConfig = { course, - title: examData.title, + title: 'exam' + generateUUID(), }; - const examResponse = await examAPIRequests.createExam(examConfig); - examId = examResponse.id!; + exam = await examAPIRequests.createExam(examConfig); }); test('Deletes an existing exam', async ({ navigationBar, courseManagement, examManagement, examDetails }) => { await navigationBar.openCourseManagement(); await courseManagement.openExamsOfCourse(course.id!); - await examManagement.openExam(examId); - await examDetails.deleteExam(examData.title); - await expect(examManagement.getExamSelector(examData.title)).not.toBeVisible(); + await examManagement.openExam(exam.id!); + await examDetails.deleteExam(exam.title!); + await expect(examManagement.getExamSelector(exam.title!)).not.toBeVisible(); }); }); test.describe('Edits an exam', () => { - let examId: number; + let exam: Exam; test.beforeEach(async ({ examAPIRequests }) => { - examData.title = 'exam' + generateUUID(); const examConfig = { course, - title: examData.title, + title: 'exam' + generateUUID(), visibleDate: dayjs(), - startDate: dayjs().add(1, 'hour'), }; - const examResponse = await examAPIRequests.createExam(examConfig); - examId = examResponse.id!; + exam = await examAPIRequests.createExam(examConfig); }); test('Edits an existing exam', async ({ navigationBar, courseManagement, examManagement, examCreation }) => { + const visibleDate = dayjs(); + const startDate = visibleDate.add(1, 'hour'); + const endDate = startDate.add(4, 'hour'); + + const editedExamData = { + title: 'exam' + generateUUID(), + visibleDate: visibleDate, + startDate: startDate, + endDate: endDate, + numberOfExercisesInExam: 3, + examMaxPoints: 30, + startText: 'Edited exam start text', + endText: 'Edited exam end text', + confirmationStartText: 'Edited exam confirmation start text', + confirmationEndText: 'Edited exam confirmation end text', + }; + await navigationBar.openCourseManagement(); await courseManagement.openExamsOfCourse(course.id!); - await examManagement.openExam(examId); + await examManagement.openExam(exam.id!); - await expect(examManagement.getExamTitle()).toContainText(examData.title); + await expect(examManagement.getExamTitle()).toContainText(exam.title!); await examManagement.clickEdit(); await examCreation.setTitle(editedExamData.title); @@ -136,19 +136,18 @@ test.describe('Exam creation/deletion', { tag: '@fast' }, () => { const response = await examCreation.update(); expect(response.status()).toBe(200); - const exam = await response.json(); - - examId = exam.id; - expect(exam.testExam).toBeFalsy(); - expect(trimDate(exam.visibleDate)).toBe(trimDate(dayjsToString(editedExamData.visibleDate))); - expect(trimDate(exam.startDate)).toBe(trimDate(dayjsToString(editedExamData.startDate))); - expect(trimDate(exam.endDate)).toBe(trimDate(dayjsToString(editedExamData.endDate))); - expect(exam.numberOfExercisesInExam).toBe(editedExamData.numberOfExercisesInExam); - expect(exam.examMaxPoints).toBe(editedExamData.examMaxPoints); - expect(exam.startText).toBe(editedExamData.startText); - expect(exam.endText).toBe(editedExamData.endText); - expect(exam.confirmationStartText).toBe(editedExamData.confirmationStartText); - expect(exam.confirmationEndText).toBe(editedExamData.confirmationEndText); + const editedExam = await response.json(); + + expect(editedExam.testExam).toBeFalsy(); + expect(trimDate(editedExam.visibleDate)).toBe(trimDate(dayjsToString(editedExamData.visibleDate))); + expect(trimDate(editedExam.startDate)).toBe(trimDate(dayjsToString(editedExamData.startDate))); + expect(trimDate(editedExam.endDate)).toBe(trimDate(dayjsToString(editedExamData.endDate))); + expect(editedExam.numberOfExercisesInExam).toBe(editedExamData.numberOfExercisesInExam); + expect(editedExam.examMaxPoints).toBe(editedExamData.examMaxPoints); + expect(editedExam.startText).toBe(editedExamData.startText); + expect(editedExam.endText).toBe(editedExamData.endText); + expect(editedExam.confirmationStartText).toBe(editedExamData.confirmationStartText); + expect(editedExam.confirmationEndText).toBe(editedExamData.confirmationEndText); await expect(examManagement.getExamTitle()).toContainText(editedExamData.title); await expect(examManagement.getExamVisibleDate()).toContainText(dayjs(editedExamData.visibleDate).format(dateFormat)); From 6ac351adfac0ea9beea0729a17f19233771bfd34 Mon Sep 17 00:00:00 2001 From: Murad Talibov Date: Mon, 23 Dec 2024 17:04:39 +0100 Subject: [PATCH 10/10] Add comments about why the tests are skipped --- src/test/playwright/e2e/exam/ExamChecklists.spec.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/test/playwright/e2e/exam/ExamChecklists.spec.ts b/src/test/playwright/e2e/exam/ExamChecklists.spec.ts index e6c66d153094..8b1d943f863f 100644 --- a/src/test/playwright/e2e/exam/ExamChecklists.spec.ts +++ b/src/test/playwright/e2e/exam/ExamChecklists.spec.ts @@ -229,6 +229,8 @@ test.describe('Exam Checklists', async () => { }, ); + // This test is skipped for now because it currently fails due to a known issue: + // https://github.com/ls1intum/Artemis/issues/10074 test.skip( 'Student makes a quiz submission and unassessed quizzes check is marked for instructor after assessment', { tag: '@slow' }, @@ -243,6 +245,8 @@ test.describe('Exam Checklists', async () => { }, ); + // This test is skipped for now because it currently fails due to a known issue: + // https://github.com/ls1intum/Artemis/issues/10076 test.skip( 'Student does not submit the exam on time and corresponding check is marked', { tag: '@slow' },