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 @@
- 0" />
+ 0"
+ data-testid="check-least-one-exercise-group"
+ />
@@ -63,6 +66,7 @@
-
+
-
+
-
+
@@ -139,7 +143,7 @@
- 0" />
+ 0" data-testid="check-least-one-student" />
@@ -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 @@
-
+
|
|
@@ -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();
+}
| |