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..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 @@ -50,7 +50,10 @@

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

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

  • - +
  • @@ -150,7 +154,7 @@

    + @@ -167,14 +171,14 @@

  • - +
  • - + @@ -191,14 +195,17 @@

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

  • - +
  • @if (exam.publishResultsDate) { @@ -487,7 +494,10 @@

    - +

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

    @if (isAssessingUnsubmittedExams) { } @else { - + } @@ -580,13 +596,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 { - + }   diff --git a/src/test/playwright/e2e/exam/ExamAssessment.spec.ts b/src/test/playwright/e2e/exam/ExamAssessment.spec.ts index 4881c7337718..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(); -} - -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 new file mode 100644 index 000000000000..8b1d943f863f --- /dev/null +++ b/src/test/playwright/e2e/exam/ExamChecklists.spec.ts @@ -0,0 +1,308 @@ +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, prepareExam, startAssessing } 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'; +import { Commands } from '../../support/commands'; +import { ExamAPIRequests } from '../../support/requests/ExamAPIRequests'; + +test.describe('Exam Checklists', async () => { + let course: Course; + + test.beforeEach('Create course', async ({ login, courseManagementAPIRequests }) => { + await login(admin); + course = await courseManagementAPIRequests.createCourse({ customizeGroups: true }); + 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, + }) => { + const exam = await createExam(course, page); + 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, + }) => { + 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 < exam.numberOfExercisesInExam!; 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, + }) => { + const exam = await createExam(course, page); + 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, + }) => { + const exam = await createExam(course, page); + 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, + }) => { + const exam = await createExam(course, page); + 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', { tag: '@fast' }, 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); + 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' }, () => { + 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 < exam.numberOfExercisesInExam!; 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', { tag: '@fast' }, 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}`); + 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); + }); + + 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); + }, + ); + + // 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' }, + 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); + }, + ); + + // 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' }, + 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, 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, + }; + + 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}`); +} + +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/e2e/exam/ExamCreationDeletion.spec.ts b/src/test/playwright/e2e/exam/ExamCreationDeletion.spec.ts index a17426823b20..fb0e69ef5c92 100644 --- a/src/test/playwright/e2e/exam/ExamCreationDeletion.spec.ts +++ b/src/test/playwright/e2e/exam/ExamCreationDeletion.spec.ts @@ -9,37 +9,10 @@ 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' }, () => { let course: Course; - let examId: number; test.beforeEach(async ({ login, courseManagementAPIRequests }) => { await login(admin); @@ -47,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!); @@ -64,8 +50,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,44 +66,60 @@ test.describe('Exam creation/deletion', { tag: '@fast' }, () => { }); test.describe('Exam deletion', () => { + 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 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)); 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 00ea2c5bdef8..9d25ab252f19 100644 --- a/src/test/playwright/support/fixtures.ts +++ b/src/test/playwright/support/fixtures.ts @@ -68,7 +68,6 @@ import { QuizExerciseParticipationPage } from './pageobjects/exercises/quiz/Quiz import { ModalDialogBox } from './pageobjects/exam/ModalDialogBox'; import { ExamParticipationActions } from './pageobjects/exam/ExamParticipationActions'; import { AccountManagementAPIRequests } from './requests/AccountManagementAPIRequests'; -import { EditExamPage } from './pageobjects/exam/EditExamPage'; /* * Define custom types for fixtures @@ -98,7 +97,6 @@ export type ArtemisPageObjects = { courseCommunication: CourseCommunicationPage; lectureManagement: LectureManagementPage; lectureCreation: LectureCreationPage; - editExam: EditExamPage; examCreation: ExamCreationPage; examDetails: ExamDetailsPage; examExerciseGroupCreation: ExamExerciseGroupCreationPage; @@ -223,9 +221,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/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 29bc3b8aa8a0..33e4ef66329e 100644 --- a/src/test/playwright/support/pageobjects/exam/ExamDetailsPage.ts +++ b/src/test/playwright/support/pageobjects/exam/ExamDetailsPage.ts @@ -10,6 +10,56 @@ 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); + } + + 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(); + } + + 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 @@ -23,3 +73,20 @@ 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', + UNFINISHED_ASSESSMENTS = 'check-unfinished-assessments', + UNASSESSED_QUIZZES = 'check-unassessed-quizzes', + UNSUBMITTED_EXERCISES = 'check-unsubmitted-exercises', +} diff --git a/src/test/playwright/support/pageobjects/exam/ExamExerciseGroupCreationPage.ts b/src/test/playwright/support/pageobjects/exam/ExamExerciseGroupCreationPage.ts index 435202bf7971..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 = {}): Promise { - const response = await this.handleAddGroupWithExercise(exam, 'Exercise ' + generateUUID(), exerciseType, additionalData); + 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,11 +79,18 @@ export class ExamExerciseGroupCreationPage { return exercise; } - async handleAddGroupWithExercise(exam: Exam, title: string, exerciseType: ExerciseType, additionalData: AdditionalData): 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); + 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/pageobjects/exam/StudentExamManagementPage.ts b/src/test/playwright/support/pageobjects/exam/StudentExamManagementPage.ts index 593fc5451872..9dee95c1e573 100644 --- a/src/test/playwright/support/pageobjects/exam/StudentExamManagementPage.ts +++ b/src/test/playwright/support/pageobjects/exam/StudentExamManagementPage.ts @@ -21,6 +21,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..899e1f396e93 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 @@ -208,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 }); + } + } } 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`; 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(); +}