diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java index 5f68ced7f447..7d9314a59305 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentExamRepository.java @@ -474,4 +474,18 @@ private Exercise selectRandomExercise(SecureRandom random, ExerciseGroup exercis WHERE se.id IN :ids """) List findAllWithEagerExercisesById(@Param("ids") List ids); + + /** + * Gets the longest working time of the exam with the given id + * + * @param examId the id of the exam + * @return number longest working time of the exam + */ + @Query(""" + SELECT MAX(se.workingTime) + FROM StudentExam se + JOIN se.exam e + WHERE e.id = :examId + """) + Integer findLongestWorkingTimeForExam(@Param("examId") Long examId); } diff --git a/src/main/java/de/tum/in/www1/artemis/repository/SubmissionRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/SubmissionRepository.java index bdea6bf81253..293f8d5d0cdc 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/SubmissionRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/SubmissionRepository.java @@ -555,4 +555,41 @@ SELECT MAX(s2.submissionDate) default Submission findByIdElseThrow(long submissionId) { return findById(submissionId).orElseThrow(() -> new EntityNotFoundException("Submission", submissionId)); } + + /** + * GChecks if unassessed Quiz Submissions exist for the given exam + * + * @param examId the ID of the exam + * @return boolean indicating if there are unassessed Quiz Submission + */ + @Query(""" + SELECT COUNT(p.exercise) > 0 + FROM StudentParticipation p + JOIN p.submissions s + LEFT JOIN s.results r + WHERE p.exercise.exerciseGroup.exam.id = :examId + AND p.testRun IS FALSE + AND TYPE(s) = QuizSubmission + AND s.submitted IS TRUE + AND r.id IS NULL + """) + boolean existsUnassessedQuizzesByExamId(@Param("examId") long examId); + + /** + * Checks if unsubmitted text and modeling submissions exist for the exam with the given id + * + * @param examId the ID of the exam + * @return boolean indicating if there are unsubmitted text and modelling submissions + */ + @Query(""" + SELECT COUNT(p.exercise) > 0 + FROM StudentParticipation p + JOIN p.submissions s + WHERE p.exercise.exerciseGroup.exam.id = :examId + AND p.testRun IS FALSE + AND TYPE(s) IN (TextSubmission, ModelingSubmission) + AND (s.submitted IS NULL OR s.submitted IS FALSE) + AND s.submissionDate IS NULL + """) + boolean existsUnsubmittedExercisesByExamId(@Param("examId") long examId); } diff --git a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamService.java b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamService.java index 6a23bc2cc2f8..05fb53af1af7 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/exam/ExamService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/exam/ExamService.java @@ -1123,6 +1123,10 @@ public ExamChecklistDTO getStatsForChecklist(Exam exam, boolean isInstructor) { examChecklistDTO.setNumberOfExamsSubmitted(numberOfStudentExamsSubmitted); } examChecklistDTO.setNumberOfTotalParticipationsForAssessment(totalNumberOfParticipationsForAssessment); + boolean existsUnassessedQuizzes = submissionRepository.existsUnassessedQuizzesByExamId(exam.getId()); + examChecklistDTO.setExistsUnassessedQuizzes(existsUnassessedQuizzes); + boolean existsUnsubmittedExercises = submissionRepository.existsUnsubmittedExercisesByExamId(exam.getId()); + examChecklistDTO.setExistsUnsubmittedExercises(existsUnsubmittedExercises); return examChecklistDTO; } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/StudentExamResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/StudentExamResource.java index 12e80f4e894f..d5ce3d16cf59 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/StudentExamResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/StudentExamResource.java @@ -890,4 +890,21 @@ public ResponseEntity unsubmitStudentExam(@PathVariable Long course return ResponseEntity.ok(studentExamRepository.save(studentExam)); } + + /** + * GET courses/{courseId}/exams/{examId}/longest-working-time : Returns the value of + * the longest working time of the exam + * + * @param courseId the course to which the student exams belong to + * @param examId the exam to which the student exams belong to + * @return the longest working time of the exam (in seconds) + */ + @EnforceAtLeastInstructor + @GetMapping("courses/{courseId}/exams/{examId}/longest-working-time") + public ResponseEntity getLongestWorkingTimeForExam(@PathVariable Long courseId, @PathVariable Long examId) { + + examAccessService.checkCourseAndExamAccessForInstructorElseThrow(courseId, examId); + Integer longestWorkingTime = studentExamRepository.findLongestWorkingTimeForExam(examId); + return ResponseEntity.ok().body(longestWorkingTime); + } } diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/ExamChecklistDTO.java b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/ExamChecklistDTO.java index 9f4d54e142f3..ab13c8f8c0d1 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/dto/ExamChecklistDTO.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/dto/ExamChecklistDTO.java @@ -26,6 +26,26 @@ public class ExamChecklistDTO { private boolean allExamExercisesAllStudentsPrepared; + private boolean existsUnassessedQuizzes; + + private boolean existsUnsubmittedExercises; + + public boolean getExistsUnassessedQuizzes() { + return existsUnassessedQuizzes; + } + + public void setExistsUnassessedQuizzes(boolean existsUnassessedQuizzes) { + this.existsUnassessedQuizzes = existsUnassessedQuizzes; + } + + public boolean getExistsUnsubmittedExercises() { + return existsUnsubmittedExercises; + } + + public void setExistsUnsubmittedExercises(boolean existsUnsubmittedExercises) { + this.existsUnsubmittedExercises = existsUnsubmittedExercises; + } + public Long getNumberOfTotalParticipationsForAssessment() { return numberOfTotalParticipationsForAssessment; } diff --git a/src/main/webapp/app/entities/exam-checklist.model.ts b/src/main/webapp/app/entities/exam-checklist.model.ts index 569ae25f58dd..ec4336543736 100644 --- a/src/main/webapp/app/entities/exam-checklist.model.ts +++ b/src/main/webapp/app/entities/exam-checklist.model.ts @@ -16,4 +16,8 @@ export class ExamChecklist { public numberOfAllComplaintsDone?: number; public allExamExercisesAllStudentsPrepared?: boolean; + + public existsUnassessedQuizzes: boolean; + + public existsUnsubmittedExercises: boolean; } 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 ddf9e76f4d9a..51902726da60 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 @@ -462,8 +462,105 @@

{{ 'artemisApp.examStatus.correction.examCorrection' | artemisTranslate }}
  • - - {{ 'artemisApp.examManagement.checklist.textitems.pulishingdateset' | artemisTranslate }} +
  • + + {{ 'artemisApp.examManagement.checklist.textitems.pulishingdateset' | artemisTranslate }} +
  • + @if (exam.publishResultsDate) { +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + +
    + + + + + +
    + + + @if (isEvaluatingQuizExercises) { + + } @else { + + } + + +
    + + + @if (isAssessingUnsubmittedExams) { + + } @else { + + } + + +
    +
    + } diff --git a/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-checklist.component.ts b/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-checklist.component.ts index 956bcf2b039b..81cc9fb5608d 100644 --- a/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-checklist.component.ts +++ b/src/main/webapp/app/exam/manage/exams/exam-checklist-component/exam-checklist.component.ts @@ -4,6 +4,13 @@ import { ExamChecklist } from 'app/entities/exam-checklist.model'; import { faChartBar, faEye, faListAlt, faThList, faUser, faWrench } from '@fortawesome/free-solid-svg-icons'; import { ExamChecklistService } from 'app/exam/manage/exams/exam-checklist-component/exam-checklist.service'; import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; +import { ExamManagementService } from 'app/exam/manage/exam-management.service'; +import { AlertService } from 'app/core/util/alert.service'; +import { HttpErrorResponse } from '@angular/common/http'; +import dayjs from 'dayjs/esm'; +import { StudentExamService } from 'app/exam/manage/student-exams/student-exam.service'; +import { Subject, Subscription } from 'rxjs'; +import { captureException } from '@sentry/angular-ivy'; @Component({ selector: 'jhi-exam-checklist', @@ -12,6 +19,7 @@ import { JhiWebsocketService } from 'app/core/websocket/websocket.service'; export class ExamChecklistComponent implements OnChanges, OnInit, OnDestroy { @Input() exam: Exam; @Input() getExamRoutesByIdentifier: any; + private longestWorkingTimeSub: Subscription | null = null; examChecklist: ExamChecklist; isLoading = false; @@ -22,6 +30,13 @@ export class ExamChecklistComponent implements OnChanges, OnInit, OnDestroy { hasOptionalExercises = false; countMandatoryExercises = 0; isTestExam: boolean; + isEvaluatingQuizExercises: boolean; + isAssessingUnsubmittedExams: boolean; + existsUnfinishedAssessments = false; + existsUnassessedQuizzes = false; + existsUnsubmittedExercises = false; + isExamOver = false; + longestWorkingTime?: number; numberOfSubmitted = 0; numberOfStarted = 0; @@ -36,9 +51,15 @@ export class ExamChecklistComponent implements OnChanges, OnInit, OnDestroy { faThList = faThList; faChartBar = faChartBar; + private dialogErrorSource = new Subject(); + dialogError$ = this.dialogErrorSource.asObservable(); + constructor( private examChecklistService: ExamChecklistService, private websocketService: JhiWebsocketService, + private examManagementService: ExamManagementService, + private alertService: AlertService, + private studentExamService: StudentExamService, ) {} ngOnInit() { @@ -48,6 +69,12 @@ export class ExamChecklistComponent implements OnChanges, OnInit, OnDestroy { const startedTopic = this.examChecklistService.getStartedTopic(this.exam); this.websocketService.subscribe(startedTopic); this.websocketService.receive(startedTopic).subscribe(() => (this.numberOfStarted += 1)); + if (this.exam?.course?.id && this.exam?.id) { + this.longestWorkingTimeSub = this.studentExamService.getLongestWorkingTimeForExam(this.exam.course.id, this.exam.id).subscribe((res) => { + this.longestWorkingTime = res; + this.calculateIsExamOver(); + }); + } } ngOnChanges() { @@ -63,6 +90,14 @@ export class ExamChecklistComponent implements OnChanges, OnInit, OnDestroy { !!this.exam.numberOfExamUsers && this.exam.numberOfExamUsers > 0 && this.examChecklistService.checkAllExamsGenerated(this.exam, this.examChecklist); this.numberOfStarted = this.examChecklist.numberOfExamsStarted; this.numberOfSubmitted = this.examChecklist.numberOfExamsSubmitted; + if (this.isExamOver) { + if (this.examChecklist.numberOfTotalExamAssessmentsFinishedByCorrectionRound !== undefined) { + const lastAssessmentFinished = this.examChecklist.numberOfTotalExamAssessmentsFinishedByCorrectionRound.last(); + this.existsUnfinishedAssessments = lastAssessmentFinished !== this.examChecklist.numberOfTotalParticipationsForAssessment; + } + } + this.existsUnassessedQuizzes = this.examChecklist.existsUnassessedQuizzes; + this.existsUnsubmittedExercises = this.examChecklist.existsUnsubmittedExercises; }); } @@ -71,5 +106,65 @@ export class ExamChecklistComponent implements OnChanges, OnInit, OnDestroy { this.websocketService.unsubscribe(submittedTopic); const startedTopic = this.examChecklistService.getStartedTopic(this.exam); this.websocketService.unsubscribe(startedTopic); + if (this.longestWorkingTimeSub) { + this.longestWorkingTimeSub.unsubscribe(); + } + } + + /** + * Evaluates all the quiz exercises that belong to the exam + */ + evaluateQuizExercises() { + this.isEvaluatingQuizExercises = true; + if (this.exam.course?.id && this.exam.id) { + this.examManagementService.evaluateQuizExercises(this.exam.course.id, this.exam.id).subscribe({ + next: (res) => { + this.alertService.success('artemisApp.studentExams.evaluateQuizExerciseSuccess', { number: res?.body }); + this.existsUnassessedQuizzes = false; + this.isEvaluatingQuizExercises = false; + }, + error: (error: HttpErrorResponse) => { + this.dialogErrorSource.next(error.message); + this.alertService.error('artemisApp.studentExams.evaluateQuizExerciseFailure'); + this.isEvaluatingQuizExercises = false; + }, + }); + } else { + captureException(new Error(`Quiz exercises could not be evaluated due to missing course ID or exam ID`)); + } + } + + /** + * Evaluates all the unsubmitted Text and Modelling submissions to 0 + */ + assessUnsubmittedExamModelingAndTextParticipations() { + this.isAssessingUnsubmittedExams = true; + if (this.exam.course?.id && this.exam.id) { + this.examManagementService.assessUnsubmittedExamModelingAndTextParticipations(this.exam.course.id, this.exam.id).subscribe({ + next: (res) => { + this.alertService.success('artemisApp.studentExams.assessUnsubmittedStudentExamsSuccess', { number: res?.body }); + this.existsUnsubmittedExercises = false; + this.isAssessingUnsubmittedExams = false; + }, + error: (error: HttpErrorResponse) => { + this.dialogErrorSource.next(error.message); + this.alertService.error('artemisApp.studentExams.assessUnsubmittedStudentExamsFailure'); + this.isAssessingUnsubmittedExams = false; + }, + }); + } else { + captureException(new Error(`Unsubmitted exercises could not be evaluated due to missing course ID or exam ID`)); + } + } + + calculateIsExamOver() { + if (this.longestWorkingTime && this.exam) { + const startDate = dayjs(this.exam.startDate); + let endDate = startDate.add(this.longestWorkingTime, 'seconds'); + if (this.exam.gracePeriod) { + endDate = endDate.add(this.exam.gracePeriod!, 'seconds'); + } + this.isExamOver = endDate.isBefore(dayjs()); + } } } diff --git a/src/main/webapp/app/exam/manage/student-exams/student-exam.service.ts b/src/main/webapp/app/exam/manage/student-exams/student-exam.service.ts index c2828dcad6f9..8d2196806de9 100644 --- a/src/main/webapp/app/exam/manage/student-exams/student-exam.service.ts +++ b/src/main/webapp/app/exam/manage/student-exams/student-exam.service.ts @@ -78,4 +78,13 @@ export class StudentExamService { }); return studentExamsResponse; } + + /** + * Get longest working time for the exam. + * @param courseId The course id. + * @param examId The exam id. + */ + getLongestWorkingTimeForExam(courseId: number, examId: number): Observable { + return this.http.get(`${this.resourceUrl}/${courseId}/exams/${examId}/longest-working-time`); + } } diff --git a/src/main/webapp/i18n/de/exam.json b/src/main/webapp/i18n/de/exam.json index 119a14cd35c7..1d427c3d1da1 100644 --- a/src/main/webapp/i18n/de/exam.json +++ b/src/main/webapp/i18n/de/exam.json @@ -384,6 +384,7 @@ "evaluateQuizExerciseFailure": "Es gab einen Fehler bei der Auswertung der Quiz-Aufgaben:\n {{message}}", "assessUnsubmittedStudentExamsSuccess": "Alle Klausuren der Studierenden überprüft und die nicht eingereichten oder leeren Modellierungs- und Textaufgaben mit 0 Punkten bewertet.", "assessUnsubmittedStudentExamsFailure": "Die Klausur ist noch nicht für alle Studierenden beendet.", + "assessUnsubmittedStudentExamsIdFailure": "Nicht eingereichte Aufgaben konnten aufgrund einer fehlenden Kurs-ID oder Prüfung-ID nicht bewertet werden.", "studentExamGenerationModalText": "Es existieren bereits individuelle Klausuren. Beim Fortfahren werden diese Klausuren gelöscht und neue individuelle Klausuren generiert.\n\nBestehende Teilnahmen und Abgaben werden ebenfalls gelöscht!", "studentExamStatusSuccess": "Alle registrierten Studierenden haben eine Klausur", "studentExamStatusWarning": "Nicht alle registrierten Studierenden haben eine Klausur", @@ -674,7 +675,13 @@ "exerciseTableSingular": "Aufgabe", "points": "Punkte", "numberParticipants": "Teilnehmende", - "variants": "Varianten" + "variants": "Varianten", + "assessmentCheckType": "Check Typ", + "assessmentStatus": "Status", + "assessmentAction": "Aktion", + "checkUnfinishedAssessments": "Unvollständige Bewertungen", + "checkUnassessedQuizzes": "Unbewertete Quizaufgaben", + "checkUnsubmittedExercises": "Nicht eingereichte Aufgaben" } }, "editWorkingTime": { diff --git a/src/main/webapp/i18n/en/exam.json b/src/main/webapp/i18n/en/exam.json index 2e858a400ced..8106b8dd6ec1 100644 --- a/src/main/webapp/i18n/en/exam.json +++ b/src/main/webapp/i18n/en/exam.json @@ -391,6 +391,7 @@ "evaluateQuizExerciseFailure": "There was an error during the evaluation of the quiz exercises:\n {{message}}", "assessUnsubmittedStudentExamsSuccess": "Reviewed all student exams and assessed the unsubmitted or empty modeling and text exercises with 0 points.", "assessUnsubmittedStudentExamsFailure": "The exam is not over yet for all students", + "assessUnsubmittedStudentExamsIdFailure": "Unsubmitted exams could not be evaluated due to a missing course ID or exam ID", "unlockAllRepositoriesSuccess": "Repositories of {{number}} programming exercises were unlocked.", "unlockAllRepositoriesFailure": "There was an error during the unlocking of the programming exercises:\n {{message}}", "lockAllRepositoriesSuccess": "Repositories of {{number}} programming exercises were locked.", @@ -675,7 +676,13 @@ "exerciseTableSingular": "Exercise", "points": "Points", "numberParticipants": "Participants", - "variants": "Variants" + "variants": "Variants", + "assessmentCheckType": "Check Type", + "assessmentStatus": "Status", + "assessmentAction": "Action", + "checkUnfinishedAssessments": "Unfinished Assessments", + "checkUnassessedQuizzes": "Unassessed Quiz Exercises", + "checkUnsubmittedExercises": "Unsubmitted Exercises" } }, "editWorkingTime": { diff --git a/src/test/java/de/tum/in/www1/artemis/exam/ExamIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exam/ExamIntegrationTest.java index 2d1df6ea19fa..50036359019e 100644 --- a/src/test/java/de/tum/in/www1/artemis/exam/ExamIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exam/ExamIntegrationTest.java @@ -1156,6 +1156,8 @@ void testGetExamStatistics() throws Exception { assertThat(returnedStatistics.getNumberOfTotalExamAssessmentsFinishedByCorrectionRound()) .isEqualTo(actualStatistics.getNumberOfTotalExamAssessmentsFinishedByCorrectionRound()); assertThat(returnedStatistics.getNumberOfTotalParticipationsForAssessment()).isEqualTo(actualStatistics.getNumberOfTotalParticipationsForAssessment()); + assertThat(returnedStatistics.getExistsUnassessedQuizzes()).isEqualTo(actualStatistics.getExistsUnassessedQuizzes()); + assertThat(returnedStatistics.getExistsUnsubmittedExercises()).isEqualTo(actualStatistics.getExistsUnsubmittedExercises()); } @Test diff --git a/src/test/java/de/tum/in/www1/artemis/exam/StudentExamIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/exam/StudentExamIntegrationTest.java index 3dc8017fbe93..5867abd3a8d2 100644 --- a/src/test/java/de/tum/in/www1/artemis/exam/StudentExamIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/exam/StudentExamIntegrationTest.java @@ -2810,6 +2810,22 @@ void testConductionOfTestExam_successful() throws Exception { } } + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testGetLongestWorkingTimeForExam() throws Exception { + // Step 1: Create mock student exams + List studentExams = prepareStudentExamsForConduction(false, true, NUMBER_OF_STUDENTS); + + // Step 2: Get the maximum working time among the exams to find the longest time + final int longestWorkingTime = studentExams.stream().mapToInt(StudentExam::getWorkingTime).max().orElse(0); + + // When + final int response = request.get("/api/courses/" + course2.getId() + "/exams/" + exam2.getId() + "/longest-working-time", HttpStatus.OK, Integer.class); + + // Then + assertThat(response).isEqualTo(longestWorkingTime); + } + @Nested class ChangedAndUnchangedSubmissionsIntegrationTest { diff --git a/src/test/java/de/tum/in/www1/artemis/service/exam/ExamServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/exam/ExamServiceTest.java index 89438e376fc7..88d56575e447 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/exam/ExamServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/exam/ExamServiceTest.java @@ -215,6 +215,8 @@ void getChecklistStatsEmpty() { assertThat(examChecklistDTO.getNumberOfAllComplaints()).isZero(); assertThat(examChecklistDTO.getNumberOfAllComplaintsDone()).isZero(); assertThat(examChecklistDTO.getAllExamExercisesAllStudentsPrepared()).isFalse(); + assertThat(examChecklistDTO.getExistsUnassessedQuizzes()).isFalse(); + assertThat(examChecklistDTO.getExistsUnsubmittedExercises()).isFalse(); } @Nested diff --git a/src/test/javascript/spec/component/exam/manage/exams/exam-checklist.component.spec.ts b/src/test/javascript/spec/component/exam/manage/exams/exam-checklist.component.spec.ts index c49036295352..317ab262a4a9 100644 --- a/src/test/javascript/spec/component/exam/manage/exams/exam-checklist.component.spec.ts +++ b/src/test/javascript/spec/component/exam/manage/exams/exam-checklist.component.spec.ts @@ -130,4 +130,24 @@ describe('ExamChecklistComponent', () => { expect(getExamStatisticsStub).toHaveBeenCalledWith(exam); expect(component.examChecklist).toEqual(examChecklist); }); + + it('should set existsUnassessedQuizzes correctly', () => { + const getExamStatisticsStub = jest.spyOn(examChecklistService, 'getExamStatistics').mockReturnValue(of(examChecklist)); + + component.ngOnChanges(); + + expect(getExamStatisticsStub).toHaveBeenCalledOnce(); + expect(getExamStatisticsStub).toHaveBeenCalledWith(exam); + expect(component.examChecklist.existsUnassessedQuizzes).toEqual(examChecklist.existsUnassessedQuizzes); + }); + + it('should set existsUnsubmittedExercises correctly', () => { + const getExamStatisticsStub = jest.spyOn(examChecklistService, 'getExamStatistics').mockReturnValue(of(examChecklist)); + + component.ngOnChanges(); + + expect(getExamStatisticsStub).toHaveBeenCalledOnce(); + expect(getExamStatisticsStub).toHaveBeenCalledWith(exam); + expect(component.examChecklist.existsUnsubmittedExercises).toEqual(examChecklist.existsUnsubmittedExercises); + }); });