diff --git a/src/main/webapp/app/exam/participate/exam-participation.component.html b/src/main/webapp/app/exam/participate/exam-participation.component.html index 391c435fee09..e90b28528020 100644 --- a/src/main/webapp/app/exam/participate/exam-participation.component.html +++ b/src/main/webapp/app/exam/participate/exam-participation.component.html @@ -56,22 +56,40 @@
@switch (exercise.type) { @case (QUIZ) { - + @if (exercise.studentParticipations[0].submissions) { + + } } @case (FILEUPLOAD) { - + @if (exercise.studentParticipations[0].submissions) { + + } } @case (TEXT) { - + @if (exercise.studentParticipations[0].submissions) { + + } } @case (MODELING) { - + @if (exercise.studentParticipations[0].submissions) { + + } } @case (PROGRAMMING) { + + + + diff --git a/src/main/webapp/app/exam/participate/exercises/exercise-save-button/exercise-save-button.component.scss b/src/main/webapp/app/exam/participate/exercises/exercise-save-button/exercise-save-button.component.scss new file mode 100644 index 000000000000..0b4d4eb0b98c --- /dev/null +++ b/src/main/webapp/app/exam/participate/exercises/exercise-save-button/exercise-save-button.component.scss @@ -0,0 +1,3 @@ +.saved { + --fa-secondary-opacity: 1; +} diff --git a/src/main/webapp/app/exam/participate/exercises/exercise-save-button/exercise-save-button.component.ts b/src/main/webapp/app/exam/participate/exercises/exercise-save-button/exercise-save-button.component.ts new file mode 100644 index 000000000000..01e627a6941f --- /dev/null +++ b/src/main/webapp/app/exam/participate/exercises/exercise-save-button/exercise-save-button.component.ts @@ -0,0 +1,25 @@ +import { Component, input, output } from '@angular/core'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { faFloppyDisk } from '@fortawesome/free-solid-svg-icons'; +import { facSaveSuccess } from '../../../../../content/icons/icons'; +import { Submission } from 'app/entities/submission.model'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; + +@Component({ + selector: 'jhi-exercise-save-button', + templateUrl: './exercise-save-button.component.html', + styleUrls: ['./exercise-save-button.component.scss'], + standalone: true, + imports: [FaIconComponent, TranslateDirective], +}) +export class ExerciseSaveButtonComponent { + protected readonly faFloppyDisk = faFloppyDisk; + protected readonly facSaveSuccess = facSaveSuccess; + + submission = input(); + save = output(); + + onSave() { + this.save.emit(); + } +} diff --git a/src/main/webapp/app/exam/participate/exercises/file-upload/file-upload-exam-submission.component.html b/src/main/webapp/app/exam/participate/exercises/file-upload/file-upload-exam-submission.component.html index be860bc79852..e85e710164e9 100644 --- a/src/main/webapp/app/exam/participate/exercises/file-upload/file-upload-exam-submission.component.html +++ b/src/main/webapp/app/exam/participate/exercises/file-upload/file-upload-exam-submission.component.html @@ -3,14 +3,14 @@

{{ exercise.exerciseGroup?.title }} - -  ({{ exercise.maxPoints }} {{ 'artemisApp.examParticipation.points' | artemisTranslate }}@if (exercise.bonusPoints) { - , {{ exercise.bonusPoints }} {{ 'artemisApp.examParticipation.bonus' | artemisTranslate }} - }) @if (exercise.includedInOverallScore !== IncludedInOverallScore.INCLUDED_COMPLETELY) { - - } + + + @if (exercise.includedInOverallScore !== IncludedInOverallScore.INCLUDED_COMPLETELY) { + + }


diff --git a/src/main/webapp/app/exam/participate/exercises/modeling/modeling-exam-submission.component.html b/src/main/webapp/app/exam/participate/exercises/modeling/modeling-exam-submission.component.html index aa3306277c53..d1e89ff3f1ea 100644 --- a/src/main/webapp/app/exam/participate/exercises/modeling/modeling-exam-submission.component.html +++ b/src/main/webapp/app/exam/participate/exercises/modeling/modeling-exam-submission.component.html @@ -1,17 +1,20 @@ @if (exercise) { -

- - {{ exercise.exerciseGroup?.title }} - - -  ({{ exercise.maxPoints }} {{ 'artemisApp.examParticipation.points' | artemisTranslate }}@if (exercise.bonusPoints) { +
+

+ + {{ exercise.exerciseGroup?.title }} + , {{ exercise.bonusPoints }} {{ 'artemisApp.examParticipation.bonus' | artemisTranslate }} - }) @if (exercise.includedInOverallScore !== IncludedInOverallScore.INCLUDED_COMPLETELY) { - - } -

+ [jhiTranslate]="exercise.bonusPoints ? 'artemisApp.examParticipation.bonus' : 'artemisApp.examParticipation.points'" + [translateValues]="{ points: exercise.maxPoints, bonusPoints: exercise.bonusPoints }" + > + + @if (exercise.includedInOverallScore !== IncludedInOverallScore.INCLUDED_COMPLETELY) { + + } +

+ +

@@ -38,7 +41,7 @@

-   +   diff --git a/src/main/webapp/app/exam/participate/exercises/modeling/modeling-exam-submission.component.ts b/src/main/webapp/app/exam/participate/exercises/modeling/modeling-exam-submission.component.ts index 60957411fcf8..d16f4d56ff6d 100644 --- a/src/main/webapp/app/exam/participate/exercises/modeling/modeling-exam-submission.component.ts +++ b/src/main/webapp/app/exam/participate/exercises/modeling/modeling-exam-submission.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit, ViewChild } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit, ViewChild, input, output } from '@angular/core'; import { UMLModel } from '@ls1intum/apollon'; import dayjs from 'dayjs/esm'; import { ModelingSubmission } from 'app/entities/modeling-submission.model'; @@ -34,12 +34,17 @@ export class ModelingExamSubmissionComponent extends ExamSubmissionComponent imp exercise: ModelingExercise; umlModel: UMLModel; // input model for Apollon+ + // explicitly needed to track if submission.isSynced is changed, otherwise component + // does not update the state due to onPush strategy + isSubmissionSynced = input(); + saveCurrentExercise = output(); + explanationText: string; // current explanation text readonly IncludedInOverallScore = IncludedInOverallScore; // Icons - farListAlt = faListAlt; + protected readonly faListAlt = faListAlt; constructor(changeDetectorReference: ChangeDetectorRef) { super(changeDetectorReference); @@ -154,4 +159,11 @@ export class ModelingExamSubmissionComponent extends ExamSubmissionComponent imp this.changeDetectorReference.detectChanges(); } } + + /** + * Trigger save action in exam participation component + */ + notifyTriggerSave() { + this.saveCurrentExercise.emit(); + } } diff --git a/src/main/webapp/app/exam/participate/exercises/programming/programming-exam-submission.component.html b/src/main/webapp/app/exam/participate/exercises/programming/programming-exam-submission.component.html index 2bb8078c07ce..35129261f2ec 100644 --- a/src/main/webapp/app/exam/participate/exercises/programming/programming-exam-submission.component.html +++ b/src/main/webapp/app/exam/participate/exercises/programming/programming-exam-submission.component.html @@ -2,14 +2,14 @@

{{ exercise.exerciseGroup?.title }} - -  ({{ exercise.maxPoints }} {{ 'artemisApp.examParticipation.points' | artemisTranslate }}@if (exercise.bonusPoints) { - , {{ exercise.bonusPoints }} {{ 'artemisApp.examParticipation.bonus' | artemisTranslate }} - }) @if (exercise.includedInOverallScore !== IncludedInOverallScore.INCLUDED_COMPLETELY) { - - } + + + @if (exercise.includedInOverallScore !== IncludedInOverallScore.INCLUDED_COMPLETELY) { + + }


diff --git a/src/main/webapp/app/exam/participate/exercises/quiz/quiz-exam-submission.component.html b/src/main/webapp/app/exam/participate/exercises/quiz/quiz-exam-submission.component.html index ace69b781b7e..3bbe2afbdfe3 100644 --- a/src/main/webapp/app/exam/participate/exercises/quiz/quiz-exam-submission.component.html +++ b/src/main/webapp/app/exam/participate/exercises/quiz/quiz-exam-submission.component.html @@ -1,14 +1,15 @@ -

- - {{ quizConfiguration.exerciseGroup?.title }} - - ({{ quizConfiguration.maxPoints }} {{ 'artemisApp.examParticipation.points' | artemisTranslate }}) +
+

+ + {{ quizConfiguration.exerciseGroup?.title }} + + @if (quizConfiguration.includedInOverallScore !== IncludedInOverallScore.INCLUDED_COMPLETELY) { } - -

+

+ +
diff --git a/src/main/webapp/app/exam/participate/exercises/quiz/quiz-exam-submission.component.ts b/src/main/webapp/app/exam/participate/exercises/quiz/quiz-exam-submission.component.ts index f967003238ba..26aa88a762d5 100644 --- a/src/main/webapp/app/exam/participate/exercises/quiz/quiz-exam-submission.component.ts +++ b/src/main/webapp/app/exam/participate/exercises/quiz/quiz-exam-submission.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, Input, OnInit, QueryList, ViewChildren } from '@angular/core'; +import { ChangeDetectorRef, Component, Input, OnInit, QueryList, ViewChildren, output } from '@angular/core'; import { Exercise, ExerciseType, IncludedInOverallScore } from 'app/entities/exercise.model'; import { AbstractQuizSubmission } from 'app/entities/quiz/abstract-quiz-exam-submission.model'; import { AnswerOption } from 'app/entities/quiz/answer-option.model'; @@ -53,6 +53,8 @@ export class QuizExamSubmissionComponent extends ExamSubmissionComponent impleme @Input() examTimeline = false; @Input() quizConfiguration: QuizConfiguration; + saveCurrentExercise = output(); + selectedAnswerOptions = new Map(); dragAndDropMappings = new Map(); shortAnswerSubmittedTexts = new Map(); @@ -285,4 +287,11 @@ export class QuizExamSubmissionComponent extends ExamSubmissionComponent impleme this.submissionVersion = submissionVersion; this.updateViewFromSubmissionVersion(); } + + /** + * Trigger save action in exam participation component + */ + notifyTriggerSave() { + this.saveCurrentExercise.emit(); + } } diff --git a/src/main/webapp/app/exam/participate/exercises/text/text-exam-submission.component.html b/src/main/webapp/app/exam/participate/exercises/text/text-exam-submission.component.html index cc0a6fba438f..4087130be3ea 100644 --- a/src/main/webapp/app/exam/participate/exercises/text/text-exam-submission.component.html +++ b/src/main/webapp/app/exam/participate/exercises/text/text-exam-submission.component.html @@ -1,17 +1,20 @@ @if (exercise) { -

- - {{ exercise.exerciseGroup?.title }} - - -  ({{ exercise.maxPoints }} {{ 'artemisApp.examParticipation.points' | artemisTranslate }}@if (exercise.bonusPoints) { +
+

+ + {{ exercise.exerciseGroup?.title }} + , {{ exercise.bonusPoints }} {{ 'artemisApp.examParticipation.bonus' | artemisTranslate }} - }) @if (exercise.includedInOverallScore !== IncludedInOverallScore.INCLUDED_COMPLETELY) { - - } -

+ [jhiTranslate]="exercise.bonusPoints ? 'artemisApp.examParticipation.bonus' : 'artemisApp.examParticipation.points'" + [translateValues]="{ points: exercise.maxPoints, bonusPoints: exercise.bonusPoints }" + > + + @if (exercise.includedInOverallScore !== IncludedInOverallScore.INCLUDED_COMPLETELY) { + + } +

+ +

@@ -40,7 +43,7 @@

-   +   diff --git a/src/main/webapp/app/exam/participate/exercises/text/text-exam-submission.component.ts b/src/main/webapp/app/exam/participate/exercises/text/text-exam-submission.component.ts index 3a493af08507..cb2dc3c0fd51 100644 --- a/src/main/webapp/app/exam/participate/exercises/text/text-exam-submission.component.ts +++ b/src/main/webapp/app/exam/participate/exercises/text/text-exam-submission.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, Input, OnInit, output } from '@angular/core'; import { TextEditorService } from 'app/exercises/text/participate/text-editor.service'; import { Subject } from 'rxjs'; import { TextSubmission } from 'app/entities/text/text-submission.model'; @@ -6,7 +6,7 @@ import { StringCountService } from 'app/exercises/text/participate/string-count. import { Exercise, ExerciseType, IncludedInOverallScore } from 'app/entities/exercise.model'; import { ExamSubmissionComponent } from 'app/exam/participate/exercises/exam-submission.component'; import { Submission } from 'app/entities/submission.model'; -import { faListAlt } from '@fortawesome/free-regular-svg-icons'; +import { faListAlt } from '@fortawesome/free-solid-svg-icons'; import { MAX_SUBMISSION_TEXT_LENGTH } from 'app/shared/constants/input.constants'; import { SubmissionVersion } from 'app/entities/submission-version.model'; import { htmlForMarkdown } from 'app/shared/util/markdown.conversion.util'; @@ -26,6 +26,8 @@ export class TextExamSubmissionComponent extends ExamSubmissionComponent impleme @Input() exercise: Exercise; + saveCurrentExercise = output(); + readonly IncludedInOverallScore = IncludedInOverallScore; readonly maxCharacterCount = MAX_SUBMISSION_TEXT_LENGTH; @@ -35,7 +37,7 @@ export class TextExamSubmissionComponent extends ExamSubmissionComponent impleme private textEditorInput = new Subject(); // Icons - farListAlt = faListAlt; + protected readonly faListAlt = faListAlt; constructor( private textService: TextEditorService, @@ -121,4 +123,11 @@ export class TextExamSubmissionComponent extends ExamSubmissionComponent impleme this.submissionVersion = submissionVersion; this.updateViewFromSubmissionVersion(); } + + /** + * Trigger save action in exam participation component + */ + notifyTriggerSave() { + this.saveCurrentExercise.emit(); + } } diff --git a/src/main/webapp/app/exam/participate/summary/exercises/header/exam-result-summary-exercise-card-header.component.html b/src/main/webapp/app/exam/participate/summary/exercises/header/exam-result-summary-exercise-card-header.component.html index 74dfce8f5638..4c9d11e7aab6 100644 --- a/src/main/webapp/app/exam/participate/summary/exercises/header/exam-result-summary-exercise-card-header.component.html +++ b/src/main/webapp/app/exam/participate/summary/exercises/header/exam-result-summary-exercise-card-header.component.html @@ -11,7 +11,7 @@
@if (resultsPublished && exerciseInfo?.achievedPoints !== undefined) {
- [{{ exerciseInfo?.achievedPoints }} / {{ exercise.maxPoints }} {{ 'artemisApp.examParticipation.points' | artemisTranslate }}] + [{{ exerciseInfo?.achievedPoints }} / {{ exercise.maxPoints }} {{ 'artemisApp.examParticipation.exercisePoints' | artemisTranslate }}]
@if (exerciseInfo?.resultIconClass) { @@ -22,7 +22,7 @@
} @else { - [{{ exercise.maxPoints }} {{ 'artemisApp.examParticipation.points' | artemisTranslate }}] + [{{ exercise.maxPoints }} {{ 'artemisApp.examParticipation.exercisePoints' | artemisTranslate }}] }
diff --git a/src/main/webapp/i18n/de/exam.json b/src/main/webapp/i18n/de/exam.json index c8644192874a..34cea3d9900d 100644 --- a/src/main/webapp/i18n/de/exam.json +++ b/src/main/webapp/i18n/de/exam.json @@ -314,8 +314,8 @@ "preparingParticipation": "Aufgabe wird vorbereitet. Dies kann ein paar Sekunden dauern.", "generateParticipationFailed": "Das Vorbereiten der Aufgabe ist fehlgeschlagen.", "generateParticipationRetry": "Erneut versuchen", - "points": "Punkte", - "bonus": "Bonuspunkte", + "points": "({{ points }} Punkte)", + "bonus": "({{ points }} Punkte, {{ bonusPoints }} Bonuspunkte)", "synced": "Aufgabe gespeichert", "notSynced": "Aufgabe nicht gespeichert", "notStarted": "Aufgabe nicht gestartet", @@ -338,7 +338,9 @@ "noActionRequired": "Artemis erfordert keine weitere Aktion, das Fenster kann geschlossen werden.", "followExamProtocol": "Halte dich an die Klausuranweisungen deiner Lehrenden.", "button": "Zusammenfassung der Klausur anzeigen{{ countdown }}" - } + }, + "saveExercise": "Aufgabe speichern", + "exerciseSaved": "Aufgabe gespeichert" }, "exerciseGroup": { "created": "Neue Aufgabengruppe erstellt", diff --git a/src/main/webapp/i18n/en/exam.json b/src/main/webapp/i18n/en/exam.json index 691db17fe332..374506d757af 100644 --- a/src/main/webapp/i18n/en/exam.json +++ b/src/main/webapp/i18n/en/exam.json @@ -314,8 +314,8 @@ "preparingParticipation": "Preparing exercise. This may take a few seconds.", "generateParticipationFailed": "The preparation of the exercise failed.", "generateParticipationRetry": "Retry", - "points": "Points", - "bonus": "Bonus Points", + "points": "({{ points }} Points)", + "bonus": "({{ points }} Points, {{ bonusPoints }} Bonus Points)", "synced": "Exercise saved", "notSynced": "Exercise not saved", "notStarted": "Exercise not started", @@ -338,7 +338,9 @@ "noActionRequired": "Artemis does not require any further action and this window can be closed.", "followExamProtocol": "Be sure to follow your instructor's exam protocol.", "button": "Show exam summary{{countdown}}" - } + }, + "saveExercise": "Save exercise", + "exerciseSaved": "Exercise saved" }, "exerciseGroup": { "created": "New exercise group created", diff --git a/src/test/javascript/spec/component/exam/participate/exercises/exercise-save-button.component.spec.ts b/src/test/javascript/spec/component/exam/participate/exercises/exercise-save-button.component.spec.ts new file mode 100644 index 000000000000..c99743924bb6 --- /dev/null +++ b/src/test/javascript/spec/component/exam/participate/exercises/exercise-save-button.component.spec.ts @@ -0,0 +1,95 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateService } from '@ngx-translate/core'; +import { input } from '@angular/core'; +import { MockPipe } from 'ng-mocks'; +import { MockTranslateService } from '../../../../helpers/mocks/service/mock-translate.service'; +import { ArtemisTestModule } from '../../../../test.module'; +import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; +import { By } from '@angular/platform-browser'; +import { ExerciseSaveButtonComponent } from '../../../../../../../main/webapp/app/exam/participate/exercises/exercise-save-button/exercise-save-button.component'; +import { Submission } from '../../../../../../../main/webapp/app/entities/submission.model'; +import { facSaveSuccess } from '../../../../../../../main/webapp/content/icons/icons'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { faFloppyDisk } from '@fortawesome/free-solid-svg-icons'; + +describe('ExerciseSaveButtonComponent', () => { + let component: ExerciseSaveButtonComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ArtemisTestModule], + declarations: [ExerciseSaveButtonComponent, MockPipe(ArtemisTranslatePipe)], + providers: [{ provide: TranslateService, useClass: MockTranslateService }], + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(ExerciseSaveButtonComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should create the component', () => { + expect(component).toBeTruthy(); + }); + + it('should disable the button if submission is synced', () => { + TestBed.runInInjectionContext(() => { + component.submission = input({ isSynced: true, submitted: false } as Submission); + }); + + fixture.detectChanges(); + + const button = fixture.debugElement.query(By.css('#save-exam')); + expect(button.nativeElement.disabled).toBeTrue(); + }); + + it('should enable the button if submission is not synced', () => { + TestBed.runInInjectionContext(() => { + component.submission = input({ isSynced: false, submitted: false } as Submission); + }); + fixture.detectChanges(); + + const button = fixture.debugElement.query(By.css('#save-exam')); + expect(button.nativeElement.disabled).toBeFalse(); + }); + + it('should display facSaveSuccess icon if submission is synced and submitted', () => { + TestBed.runInInjectionContext(() => { + component.submission = input({ isSynced: true, submitted: true } as Submission); + }); + fixture.detectChanges(); + + const icon = fixture.debugElement.query(By.directive(FaIconComponent)); + expect(icon.componentInstance.icon).toBe(facSaveSuccess); + }); + + it('should display faFloppyDisk icon if submission is not synced and submitted', () => { + TestBed.runInInjectionContext(() => { + component.submission = input({ isSynced: false, submitted: false } as Submission); + }); + fixture.detectChanges(); + + const icon = fixture.debugElement.query(By.directive(FaIconComponent)); + expect(icon.componentInstance.icon).toBe(faFloppyDisk); + }); + + it('should call onSave when the button is clicked and submission is not synced', () => { + TestBed.runInInjectionContext(() => { + component.submission = input({ isSynced: false, submitted: false } as Submission); + }); + fixture.detectChanges(); + + const onSaveSpy = jest.spyOn(component, 'onSave'); + + const button = fixture.debugElement.query(By.css('#save-exam')); + button.nativeElement.click(); + + expect(onSaveSpy).toHaveBeenCalled(); + }); +}); diff --git a/src/test/javascript/spec/component/exam/participate/exercises/file-upload-exam-submission.component.spec.ts b/src/test/javascript/spec/component/exam/participate/exercises/file-upload-exam-submission.component.spec.ts index 9c10ec5ca996..90a35bd8a76b 100644 --- a/src/test/javascript/spec/component/exam/participate/exercises/file-upload-exam-submission.component.spec.ts +++ b/src/test/javascript/spec/component/exam/participate/exercises/file-upload-exam-submission.component.spec.ts @@ -6,7 +6,7 @@ import { Course } from 'app/entities/course.model'; import { FullscreenComponent } from 'app/shared/fullscreen/fullscreen.component'; import { HtmlForMarkdownPipe } from 'app/shared/pipes/html-for-markdown.pipe'; import { ResizeableContainerComponent } from 'app/shared/resizeable-container/resizeable-container.component'; -import { MockComponent, MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponent, MockDirective, MockPipe, MockProvider } from 'ng-mocks'; import { MockTranslateService, TranslatePipeMock } from '../../../../helpers/mocks/service/mock-translate.service'; import { ArtemisTestModule } from '../../../../test.module'; import { IncludedInScoreBadgeComponent } from 'app/exercises/shared/exercise-headers/included-in-score-badge.component'; @@ -22,6 +22,7 @@ import { FileUploadSubmissionService } from 'app/exercises/file-upload/participa import { HttpResponse } from '@angular/common/http'; import { of } from 'rxjs'; import { ExamExerciseUpdateHighlighterComponent } from 'app/exam/participate/exercises/exam-exercise-update-highlighter/exam-exercise-update-highlighter.component'; +import { TranslateDirective } from '../../../../../../../main/webapp/app/shared/language/translate.directive'; describe('FileUploadExamSubmissionComponent', () => { let fixture: ComponentFixture; @@ -52,10 +53,11 @@ describe('FileUploadExamSubmissionComponent', () => { FileUploadExamSubmissionComponent, FullscreenComponent, ResizeableContainerComponent, - TranslatePipeMock, MockPipe(HtmlForMarkdownPipe, (markdown) => markdown as SafeHtml), + TranslatePipeMock, MockComponent(IncludedInScoreBadgeComponent), MockComponent(ExamExerciseUpdateHighlighterComponent), + MockDirective(TranslateDirective), ], providers: [MockProvider(ChangeDetectorRef), { provide: TranslateService, useClass: MockTranslateService }], }) @@ -78,6 +80,7 @@ describe('FileUploadExamSubmissionComponent', () => { }); it('should show static text in header', () => { + comp.examTimeline = false; fixture.detectChanges(); const el = fixture.debugElement.query((de) => de.nativeElement.textContent === 'artemisApp.exam.yourSolution'); expect(el).not.toBeNull(); @@ -87,8 +90,12 @@ describe('FileUploadExamSubmissionComponent', () => { const maxScore = 30; comp.exercise.maxPoints = maxScore; fixture.detectChanges(); - const el = fixture.debugElement.query((de) => de.nativeElement.textContent.includes(`(${maxScore} artemisApp.examParticipation.points)`)); + const el = fixture.debugElement.query(By.directive(TranslateDirective)); expect(el).not.toBeNull(); + + const directiveInstance = el.injector.get(TranslateDirective); + expect(directiveInstance.jhiTranslate).toBe('artemisApp.examParticipation.points'); + expect(directiveInstance.translateValues).toEqual({ points: maxScore, bonusPoints: 0 }); }); it('should show exercise bonus score if any', () => { @@ -97,11 +104,14 @@ describe('FileUploadExamSubmissionComponent', () => { const bonusPoints = 55; comp.exercise.bonusPoints = bonusPoints; fixture.detectChanges(); - const el = fixture.debugElement.query((de) => - de.nativeElement.textContent.includes(`(${maxScore} artemisApp.examParticipation.points, ${bonusPoints} artemisApp.examParticipation.bonus)`), - ); + const el = fixture.debugElement.query(By.directive(TranslateDirective)); expect(el).not.toBeNull(); + + const directiveInstance = el.injector.get(TranslateDirective); + expect(directiveInstance.jhiTranslate).toBe('artemisApp.examParticipation.bonus'); + expect(directiveInstance.translateValues).toEqual({ points: maxScore, bonusPoints: bonusPoints }); }); + it('should show problem statement if there is any', () => { fixture.detectChanges(); const el = fixture.debugElement.query((de) => de.nativeElement.textContent === mockExercise.problemStatement); diff --git a/src/test/javascript/spec/component/exam/participate/exercises/modeling-exam-submission.component.spec.ts b/src/test/javascript/spec/component/exam/participate/exercises/modeling-exam-submission.component.spec.ts index 60731362866e..5b2845a76b22 100644 --- a/src/test/javascript/spec/component/exam/participate/exercises/modeling-exam-submission.component.spec.ts +++ b/src/test/javascript/spec/component/exam/participate/exercises/modeling-exam-submission.component.spec.ts @@ -10,13 +10,15 @@ import { ModelingEditorComponent } from 'app/exercises/modeling/shared/modeling- import { FullscreenComponent } from 'app/shared/fullscreen/fullscreen.component'; import { HtmlForMarkdownPipe } from 'app/shared/pipes/html-for-markdown.pipe'; import { ResizeableContainerComponent } from 'app/shared/resizeable-container/resizeable-container.component'; -import { MockComponent, MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponent, MockDirective, MockPipe, MockProvider } from 'ng-mocks'; import { TranslatePipeMock } from '../../../../helpers/mocks/service/mock-translate.service'; import { ArtemisTestModule } from '../../../../test.module'; import { IncludedInScoreBadgeComponent } from 'app/exercises/shared/exercise-headers/included-in-score-badge.component'; import { ExamExerciseUpdateHighlighterComponent } from 'app/exam/participate/exercises/exam-exercise-update-highlighter/exam-exercise-update-highlighter.component'; import { NgbTooltipMocksModule } from '../../../../helpers/mocks/directive/ngbTooltipMocks.module'; import { SubmissionVersion } from 'app/entities/submission-version.model'; +import { ExerciseSaveButtonComponent } from 'app/exam/participate/exercises/exercise-save-button/exercise-save-button.component'; +import { TranslateDirective } from '../../../../../../../main/webapp/app/shared/language/translate.directive'; describe('ModelingExamSubmissionComponent', () => { let fixture: ComponentFixture; @@ -49,6 +51,8 @@ describe('ModelingExamSubmissionComponent', () => { MockPipe(HtmlForMarkdownPipe, (markdown) => markdown as SafeHtml), MockComponent(IncludedInScoreBadgeComponent), MockComponent(ExamExerciseUpdateHighlighterComponent), + MockComponent(ExerciseSaveButtonComponent), + MockDirective(TranslateDirective), ], providers: [MockProvider(ChangeDetectorRef)], }) @@ -78,8 +82,12 @@ describe('ModelingExamSubmissionComponent', () => { const maxScore = 30; comp.exercise.maxPoints = maxScore; fixture.detectChanges(); - const el = fixture.debugElement.query((de) => de.nativeElement.textContent.includes(`(${maxScore} artemisApp.examParticipation.points)`)); + const el = fixture.debugElement.query(By.directive(TranslateDirective)); expect(el).not.toBeNull(); + + const directiveInstance = el.injector.get(TranslateDirective); + expect(directiveInstance.jhiTranslate).toBe('artemisApp.examParticipation.points'); + expect(directiveInstance.translateValues).toEqual({ points: maxScore, bonusPoints: 0 }); }); it('should show exercise bonus score if any', () => { @@ -88,10 +96,20 @@ describe('ModelingExamSubmissionComponent', () => { const bonusPoints = 55; comp.exercise.bonusPoints = bonusPoints; fixture.detectChanges(); - const el = fixture.debugElement.query((de) => - de.nativeElement.textContent.includes(`(${maxScore} artemisApp.examParticipation.points, ${bonusPoints} artemisApp.examParticipation.bonus)`), - ); + const el = fixture.debugElement.query(By.directive(TranslateDirective)); expect(el).not.toBeNull(); + + const directiveInstance = el.injector.get(TranslateDirective); + expect(directiveInstance.jhiTranslate).toBe('artemisApp.examParticipation.bonus'); + expect(directiveInstance.translateValues).toEqual({ points: maxScore, bonusPoints: bonusPoints }); + }); + + it('should call triggerSave if save exercise button is clicked', () => { + fixture.detectChanges(); + const saveExerciseSpy = jest.spyOn(comp, 'notifyTriggerSave'); + const saveButton = fixture.debugElement.query(By.directive(ExerciseSaveButtonComponent)); + saveButton.triggerEventHandler('save', null); + expect(saveExerciseSpy).toHaveBeenCalledOnce(); }); it('should show modeling editor with correct props when there is submission and exercise', () => { diff --git a/src/test/javascript/spec/component/exam/participate/exercises/quiz-exam-submission.component.spec.ts b/src/test/javascript/spec/component/exam/participate/exercises/quiz-exam-submission.component.spec.ts index c83015c99c75..e3e9c1a94556 100644 --- a/src/test/javascript/spec/component/exam/participate/exercises/quiz-exam-submission.component.spec.ts +++ b/src/test/javascript/spec/component/exam/participate/exercises/quiz-exam-submission.component.spec.ts @@ -15,7 +15,7 @@ import { QuizExamSubmissionComponent } from 'app/exam/participate/exercises/quiz import { IncludedInScoreBadgeComponent } from 'app/exercises/shared/exercise-headers/included-in-score-badge.component'; import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { ArtemisQuizService } from 'app/shared/quiz/quiz.service'; -import { MockComponent, MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponent, MockDirective, MockPipe, MockProvider } from 'ng-mocks'; import { MultipleChoiceQuestionComponent } from 'app/exercises/quiz/shared/questions/multiple-choice-question/multiple-choice-question.component'; import { DragAndDropQuestionComponent } from 'app/exercises/quiz/shared/questions/drag-and-drop-question/drag-and-drop-question.component'; import { ShortAnswerQuestionComponent } from 'app/exercises/quiz/shared/questions/short-answer-question/short-answer-question.component'; @@ -25,6 +25,9 @@ import { ModelingSubmission } from 'app/entities/modeling-submission.model'; import { QuizExercise } from 'app/entities/quiz/quiz-exercise.model'; import { Course } from 'app/entities/course.model'; import { provideRouter } from '@angular/router'; +import { ExerciseSaveButtonComponent } from 'app/exam/participate/exercises/exercise-save-button/exercise-save-button.component'; +import { TranslateDirective } from '../../../../../../../main/webapp/app/shared/language/translate.directive'; +import { By } from '@angular/platform-browser'; describe('QuizExamSubmissionComponent', () => { let fixture: ComponentFixture; @@ -55,6 +58,8 @@ describe('QuizExamSubmissionComponent', () => { MockComponent(MultipleChoiceQuestionComponent), MockComponent(DragAndDropQuestionComponent), MockComponent(ShortAnswerQuestionComponent), + MockComponent(ExerciseSaveButtonComponent), + MockDirective(TranslateDirective), ], providers: [provideRouter([]), MockProvider(ArtemisQuizService)], }) @@ -211,4 +216,21 @@ describe('QuizExamSubmissionComponent', () => { expect(component.dragAndDropMappings.size).toBe(1); expect(component.shortAnswerSubmittedTexts.size).toBe(0); }); + + it('should call triggerSave if save exercise button is clicked', () => { + const submissionVersion = { + content: + '[ {\r\n "quizQuestion" : {\r\n "type" : "drag-and-drop",\r\n "id" : 2,\r\n "title" : "dnd image",\r\n "text" : "Enter your long question if needed",\r\n "hint" : "Add a hint here (visible during the quiz via ?-Button)",\r\n "points" : 1,\r\n "scoringType" : "PROPORTIONAL_WITH_PENALTY",\r\n "randomizeOrder" : true,\r\n "invalid" : false,\r\n "backgroundFilePath" : "/api/files/drag-and-drop/backgrounds/14/DragAndDropBackground_2023-07-08T19-35-26-953_a3265da6.jpg",\r\n "dropLocations" : [ {\r\n "id" : 12,\r\n "posX" : 45.0,\r\n "posY" : 120.0,\r\n "width" : 62.0,\r\n "height" : 52.0,\r\n "invalid" : false\r\n } ],\r\n "dragItems" : [ {\r\n "id" : 11,\r\n "pictureFilePath" : "/api/files/drag-and-drop/drag-items/11/DragItem_2023-07-08T19-35-26-956_2ffe94ba.jpg",\r\n "invalid" : false\r\n } ]\r\n },\r\n "mappings" : [ {\r\n "invalid" : false,\r\n "dragItem" : {\r\n "id" : 11,\r\n "pictureFilePath" : "/api/files/drag-and-drop/drag-items/11/DragItem_2023-07-08T19-35-26-956_2ffe94ba.jpg",\r\n "invalid" : false\r\n },\r\n "dropLocation" : {\r\n "id" : 12,\r\n "posX" : 45.0,\r\n "posY" : 120.0,\r\n "width" : 62.0,\r\n "height" : 52.0,\r\n "invalid" : false\r\n }\r\n } ]\r\n} ]', + } as unknown as SubmissionVersion; + component.studentSubmission = new ModelingSubmission(); + component.exercise = new QuizExercise(new Course(), undefined); + component.exercise.quizQuestions = [dragAndDropQuestion]; + component.quizConfiguration = { quizQuestions: [dragAndDropQuestion] }; + component.setSubmissionVersion(submissionVersion); + fixture.detectChanges(); + const saveExerciseSpy = jest.spyOn(component, 'notifyTriggerSave'); + const saveButton = fixture.debugElement.query(By.directive(ExerciseSaveButtonComponent)); + saveButton.triggerEventHandler('save', null); + expect(saveExerciseSpy).toHaveBeenCalledOnce(); + }); }); diff --git a/src/test/javascript/spec/component/exam/participate/exercises/text-exam-submission.component.spec.ts b/src/test/javascript/spec/component/exam/participate/exercises/text-exam-submission.component.spec.ts index 176692131b10..a0850296fc26 100644 --- a/src/test/javascript/spec/component/exam/participate/exercises/text-exam-submission.component.spec.ts +++ b/src/test/javascript/spec/component/exam/participate/exercises/text-exam-submission.component.spec.ts @@ -17,6 +17,7 @@ import { ArtemisTestModule } from '../../../../test.module'; import { ResizeableContainerComponent } from 'app/shared/resizeable-container/resizeable-container.component'; import dayjs from 'dayjs/esm'; import { TranslateDirective } from 'app/shared/language/translate.directive'; +import { ExerciseSaveButtonComponent } from 'app/exam/participate/exercises/exercise-save-button/exercise-save-button.component'; describe('TextExamSubmissionComponent', () => { let fixture: ComponentFixture; @@ -39,6 +40,7 @@ describe('TextExamSubmissionComponent', () => { MockComponent(IncludedInScoreBadgeComponent), MockComponent(ExamExerciseUpdateHighlighterComponent), MockComponent(ResizeableContainerComponent), + MockComponent(ExerciseSaveButtonComponent), MockDirective(TranslateDirective), ], providers: [MockProvider(TextEditorService), MockProvider(ArtemisMarkdownService)], @@ -140,4 +142,15 @@ describe('TextExamSubmissionComponent', () => { expect(component.answer).toBe('submission version'); expect(component.submissionVersion).toBe(submissionVersion); }); + + it('should call triggerSave if save exercise button is clicked', () => { + component.exercise = exercise; + textSubmission.text = 'Hello World'; + component.studentSubmission = textSubmission; + fixture.detectChanges(); + const saveExerciseSpy = jest.spyOn(component, 'notifyTriggerSave'); + const saveButton = fixture.debugElement.query(By.directive(ExerciseSaveButtonComponent)); + saveButton.triggerEventHandler('save', null); + expect(saveExerciseSpy).toHaveBeenCalledOnce(); + }); }); diff --git a/src/test/playwright/e2e/exam/ExamDateVerification.spec.ts b/src/test/playwright/e2e/exam/ExamDateVerification.spec.ts index 48d80de67065..40981da6f03f 100644 --- a/src/test/playwright/e2e/exam/ExamDateVerification.spec.ts +++ b/src/test/playwright/e2e/exam/ExamDateVerification.spec.ts @@ -92,7 +92,8 @@ test.describe('Exam date verification', () => { await examNavigation.openOrSaveExerciseByTitle(exercise.exerciseGroup!.title!); await page.hover('.fa-save-success'); - await expect(page.getByText('Exercise saved')).toBeVisible(); + // nth(0) is the button, nth(1) is the ngtooltip, which is tested + await expect(page.getByText('Exercise saved').nth(1)).toBeVisible(); }); test('Exam ends after end time', async ({ page, login, examAPIRequests, exerciseAPIRequests, examStartEnd, examNavigation, textExerciseEditor, examParticipation }) => {