diff --git a/src/main/webapp/app/course/learning-paths/components/learning-path-lecture-unit/learning-path-lecture-unit.component.html b/src/main/webapp/app/course/learning-paths/components/learning-path-lecture-unit/learning-path-lecture-unit.component.html index 2341a771ae26..afe1b2e3e29b 100644 --- a/src/main/webapp/app/course/learning-paths/components/learning-path-lecture-unit/learning-path-lecture-unit.component.html +++ b/src/main/webapp/app/course/learning-paths/components/learning-path-lecture-unit/learning-path-lecture-unit.component.html @@ -6,21 +6,30 @@ } @if (lectureUnit(); as lectureUnit) { - @switch (lectureUnit.type) { - @case (LectureUnitType.VIDEO) { - - } - @case (LectureUnitType.TEXT) { - - } - @case (LectureUnitType.ATTACHMENT) { - - } - @case (LectureUnitType.EXERCISE) { - - } - @case (LectureUnitType.ONLINE) { - +
+
+ @switch (lectureUnit.type) { + @case (LectureUnitType.VIDEO) { + + } + @case (LectureUnitType.TEXT) { + + } + @case (LectureUnitType.ATTACHMENT) { + + } + @case (LectureUnitType.EXERCISE) { + + } + @case (LectureUnitType.ONLINE) { + + } + } +
+ @if (isCommunicationEnabled()) { +
+ +
} - } +
} diff --git a/src/main/webapp/app/course/learning-paths/components/learning-path-lecture-unit/learning-path-lecture-unit.component.ts b/src/main/webapp/app/course/learning-paths/components/learning-path-lecture-unit/learning-path-lecture-unit.component.ts index c927e2a4532e..77f4cf3dd262 100644 --- a/src/main/webapp/app/course/learning-paths/components/learning-path-lecture-unit/learning-path-lecture-unit.component.ts +++ b/src/main/webapp/app/course/learning-paths/components/learning-path-lecture-unit/learning-path-lecture-unit.component.ts @@ -1,4 +1,4 @@ -import { Component, InputSignal, WritableSignal, effect, inject, input, signal } from '@angular/core'; +import { Component, computed, effect, inject, input, signal } from '@angular/core'; import { LectureUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/lectureUnit.service'; import { AlertService } from 'app/core/util/alert.service'; import { LectureUnit, LectureUnitType } from 'app/entities/lecture-unit/lectureUnit.model'; @@ -11,24 +11,30 @@ import { VideoUnitComponent } from 'app/overview/course-lectures/video-unit/vide import { TextUnitComponent } from 'app/overview/course-lectures/text-unit/text-unit.component'; import { AttachmentUnitComponent } from 'app/overview/course-lectures/attachment-unit/attachment-unit.component'; import { OnlineUnitComponent } from 'app/overview/course-lectures/online-unit/online-unit.component'; +import { isCommunicationEnabled } from 'app/entities/course.model'; +import { DiscussionSectionComponent } from 'app/overview/discussion-section/discussion-section.component'; @Component({ selector: 'jhi-learning-path-lecture-unit', standalone: true, - imports: [ArtemisLectureUnitsModule, ArtemisSharedModule, VideoUnitComponent, TextUnitComponent, AttachmentUnitComponent, OnlineUnitComponent], + imports: [ArtemisLectureUnitsModule, ArtemisSharedModule, VideoUnitComponent, TextUnitComponent, AttachmentUnitComponent, OnlineUnitComponent, DiscussionSectionComponent], templateUrl: './learning-path-lecture-unit.component.html', }) export class LearningPathLectureUnitComponent { protected readonly LectureUnitType = LectureUnitType; - private readonly lectureUnitService: LectureUnitService = inject(LectureUnitService); + private readonly lectureUnitService = inject(LectureUnitService); private readonly learningPathNavigationService = inject(LearningPathNavigationService); - private readonly alertService: AlertService = inject(AlertService); + private readonly alertService = inject(AlertService); - readonly lectureUnitId: InputSignal = input.required(); - readonly isLoading: WritableSignal = signal(false); + readonly lectureUnitId = input.required(); + readonly isLoading = signal(false); readonly lectureUnit = signal(undefined); + readonly lecture = computed(() => this.lectureUnit()?.lecture); + + readonly isCommunicationEnabled = computed(() => isCommunicationEnabled(this.lecture()?.course)); + constructor() { effect(() => this.loadLectureUnit(this.lectureUnitId()), { allowSignalWrites: true }); } @@ -46,11 +52,9 @@ export class LearningPathLectureUnitComponent { } setLearningObjectCompletion(completionEvent: LectureUnitCompletionEvent): void { - try { - this.lectureUnitService.completeLectureUnit(this.lectureUnit()!.lecture!, completionEvent); + this.lectureUnitService.completeLectureUnit(this.lectureUnit()!.lecture!, completionEvent); + if (this.lectureUnit()?.completed === completionEvent.completed) { this.learningPathNavigationService.setCurrentLearningObjectCompletion(completionEvent.completed); - } catch (error) { - this.alertService.error(error); } } } diff --git a/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.ts b/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.ts index 1aa64c85f315..7d96b1de44ea 100644 --- a/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.ts +++ b/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.component.ts @@ -38,8 +38,5 @@ export class LearningPathLectureUnitViewComponent { */ onChildActivate(instance: DiscussionSectionComponent) { this.discussionComponent = instance; // save the reference to the component instance - if (this.lecture) { - instance.lecture = this.lecture; - } } } diff --git a/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.module.ts b/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.module.ts index d4c29200f2d0..090649b6990f 100644 --- a/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.module.ts +++ b/src/main/webapp/app/course/learning-paths/participate/lecture-unit/learning-path-lecture-unit-view.module.ts @@ -19,13 +19,6 @@ const routes: Routes = [ pageTitle: 'overview.learningPath', }, canActivate: [UserRouteAccessService], - children: [ - { - path: '', - pathMatch: 'full', - loadChildren: () => import('app/overview/discussion-section/discussion-section.module').then((m) => m.DiscussionSectionModule), - }, - ], }, ]; diff --git a/src/main/webapp/app/overview/course-lectures/course-lecture-details.component.html b/src/main/webapp/app/overview/course-lectures/course-lecture-details.component.html index 3e27cd79cf9b..2bec1658d7e9 100644 --- a/src/main/webapp/app/overview/course-lectures/course-lecture-details.component.html +++ b/src/main/webapp/app/overview/course-lectures/course-lecture-details.component.html @@ -142,7 +142,7 @@
@if (lecture && (isCommunicationEnabled(lecture.course) || isMessagingEnabled(lecture.course))) { - + }
diff --git a/src/main/webapp/app/overview/course-lectures/course-lecture-details.component.ts b/src/main/webapp/app/overview/course-lectures/course-lecture-details.component.ts index e1ab434077ff..a87074793132 100644 --- a/src/main/webapp/app/overview/course-lectures/course-lecture-details.component.ts +++ b/src/main/webapp/app/overview/course-lectures/course-lecture-details.component.ts @@ -9,7 +9,6 @@ import { Attachment } from 'app/entities/attachment.model'; import { LectureService } from 'app/lecture/lecture.service'; import { LectureUnit, LectureUnitType } from 'app/entities/lecture-unit/lectureUnit.model'; import { AttachmentUnit } from 'app/entities/lecture-unit/attachmentUnit.model'; -import { DiscussionSectionComponent } from 'app/overview/discussion-section/discussion-section.component'; import { onError } from 'app/shared/util/global.utils'; import { finalize, tap } from 'rxjs/operators'; import { AlertService } from 'app/core/util/alert.service'; @@ -39,7 +38,6 @@ export class CourseLectureDetailsComponent extends AbstractScienceComponent impl lecture?: Lecture; isDownloadingLink?: string; lectureUnits: LectureUnit[] = []; - discussionComponent?: DiscussionSectionComponent; hasPdfLectureUnit: boolean; paramsSubscription: Subscription; @@ -107,10 +105,6 @@ export class CourseLectureDetailsComponent extends AbstractScienceComponent impl (unit) => unit.attachment?.link?.split('.').pop()!.toLocaleLowerCase() === 'pdf', ).length > 0; } - if (this.discussionComponent) { - // We need to manually update the lecture property of the student questions component - this.discussionComponent.lecture = this.lecture; - } this.endsSameDay = !!this.lecture?.startDate && !!this.lecture.endDate && dayjs(this.lecture.startDate).isSame(this.lecture.endDate, 'day'); }, error: (errorResponse: HttpErrorResponse) => onError(this.alertService, errorResponse), @@ -160,18 +154,6 @@ export class CourseLectureDetailsComponent extends AbstractScienceComponent impl this.lectureUnitService.completeLectureUnit(this.lecture!, event); } - /** - * This function gets called if the router outlet gets activated. This is - * used only for the DiscussionComponent - * @param instance The component instance - */ - onChildActivate(instance: DiscussionSectionComponent) { - this.discussionComponent = instance; // save the reference to the component instance - if (this.lecture) { - instance.lecture = this.lecture; - } - } - ngOnDestroy() { this.paramsSubscription?.unsubscribe(); this.profileSubscription?.unsubscribe(); diff --git a/src/main/webapp/app/overview/course-lectures/course-lecture-details.module.ts b/src/main/webapp/app/overview/course-lectures/course-lecture-details.module.ts index 8885f4eba764..315de44054b0 100644 --- a/src/main/webapp/app/overview/course-lectures/course-lecture-details.module.ts +++ b/src/main/webapp/app/overview/course-lectures/course-lecture-details.module.ts @@ -13,6 +13,7 @@ import { VideoUnitComponent } from 'app/overview/course-lectures/video-unit/vide import { TextUnitComponent } from 'app/overview/course-lectures/text-unit/text-unit.component'; import { OnlineUnitComponent } from 'app/overview/course-lectures/online-unit/online-unit.component'; import { AttachmentUnitComponent } from 'app/overview/course-lectures/attachment-unit/attachment-unit.component'; +import { DiscussionSectionComponent } from 'app/overview/discussion-section/discussion-section.component'; const routes: Routes = [ { @@ -23,13 +24,6 @@ const routes: Routes = [ pageTitle: 'overview.lectures', }, canActivate: [UserRouteAccessService], - children: [ - { - path: '', - pathMatch: 'full', - loadChildren: () => import('../discussion-section/discussion-section.module').then((m) => m.DiscussionSectionModule), - }, - ], }, ]; @NgModule({ @@ -45,6 +39,7 @@ const routes: Routes = [ TextUnitComponent, OnlineUnitComponent, AttachmentUnitComponent, + DiscussionSectionComponent, ], declarations: [CourseLectureDetailsComponent], exports: [CourseLectureDetailsComponent], diff --git a/src/main/webapp/app/overview/discussion-section/discussion-section.component.ts b/src/main/webapp/app/overview/discussion-section/discussion-section.component.ts index fae15df461dd..63d5873cc3b1 100644 --- a/src/main/webapp/app/overview/discussion-section/discussion-section.component.ts +++ b/src/main/webapp/app/overview/discussion-section/discussion-section.component.ts @@ -1,4 +1,4 @@ -import { AfterViewInit, Component, ElementRef, Input, OnDestroy, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core'; +import { AfterViewInit, Component, ElementRef, OnDestroy, QueryList, ViewChild, ViewChildren, effect, input } from '@angular/core'; import interact from 'interactjs'; import { Exercise } from 'app/entities/exercise.model'; import { Lecture } from 'app/entities/lecture.model'; @@ -14,17 +14,22 @@ import { CourseDiscussionDirective } from 'app/shared/metis/course-discussion.di import { FormBuilder } from '@angular/forms'; import { Channel, ChannelDTO } from 'app/entities/metis/conversation/channel.model'; import { ChannelService } from 'app/shared/metis/conversations/channel.service'; -import { MetisConversationService } from 'app/shared/metis/metis-conversation.service'; +import { ArtemisSharedModule } from 'app/shared/shared.module'; +import { ArtemisPlagiarismCasesSharedModule } from 'app/course/plagiarism-cases/shared/plagiarism-cases-shared.module'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { InfiniteScrollModule } from 'ngx-infinite-scroll'; @Component({ selector: 'jhi-discussion-section', templateUrl: './discussion-section.component.html', styleUrls: ['./discussion-section.component.scss'], + imports: [FontAwesomeModule, ArtemisSharedModule, ArtemisPlagiarismCasesSharedModule, InfiniteScrollModule], + standalone: true, providers: [MetisService], }) -export class DiscussionSectionComponent extends CourseDiscussionDirective implements OnInit, AfterViewInit, OnDestroy { - @Input() exercise?: Exercise; - @Input() lecture?: Lecture; +export class DiscussionSectionComponent extends CourseDiscussionDirective implements AfterViewInit, OnDestroy { + exercise = input(); + lecture = input(); @ViewChild(PostCreateEditModalComponent) postCreateEditModal?: PostCreateEditModalComponent; @ViewChildren('postingThread') messages: QueryList; @@ -60,22 +65,18 @@ export class DiscussionSectionComponent extends CourseDiscussionDirective implem private activatedRoute: ActivatedRoute, private router: Router, private formBuilder: FormBuilder, - private metisConversationService: MetisConversationService, ) { super(metisService); + effect(() => this.loadData(this.exercise(), this.lecture())); } - /** - * on initialization: initializes the metis service, fetches the posts for the exercise or lecture the discussion section is placed at, - * creates the subscription to posts to stay updated on any changes of posts in this course - */ - ngOnInit(): void { + loadData(exercise?: Exercise, lecture?: Lecture): void { this.paramSubscription = combineLatest({ params: this.activatedRoute.params, queryParams: this.activatedRoute.queryParams, }).subscribe((routeParams: { params: Params; queryParams: Params }) => { this.currentPostId = +routeParams.queryParams.postId; - this.course = this.exercise?.course ?? this.lecture?.course; + this.course = exercise?.course ?? lecture?.course; this.metisService.setCourse(this.course); this.metisService.setPageType(this.PAGE_TYPE); if (routeParams.params.courseId) { @@ -146,14 +147,14 @@ export class DiscussionSectionComponent extends CourseDiscussionDirective implem // Currently, an additional REST call is made to retrieve the channel associated with the lecture/exercise // TODO: Add the channel to the response for loading the lecture/exercise - if (this.lecture?.id) { + if (this.lecture()) { this.channelService - .getChannelOfLecture(courseId, this.lecture.id) + .getChannelOfLecture(courseId, this.lecture()!.id!) .pipe(map((res: HttpResponse) => res.body)) .subscribe(getChannel()); - } else if (this.exercise?.id) { + } else if (this.exercise()) { this.channelService - .getChannelOfExercise(courseId, this.exercise.id) + .getChannelOfExercise(courseId, this.exercise()!.id!) .pipe(map((res: HttpResponse) => res.body)) .subscribe(getChannel()); } @@ -271,8 +272,8 @@ export class DiscussionSectionComponent extends CourseDiscussionDirective implem resetFormGroup(): void { this.formGroup = this.formBuilder.group({ conversationId: this.channel?.id, - exerciseId: this.exercise?.id, - lectureId: this.lecture?.id, + exerciseId: this.exercise()?.id, + lectureId: this.lecture()?.id, filterToUnresolved: false, filterToOwn: false, filterToAnsweredOrReacted: false, diff --git a/src/main/webapp/app/overview/discussion-section/discussion-section.module.ts b/src/main/webapp/app/overview/discussion-section/discussion-section.module.ts deleted file mode 100644 index 125a60831748..000000000000 --- a/src/main/webapp/app/overview/discussion-section/discussion-section.module.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { NgModule } from '@angular/core'; -import { ArtemisSharedModule } from 'app/shared/shared.module'; -import { ArtemisSidePanelModule } from 'app/shared/side-panel/side-panel.module'; -import { DiscussionSectionComponent } from 'app/overview/discussion-section/discussion-section.component'; -import { RouterModule, Routes } from '@angular/router'; -import { MetisModule } from 'app/shared/metis/metis.module'; -import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; -import { InfiniteScrollModule } from 'ngx-infinite-scroll'; - -const routes: Routes = [ - { - path: '', - pathMatch: 'full', - component: DiscussionSectionComponent, - }, -]; - -@NgModule({ - imports: [RouterModule.forChild(routes), MetisModule, ArtemisSharedModule, ArtemisSidePanelModule, ArtemisSharedComponentModule, InfiniteScrollModule], - declarations: [DiscussionSectionComponent], - exports: [DiscussionSectionComponent], -}) -export class DiscussionSectionModule {} diff --git a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html index 0d8f889d1ed5..238401e7d4e3 100644 --- a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html +++ b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.html @@ -255,7 +255,7 @@

@if (exercise.course && (isCommunicationEnabled(exercise.course) || isMessagingEnabled(exercise.course))) { - + }
diff --git a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts index d0b7264723b6..fc623b457d5d 100644 --- a/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts +++ b/src/main/webapp/app/overview/exercise-details/course-exercise-details.component.ts @@ -23,7 +23,6 @@ import { TeamAssignmentPayload } from 'app/entities/team.model'; import { TeamService } from 'app/exercises/shared/team/team.service'; import { QuizExercise, QuizStatus } from 'app/entities/quiz/quiz-exercise.model'; import { QuizExerciseService } from 'app/exercises/quiz/manage/quiz-exercise.service'; -import { DiscussionSectionComponent } from 'app/overview/discussion-section/discussion-section.component'; import { ExerciseCategory } from 'app/entities/exercise-category.model'; import { getFirstResultWithComplaintFromResults } from 'app/entities/submission.model'; import { ComplaintService } from 'app/complaints/complaint.service'; @@ -86,7 +85,6 @@ export class CourseExerciseDetailsComponent extends AbstractScienceComponent imp isAfterAssessmentDueDate: boolean; allowComplaintsForAutomaticAssessments: boolean; public gradingCriteria: GradingCriterion[]; - private discussionComponent?: DiscussionSectionComponent; baseResource: string; isExamExercise: boolean; submissionPolicy?: SubmissionPolicy; @@ -224,10 +222,6 @@ export class CourseExerciseDetailsComponent extends AbstractScienceComponent imp this.subscribeForNewResults(); this.subscribeToTeamAssignmentUpdates(); - if (this.discussionComponent && this.exercise) { - // We need to manually update the exercise property of the posts component - this.discussionComponent.exercise = this.exercise; - } this.baseResource = `/course-management/${this.courseId}/${this.exercise.type}-exercises/${this.exercise.id}/`; } @@ -416,18 +410,6 @@ export class CourseExerciseDetailsComponent extends AbstractScienceComponent imp return undefined; } - /** - * This function gets called if the router outlet gets activated. This is - * used only for the DiscussionComponent - * @param instance The component instance - */ - onChildActivate(instance: DiscussionSectionComponent) { - this.discussionComponent = instance; // save the reference to the component instance - if (this.exercise) { - instance.exercise = this.exercise; - } - } - private onError(error: string) { this.alertService.error(error); } diff --git a/src/main/webapp/app/overview/exercise-details/course-exercise-details.module.ts b/src/main/webapp/app/overview/exercise-details/course-exercise-details.module.ts index 0b264b8006a0..63f0858ea8b8 100644 --- a/src/main/webapp/app/overview/exercise-details/course-exercise-details.module.ts +++ b/src/main/webapp/app/overview/exercise-details/course-exercise-details.module.ts @@ -30,6 +30,7 @@ import { ProblemStatementComponent } from 'app/overview/exercise-details/problem import { ArtemisFeedbackModule } from 'app/exercises/shared/feedback/feedback.module'; import { ArtemisExerciseInfoModule } from 'app/exercises/shared/exercise-info/exercise-info.module'; import { IrisModule } from 'app/iris/iris.module'; +import { DiscussionSectionComponent } from 'app/overview/discussion-section/discussion-section.component'; const routes: Routes = [ { @@ -41,13 +42,6 @@ const routes: Routes = [ }, pathMatch: 'full', canActivate: [UserRouteAccessService], - children: [ - { - path: '', - pathMatch: 'full', - loadChildren: () => import('../discussion-section/discussion-section.module').then((m) => m.DiscussionSectionModule), - }, - ], }, ]; @@ -76,6 +70,7 @@ const routes: Routes = [ ArtemisFeedbackModule, ArtemisExerciseInfoModule, IrisModule, + DiscussionSectionComponent, ], declarations: [CourseExerciseDetailsComponent, OrionCourseExerciseDetailsComponent, LtiInitializerComponent, LtiInitializerModalComponent, ProblemStatementComponent], exports: [CourseExerciseDetailsComponent, OrionCourseExerciseDetailsComponent, ProblemStatementComponent], diff --git a/src/test/javascript/spec/component/learning-paths/components/learning-path-lecture-unit.component.spec.ts b/src/test/javascript/spec/component/learning-paths/components/learning-path-lecture-unit.component.spec.ts index a77d15c0709f..03905c6938a7 100644 --- a/src/test/javascript/spec/component/learning-paths/components/learning-path-lecture-unit.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/components/learning-path-lecture-unit.component.spec.ts @@ -17,20 +17,33 @@ import { AlertService } from 'app/core/util/alert.service'; import { MockAlertService } from '../../../helpers/mocks/service/mock-alert.service'; import { LectureUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/lectureUnit.service'; import { of } from 'rxjs'; +import { CourseInformationSharingConfiguration } from 'app/entities/course.model'; +import { DiscussionSectionComponent } from 'app/overview/discussion-section/discussion-section.component'; +import { MockComponent } from 'ng-mocks'; +import { LearningPathNavigationService } from 'app/course/learning-paths/services/learning-path-navigation.service'; +import { Lecture } from 'app/entities/lecture.model'; +import { LectureUnitCompletionEvent } from 'app/overview/course-lectures/course-lecture-details.component'; describe('LearningPathLectureUnitComponent', () => { let component: LearningPathLectureUnitComponent; let fixture: ComponentFixture; + let learningPathNavigationService: LearningPathNavigationService; let lectureUnitService: LectureUnitService; let getLectureUnitByIdSpy: jest.SpyInstance; + let setLearningObjectCompletionSpy: jest.SpyInstance; const learningPathId = 1; const lectureUnit: VideoUnit = { id: 1, description: 'Example video unit', name: 'Example video', - lecture: { id: 2 }, + lecture: { + id: 2, + course: { + courseInformationSharingConfiguration: CourseInformationSharingConfiguration.COMMUNICATION_AND_MESSAGING, + }, + }, completed: false, visibleToStudents: true, source: 'https://www.youtube.com/embed/8iU8LPEa4o0', @@ -38,7 +51,7 @@ describe('LearningPathLectureUnitComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [LearningPathLectureUnitComponent], + imports: [LearningPathLectureUnitComponent, MockComponent(DiscussionSectionComponent)], providers: [ provideHttpClient(), provideHttpClientTesting(), @@ -70,6 +83,8 @@ describe('LearningPathLectureUnitComponent', () => { lectureUnitService = TestBed.inject(LectureUnitService); getLectureUnitByIdSpy = jest.spyOn(lectureUnitService, 'getLectureUnitById').mockReturnValue(of(lectureUnit)); lectureUnitService = TestBed.inject(LectureUnitService); + learningPathNavigationService = TestBed.inject(LearningPathNavigationService); + setLearningObjectCompletionSpy = jest.spyOn(learningPathNavigationService, 'setCurrentLearningObjectCompletion').mockReturnValue(); fixture = TestBed.createComponent(LearningPathLectureUnitComponent); component = fixture.componentInstance; @@ -83,6 +98,7 @@ describe('LearningPathLectureUnitComponent', () => { it('should initialize', () => { expect(component).toBeTruthy(); expect(component.lectureUnitId()).toBe(learningPathId); + expect(component.isCommunicationEnabled()).toBeFalse(); }); it('should get lecture unit', async () => { @@ -101,6 +117,38 @@ describe('LearningPathLectureUnitComponent', () => { expect(component.lectureUnit()).toEqual(lectureUnit); }); + it('should not set current learning object on error', async () => { + const completeLectureUnitSpy = jest.spyOn(lectureUnitService, 'completeLectureUnit').mockImplementationOnce(() => {}); + + fixture.detectChanges(); + await fixture.whenStable(); + + component.setLearningObjectCompletion({ completed: true, lectureUnit: lectureUnit }); + + expect(completeLectureUnitSpy).toHaveBeenCalledExactlyOnceWith(lectureUnit.lecture, { + completed: true, + lectureUnit: lectureUnit, + }); + expect(setLearningObjectCompletionSpy).not.toHaveBeenCalled(); + }); + + it('should set current learning object completion', async () => { + const completeLectureUnitSpy = jest + .spyOn(lectureUnitService, 'completeLectureUnit') + .mockImplementationOnce((lecture: Lecture, completionEvent: LectureUnitCompletionEvent) => (completionEvent.lectureUnit.completed = completionEvent.completed)); + + fixture.detectChanges(); + await fixture.whenStable(); + + component.setLearningObjectCompletion({ completed: true, lectureUnit: lectureUnit }); + + expect(completeLectureUnitSpy).toHaveBeenCalledExactlyOnceWith(lectureUnit.lecture, { + completed: true, + lectureUnit: lectureUnit, + }); + expect(setLearningObjectCompletionSpy).toHaveBeenCalledExactlyOnceWith(true); + }); + it('should set loading state correctly', async () => { const setIsLoadingSpy = jest.spyOn(component.isLoading, 'set'); fixture.detectChanges(); @@ -110,4 +158,27 @@ describe('LearningPathLectureUnitComponent', () => { expect(setIsLoadingSpy).toHaveBeenCalledWith(true); expect(setIsLoadingSpy).toHaveBeenCalledWith(false); }); + + it('should show discussion section when communication is enabled', async () => { + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + + const discussionSection = fixture.nativeElement.querySelector('jhi-discussion-section'); + expect(discussionSection).toBeTruthy(); + }); + + it('should not show discussion section when communication is disabled', async () => { + const lecture = { + ...lectureUnit.lecture, + course: { courseInformationSharingConfiguration: CourseInformationSharingConfiguration.DISABLED }, + }; + getLectureUnitByIdSpy.mockReturnValue(of({ ...lectureUnit, lecture })); + + fixture.detectChanges(); + await fixture.whenStable(); + + const discussionSection = fixture.nativeElement.querySelector('jhi-discussion-section'); + expect(discussionSection).toBeFalsy(); + }); }); diff --git a/src/test/javascript/spec/component/learning-paths/participate/learning-path-lecture-unit-view.component.spec.ts b/src/test/javascript/spec/component/learning-paths/participate/learning-path-lecture-unit-view.component.spec.ts index 16fa583ba3c9..ad24cc2dd512 100644 --- a/src/test/javascript/spec/component/learning-paths/participate/learning-path-lecture-unit-view.component.spec.ts +++ b/src/test/javascript/spec/component/learning-paths/participate/learning-path-lecture-unit-view.component.spec.ts @@ -10,7 +10,6 @@ import { LectureUnitService } from 'app/lecture/lecture-unit/lecture-unit-manage import { TextUnit } from 'app/entities/lecture-unit/textUnit.model'; import { OnlineUnit } from 'app/entities/lecture-unit/onlineUnit.model'; import { Course, CourseInformationSharingConfiguration } from 'app/entities/course.model'; -import { DiscussionSectionComponent } from 'app/overview/discussion-section/discussion-section.component'; import { AttachmentUnitComponent } from 'app/overview/course-lectures/attachment-unit/attachment-unit.component'; import { VideoUnitComponent } from 'app/overview/course-lectures/video-unit/video-unit.component'; import { OnlineUnitComponent } from 'app/overview/course-lectures/online-unit/online-unit.component'; @@ -105,17 +104,4 @@ describe('LearningPathLectureUnitViewComponent', () => { expect(setCompletionStub).toHaveBeenCalledOnce(); expect(setCompletionStub).toHaveBeenCalledWith(attachment.id, lecture.id, event.completed); }); - - it('should set properties of child on activate', () => { - const attachment = new AttachmentUnit(); - attachment.id = 3; - lecture.lectureUnits = [attachment]; - comp.lecture = lecture; - comp.lectureUnit = attachment; - lecture.course = new Course(); - fixture.detectChanges(); - const instance = { lecture: undefined } as DiscussionSectionComponent; - comp.onChildActivate(instance); - expect(instance.lecture).toEqual(lecture); - }); }); diff --git a/src/test/javascript/spec/component/overview/course-lectures/course-lecture-details.component.spec.ts b/src/test/javascript/spec/component/overview/course-lectures/course-lecture-details.component.spec.ts index f5eecf4d351c..101871555716 100644 --- a/src/test/javascript/spec/component/overview/course-lectures/course-lecture-details.component.spec.ts +++ b/src/test/javascript/spec/component/overview/course-lectures/course-lecture-details.component.spec.ts @@ -2,14 +2,13 @@ import { DebugElement } from '@angular/core'; import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { ActivatedRoute, Router } from '@angular/router'; -import { RouterTestingModule } from '@angular/router/testing'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { TranslateService } from '@ngx-translate/core'; import { MockComponent, MockDirective, MockPipe, MockProvider } from 'ng-mocks'; import dayjs from 'dayjs/esm'; import { AlertService } from 'app/core/util/alert.service'; import { BehaviorSubject, of } from 'rxjs'; -import { CourseLectureDetailsComponent } from '../../../../../../main/webapp/app/overview/course-lectures/course-lecture-details.component'; +import { CourseLectureDetailsComponent } from 'app/overview/course-lectures/course-lecture-details.component'; import { AttachmentUnitComponent } from 'app/overview/course-lectures/attachment-unit/attachment-unit.component'; import { ExerciseUnitComponent } from 'app/overview/course-lectures/exercise-unit/exercise-unit.component'; import { TextUnitComponent } from 'app/overview/course-lectures/text-unit/text-unit.component'; @@ -21,14 +20,14 @@ import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; import { ArtemisTimeAgoPipe } from 'app/shared/pipes/artemis-time-ago.pipe'; import { SidePanelComponent } from 'app/shared/side-panel/side-panel.component'; import { Lecture } from 'app/entities/lecture.model'; -import { Course } from 'app/entities/course.model'; +import { Course, CourseInformationSharingConfiguration } from 'app/entities/course.model'; import { AttachmentUnit } from 'app/entities/lecture-unit/attachmentUnit.model'; import { Attachment, AttachmentType } from 'app/entities/attachment.model'; import { TextUnit } from 'app/entities/lecture-unit/textUnit.model'; import { FileService } from 'app/shared/http/file.service'; import { LectureService } from 'app/lecture/lecture.service'; import { MockTranslateService } from '../../../helpers/mocks/service/mock-translate.service'; -import { HttpHeaders, HttpResponse } from '@angular/common/http'; +import { HttpHeaders, HttpResponse, provideHttpClient } from '@angular/common/http'; import { HtmlForMarkdownPipe } from 'app/shared/pipes/html-for-markdown.pipe'; import { SubmissionResultStatusComponent } from 'app/overview/submission-result-status.component'; import { ExerciseDetailsStudentActionsComponent } from 'app/overview/exercise-details/exercise-details-student-actions.component'; @@ -39,31 +38,36 @@ import { CourseExerciseRowComponent } from 'app/overview/course-exercises/course import { MockFileService } from '../../../helpers/mocks/service/mock-file.service'; import { TranslateDirective } from 'app/shared/language/translate.directive'; import { LectureUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/lectureUnit.service'; -import { NgbCollapse, NgbPopover, NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; import { ScienceService } from 'app/shared/science/science.service'; import * as DownloadUtils from 'app/shared/util/download.util'; -import { ProfileService } from '../../../../../../main/webapp/app/shared/layouts/profiles/profile.service'; -import { ProfileInfo } from '../../../../../../main/webapp/app/shared/layouts/profiles/profile-info.model'; +import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; +import { ProfileInfo } from 'app/shared/layouts/profiles/profile-info.model'; import { MockProfileService } from '../../../helpers/mocks/service/mock-profile.service'; import { OnlineUnitComponent } from 'app/overview/course-lectures/online-unit/online-unit.component'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { NgbCollapse, NgbPopover, NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; +import { DiscussionSectionComponent } from 'app/overview/discussion-section/discussion-section.component'; describe('CourseLectureDetailsComponent', () => { let fixture: ComponentFixture; let courseLecturesDetailsComponent: CourseLectureDetailsComponent; let lecture: Lecture; + let course: Course; let lectureUnit1: AttachmentUnit; let lectureUnit2: AttachmentUnit; let lectureUnit3: TextUnit; let debugElement: DebugElement; let profileService: ProfileService; + let lectureService: LectureService; let getProfileInfoMock: jest.SpyInstance; - beforeEach(() => { + beforeEach(async () => { const releaseDate = dayjs('18-03-2020', 'DD-MM-YYYY'); - const course = new Course(); + course = new Course(); course.id = 456; + course.courseInformationSharingConfiguration = CourseInformationSharingConfiguration.COMMUNICATION_AND_MESSAGING; lecture = new Lecture(); lecture.id = 1; @@ -88,8 +92,8 @@ describe('CourseLectureDetailsComponent', () => { headers = headers.set('Content-Type', 'application/json; charset=utf-8'); const response = of(new HttpResponse({ body: lecture, headers, status: 200 })); - TestBed.configureTestingModule({ - imports: [RouterTestingModule, MockDirective(NgbTooltip), MockDirective(NgbCollapse), MockDirective(NgbPopover)], + await TestBed.configureTestingModule({ + imports: [MockDirective(NgbTooltip), MockDirective(NgbCollapse), MockDirective(NgbPopover)], declarations: [ CourseLectureDetailsComponent, MockComponent(AttachmentUnitComponent), @@ -112,8 +116,11 @@ describe('CourseLectureDetailsComponent', () => { MockComponent(FaIconComponent), MockDirective(TranslateDirective), MockComponent(SubmissionResultStatusComponent), + MockComponent(DiscussionSectionComponent), ], providers: [ + provideHttpClient(), + provideHttpClientTesting(), MockProvider(LectureService, { find: () => { return response; @@ -136,20 +143,22 @@ describe('CourseLectureDetailsComponent', () => { MockProvider(Router), MockProvider(ScienceService), ], - }) - .compileComponents() - .then(() => { - fixture = TestBed.createComponent(CourseLectureDetailsComponent); - courseLecturesDetailsComponent = fixture.componentInstance; - debugElement = fixture.debugElement; - - // mock profileService - profileService = fixture.debugElement.injector.get(ProfileService); - getProfileInfoMock = jest.spyOn(profileService, 'getProfileInfo'); - const profileInfo = { inProduction: false } as ProfileInfo; - const profileInfoSubject = new BehaviorSubject(profileInfo); - getProfileInfoMock.mockReturnValue(profileInfoSubject); - }); + }).compileComponents(); + + lectureService = TestBed.inject(LectureService); + jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(response); + jest.spyOn(lectureService, 'find').mockReturnValue(response); + + fixture = TestBed.createComponent(CourseLectureDetailsComponent); + courseLecturesDetailsComponent = fixture.componentInstance; + debugElement = fixture.debugElement; + + // mock profileService + profileService = fixture.debugElement.injector.get(ProfileService); + getProfileInfoMock = jest.spyOn(profileService, 'getProfileInfo'); + const profileInfo = { inProduction: false } as ProfileInfo; + const profileInfoSubject = new BehaviorSubject(profileInfo); + getProfileInfoMock.mockReturnValue(profileInfoSubject); }); afterEach(() => { @@ -247,6 +256,27 @@ describe('CourseLectureDetailsComponent', () => { expect(courseLecturesDetailsComponent.attachmentExtension(attachment)).toBe('N/A'); })); + it('should show discussion section when communication is enabled', fakeAsync(() => { + fixture.detectChanges(); + + const discussionSection = fixture.nativeElement.querySelector('jhi-discussion-section'); + expect(discussionSection).toBeTruthy(); + })); + + it('should not show discussion section when communication is disabled', fakeAsync(() => { + const lecture = { + ...lectureUnit3.lecture, + course: { courseInformationSharingConfiguration: CourseInformationSharingConfiguration.DISABLED }, + }; + const response = of(new HttpResponse({ body: { ...lecture }, status: 200 })); + jest.spyOn(TestBed.inject(LectureService), 'findWithDetails').mockReturnValue(response); + + fixture.detectChanges(); + + const discussionSection = fixture.nativeElement.querySelector('jhi-discussion-section'); + expect(discussionSection).toBeFalsy(); + })); + it('should download file for attachment', fakeAsync(() => { const fileService = TestBed.inject(FileService); const downloadFileSpy = jest.spyOn(fileService, 'downloadFile'); diff --git a/src/test/javascript/spec/component/overview/discussion-section/discussion-section.component.spec.ts b/src/test/javascript/spec/component/overview/discussion-section/discussion-section.component.spec.ts index 55a145f49666..f1f6ec890ecf 100644 --- a/src/test/javascript/spec/component/overview/discussion-section/discussion-section.component.spec.ts +++ b/src/test/javascript/spec/component/overview/discussion-section/discussion-section.component.spec.ts @@ -1,8 +1,7 @@ import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { of } from 'rxjs'; import { HttpResponse, provideHttpClient } from '@angular/common/http'; -import { MockComponent, MockModule, MockPipe, MockProvider } from 'ng-mocks'; -import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; +import { MockComponent, MockModule, MockProvider } from 'ng-mocks'; import { MetisService } from 'app/shared/metis/metis.service'; import { ExerciseService } from 'app/exercises/shared/exercise/exercise.service'; import { MockExerciseService } from '../../../helpers/mocks/service/mock-exercise.service'; @@ -13,8 +12,6 @@ import { MockPostService } from '../../../helpers/mocks/service/mock-post.servic import { AccountService } from 'app/core/auth/account.service'; import { MockAccountService } from '../../../helpers/mocks/service/mock-account.service'; import { DiscussionSectionComponent } from 'app/overview/discussion-section/discussion-section.component'; -import { PostingThreadComponent } from 'app/shared/metis/posting-thread/posting-thread.component'; -import { PostCreateEditModalComponent } from 'app/shared/metis/posting-create-edit-modal/post-create-edit-modal/post-create-edit-modal.component'; import { MockTranslateService } from '../../../helpers/mocks/service/mock-translate.service'; import { TranslateService } from '@ngx-translate/core'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; @@ -25,7 +22,6 @@ import { LocalStorageService, SessionStorageService } from 'ngx-webstorage'; import { MockLocalStorageService } from '../../../helpers/mocks/service/mock-local-storage.service'; import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { getElement, getElements } from '../../../helpers/utils/general.utils'; -import { ButtonComponent } from 'app/shared/components/button.component'; import { messagesBetweenUser1User2, metisCourse, @@ -47,6 +43,7 @@ import { MetisConversationService } from 'app/shared/metis/metis-conversation.se import { MockMetisConversationService } from '../../../helpers/mocks/service/mock-metis-conversation.service'; import { NotificationService } from 'app/shared/notification/notification.service'; import { MockNotificationService } from '../../../helpers/mocks/service/mock-notification.service'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; @Directive({ // eslint-disable-next-line @angular-eslint/directive-selector @@ -66,11 +63,12 @@ describe('DiscussionSectionComponent', () => { let getChannelOfLectureSpy: jest.SpyInstance; let getChannelOfExerciseSpy: jest.SpyInstance; - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [MockModule(FormsModule), MockModule(ReactiveFormsModule), MockModule(NgbTooltipModule)], + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MockModule(FormsModule), MockModule(ReactiveFormsModule), MockModule(NgbTooltipModule), DiscussionSectionComponent], providers: [ provideHttpClient(), + provideHttpClientTesting(), FormBuilder, MockProvider(SessionStorageService), MockProvider(ChannelService), @@ -89,48 +87,36 @@ describe('DiscussionSectionComponent', () => { useValue: new MockActivatedRoute({ postId: metisPostTechSupport.id, courseId: metisCourse.id }), }, ], - declarations: [ - DiscussionSectionComponent, - InfiniteScrollStubDirective, - MockComponent(PostingThreadComponent), - MockComponent(PostCreateEditModalComponent), - MockComponent(FaIconComponent), - MockComponent(ButtonComponent), - MockPipe(ArtemisTranslatePipe), - ], + declarations: [InfiniteScrollStubDirective, MockComponent(FaIconComponent)], }) .overrideComponent(DiscussionSectionComponent, { set: { providers: [{ provide: MetisService, useClass: MetisService }], }, }) - .compileComponents() - .then(() => { - fixture = TestBed.createComponent(DiscussionSectionComponent); - component = fixture.componentInstance; - metisService = fixture.debugElement.injector.get(MetisService); - channelService = TestBed.inject(ChannelService); - getChannelOfLectureSpy = jest.spyOn(channelService, 'getChannelOfLecture').mockReturnValue( - of( - new HttpResponse({ - body: metisLectureChannelDTO, - status: 200, - }), - ), - ); - getChannelOfExerciseSpy = jest.spyOn(channelService, 'getChannelOfExercise').mockReturnValue( - of( - new HttpResponse({ - body: metisExerciseChannelDTO, - status: 200, - }), - ), - ); - metisServiceGetFilteredPostsSpy = jest.spyOn(metisService, 'getFilteredPosts'); - component.lecture = { ...metisLecture, course: metisCourse }; - component.ngOnInit(); - fixture.detectChanges(); - }); + .compileComponents(); + + fixture = TestBed.createComponent(DiscussionSectionComponent); + component = fixture.componentInstance; + metisService = fixture.debugElement.injector.get(MetisService); + channelService = TestBed.inject(ChannelService); + getChannelOfLectureSpy = jest.spyOn(channelService, 'getChannelOfLecture').mockReturnValue( + of( + new HttpResponse({ + body: metisLectureChannelDTO, + status: 200, + }), + ), + ); + getChannelOfExerciseSpy = jest.spyOn(channelService, 'getChannelOfExercise').mockReturnValue( + of( + new HttpResponse({ + body: metisExerciseChannelDTO, + status: 200, + }), + ), + ); + metisServiceGetFilteredPostsSpy = jest.spyOn(metisService, 'getFilteredPosts'); }); afterEach(() => { @@ -138,8 +124,8 @@ describe('DiscussionSectionComponent', () => { }); it('should set course and messages for lecture with lecture channel on initialization', fakeAsync(() => { - component.lecture = { ...metisLecture, course: metisCourse }; - component.ngOnInit(); + fixture.componentRef.setInput('lecture', { ...metisLecture, course: metisCourse }); + fixture.detectChanges(); tick(); expect(component.course).toEqual(metisCourse); expect(component.createdPost).toBeDefined(); @@ -149,9 +135,8 @@ describe('DiscussionSectionComponent', () => { })); it('should set course and messages for exercise with exercise channel on initialization', fakeAsync(() => { - component.lecture = undefined; - component.exercise = { ...metisExercise, course: metisCourse }; - component.ngOnInit(); + fixture.componentRef.setInput('exercise', { ...metisExercise, course: metisCourse }); + fixture.detectChanges(); tick(); expect(component.course).toEqual(metisCourse); expect(component.createdPost).toBeDefined(); @@ -161,6 +146,8 @@ describe('DiscussionSectionComponent', () => { })); it('should reset current post', fakeAsync(() => { + fixture.componentRef.setInput('lecture', { ...metisLecture, course: metisCourse }); + fixture.detectChanges(); component.resetCurrentPost(); tick(); expect(component.currentPost).toBeUndefined(); @@ -168,9 +155,8 @@ describe('DiscussionSectionComponent', () => { })); it('should initialize correctly for exercise posts with default settings', fakeAsync(() => { - component.lecture = undefined; - component.exercise = { ...metisExercise, course: metisCourse }; - component.ngOnInit(); + fixture.componentRef.setInput('exercise', { ...metisExercise, course: metisCourse }); + fixture.detectChanges(); tick(); expect(component.formGroup.get('filterToUnresolved')?.value).toBeFalse(); expect(component.formGroup.get('filterToOwn')?.value).toBeFalse(); @@ -182,32 +168,32 @@ describe('DiscussionSectionComponent', () => { })); it('should display one new message button for more then 3 messages in channel', fakeAsync(() => { - component.exercise = { ...metisExercise, course: metisCourse }; - component.ngOnInit(); + fixture.componentRef.setInput('exercise', { ...metisExercise, course: metisCourse }); + fixture.detectChanges(); tick(); fixture.detectChanges(); tick(); component.posts = metisExercisePosts; fixture.detectChanges(); tick(); - const newPostButtons = getElements(fixture.debugElement, '.btn-primary'); + const newPostButtons = getElements(fixture.debugElement, '#new-post'); expect(newPostButtons).not.toBeNull(); expect(newPostButtons).toHaveLength(1); })); it('should display one new message button', fakeAsync(() => { - component.exercise = { ...metisExercise, course: metisCourse }; - component.ngOnInit(); + fixture.componentRef.setInput('exercise', { ...metisExercise, course: metisCourse }); + fixture.detectChanges(); tick(); fixture.detectChanges(); - const newPostButtons = getElements(fixture.debugElement, '.btn-primary'); + const newPostButtons = getElements(fixture.debugElement, '#new-post'); expect(newPostButtons).not.toBeNull(); expect(newPostButtons).toHaveLength(1); })); it('should show search-bar and filters if not focused to a post', fakeAsync(() => { - component.exercise = { ...metisExercise, course: metisCourse }; - component.ngOnInit(); + fixture.componentRef.setInput('exercise', { ...metisExercise, course: metisCourse }); + fixture.detectChanges(); tick(); fixture.detectChanges(); const searchInput = getElement(fixture.debugElement, 'input[name=searchText]'); @@ -222,8 +208,7 @@ describe('DiscussionSectionComponent', () => { })); it('should hide search-bar and filters if focused to a post', fakeAsync(() => { - component.lecture = undefined; - component.ngOnInit(); + fixture.detectChanges(); tick(); fixture.detectChanges(); const searchInput = getElement(fixture.debugElement, 'input[name=searchText]'); @@ -238,9 +223,9 @@ describe('DiscussionSectionComponent', () => { })); it('triggering filters should invoke the metis service', fakeAsync(() => { - component.exercise = { ...metisExercise, course: metisCourse }; + fixture.componentRef.setInput('exercise', { ...metisExercise, course: metisCourse }); metisServiceGetFilteredPostsSpy.mockReset(); - component.ngOnInit(); + fixture.detectChanges(); tick(); fixture.detectChanges(); component.formGroup.patchValue({ @@ -268,8 +253,8 @@ describe('DiscussionSectionComponent', () => { it('loads exercise messages if communication only', fakeAsync(() => { component.course = { id: 1, courseInformationSharingConfiguration: CourseInformationSharingConfiguration.COMMUNICATION_ONLY } as Course; - component.exercise = { id: 2 } as Exercise; - component.lecture = undefined; + fixture.componentRef.setInput('exercise', { id: 2 } as Exercise); + fixture.detectChanges(); component.setChannel(1); @@ -283,7 +268,8 @@ describe('DiscussionSectionComponent', () => { it('loads lecture messages if communication only', fakeAsync(() => { component.course = { id: 1, courseInformationSharingConfiguration: CourseInformationSharingConfiguration.COMMUNICATION_ONLY } as Course; - component.lecture = { id: 2 } as Lecture; + fixture.componentRef.setInput('lecture', { id: 2 } as Lecture); + fixture.detectChanges(); component.setChannel(1); @@ -297,7 +283,8 @@ describe('DiscussionSectionComponent', () => { it('collapses sidebar if no channel exists', fakeAsync(() => { component.course = { id: 1, courseInformationSharingConfiguration: CourseInformationSharingConfiguration.COMMUNICATION_ONLY } as Course; - component.lecture = { id: 2 } as Lecture; + fixture.componentRef.setInput('lecture', { id: 2 } as Lecture); + fixture.detectChanges(); getChannelOfLectureSpy = jest.spyOn(channelService, 'getChannelOfLecture').mockReturnValue( of( new HttpResponse({ @@ -315,6 +302,9 @@ describe('DiscussionSectionComponent', () => { })); it('should react to srcoll up event', fakeAsync(() => { + component.course = { id: 1, courseInformationSharingConfiguration: CourseInformationSharingConfiguration.COMMUNICATION_ONLY } as Course; + fixture.componentRef.setInput('lecture', { id: 2 } as Lecture); + fixture.detectChanges(); const fetchNextPageSpy = jest.spyOn(component, 'fetchNextPage'); const scrolledUp = new CustomEvent('scrolledUp'); @@ -332,6 +322,7 @@ describe('DiscussionSectionComponent', () => { }); it('should change sort direction', () => { + fixture.detectChanges(); component.currentSortDirection = SortDirection.ASCENDING; component.onChangeSortDir(); expect(component.currentSortDirection).toBe(SortDirection.DESCENDING); @@ -340,6 +331,9 @@ describe('DiscussionSectionComponent', () => { }); it('fetches new messages on scroll up if more messages are available', fakeAsync(() => { + component.course = { id: 1, courseInformationSharingConfiguration: CourseInformationSharingConfiguration.COMMUNICATION_ONLY } as Course; + fixture.componentRef.setInput('lecture', { id: 2 } as Lecture); + fixture.detectChanges(); component.posts = []; const commandMetisToFetchPostsSpy = jest.spyOn(component, 'fetchNextPage'); diff --git a/src/test/javascript/spec/component/overview/exercise-details/course-exercise-details.component.spec.ts b/src/test/javascript/spec/component/overview/exercise-details/course-exercise-details.component.spec.ts index 5fc8684a61cd..efb66d508837 100644 --- a/src/test/javascript/spec/component/overview/exercise-details/course-exercise-details.component.spec.ts +++ b/src/test/javascript/spec/component/overview/exercise-details/course-exercise-details.component.spec.ts @@ -45,7 +45,7 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { ExtensionPointDirective } from 'app/shared/extension-point/extension-point.directive'; import { ArtemisDatePipe } from 'app/shared/pipes/artemis-date.pipe'; import { ComplaintsStudentViewComponent } from 'app/complaints/complaints-for-students/complaints-student-view.component'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; import { MockRouterLinkDirective } from '../../../helpers/mocks/directive/mock-router-link.directive'; import { LtiInitializerComponent } from 'app/overview/exercise-details/lti-initializer.component'; import { ModelingEditorComponent } from 'app/exercises/modeling/shared/modeling-editor.component'; @@ -71,6 +71,8 @@ import { MockScienceService } from '../../../helpers/mocks/service/mock-science- import { ScienceEventType } from 'app/shared/science/science.model'; import { PROFILE_IRIS } from 'app/app.constants'; import { ExerciseHintService } from 'app/exercises/shared/exercise-hint/shared/exercise-hint.service'; +import { CourseInformationSharingConfiguration } from 'app/entities/course.model'; +import { provideHttpClient } from '@angular/common/http'; describe('CourseExerciseDetailsComponent', () => { let comp: CourseExerciseDetailsComponent; @@ -92,7 +94,15 @@ describe('CourseExerciseDetailsComponent', () => { let scienceService: ScienceService; let logEventStub: jest.SpyInstance; - const exercise = { id: 42, type: ExerciseType.TEXT, studentParticipations: [], course: {} } as unknown as Exercise; + const exercise = { + id: 42, + type: ExerciseType.TEXT, + studentParticipations: [], + course: { + id: 1, + courseInformationSharingConfiguration: CourseInformationSharingConfiguration.COMMUNICATION_AND_MESSAGING, + }, + } as unknown as Exercise; const textExercise = { id: 24, @@ -119,11 +129,15 @@ describe('CourseExerciseDetailsComponent', () => { const parentParams = { courseId: 1 }; const parentRoute = { parent: { parent: { params: of(parentParams) } } } as any as ActivatedRoute; - const route = { params: of({ exerciseId: exercise.id }), parent: parentRoute, queryParams: of({ welcome: '' }) } as any as ActivatedRoute; + const route = { + params: of({ exerciseId: exercise.id }), + parent: parentRoute, + queryParams: of({ welcome: '' }), + } as any as ActivatedRoute; beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], + imports: [MockComponent(DiscussionSectionComponent)], declarations: [ CourseExerciseDetailsComponent, MockPipe(ArtemisTranslatePipe), @@ -152,6 +166,8 @@ describe('CourseExerciseDetailsComponent', () => { MockComponent(ExerciseInfoComponent), ], providers: [ + provideHttpClient(), + provideHttpClientTesting(), { provide: ActivatedRoute, useValue: route }, { provide: Router, useClass: MockRouter }, { provide: ProfileService, useClass: MockProfileService }, @@ -195,7 +211,11 @@ describe('CourseExerciseDetailsComponent', () => { // mock teamService, needed for team assignment teamService = fixture.debugElement.injector.get(TeamService); - const teamAssignmentPayload = { exerciseId: 2, teamId: 2, studentParticipations: [] } as TeamAssignmentPayload; + const teamAssignmentPayload = { + exerciseId: 2, + teamId: 2, + studentParticipations: [], + } as TeamAssignmentPayload; jest.spyOn(teamService, 'teamAssignmentUpdates', 'get').mockReturnValue(Promise.resolve(of(teamAssignmentPayload))); // mock participationService, needed for team assignment @@ -313,14 +333,6 @@ describe('CourseExerciseDetailsComponent', () => { expect(comp.exampleSolutionCollapsed).toBeFalse(); }); - it('should store a reference to child component', () => { - comp.exercise = exercise; - - const childComponent = {} as DiscussionSectionComponent; - comp.onChildActivate(childComponent); - expect(childComponent.exercise).toEqual(exercise); - }); - it('should activate hint', () => { comp.availableExerciseHints = [{ id: 1 }, { id: 2 }]; comp.activatedExerciseHints = []; @@ -331,10 +343,47 @@ describe('CourseExerciseDetailsComponent', () => { expect(comp.activatedExerciseHints).toContain(activatedHint); }); - it('should handle new programming exercise', () => { - const childComponent = {} as DiscussionSectionComponent; - comp.onChildActivate(childComponent); + it('should sort results by completion date in ascending order', () => { + const result1 = { completionDate: dayjs().subtract(2, 'days') } as Result; + const result2 = { completionDate: dayjs().subtract(1, 'day') } as Result; + const result3 = { completionDate: dayjs() } as Result; + + const results = [result3, result1, result2]; + results.sort((a, b) => comp['resultSortFunction'](a, b)); + + expect(results).toEqual([result1, result2, result3]); + }); + + it('should handle results with undefined completion dates', () => { + const result1 = { completionDate: dayjs().subtract(2, 'days') } as Result; + const result2 = { completionDate: undefined } as Result; + const result3 = { completionDate: dayjs() } as Result; + + const results = [result3, result1, result2]; + results.sort((a, b) => comp['resultSortFunction'](a, b)); + + expect(results).toEqual([result1, result3, result2]); + }); + + it('should handle empty results array', () => { + const results: Result[] = []; + results.sort((a, b) => comp['resultSortFunction'](a, b)); + + expect(results).toEqual([]); + }); + + it('should handle results with same completion dates', () => { + const date = dayjs(); + const result1 = { completionDate: date } as Result; + const result2 = { completionDate: date } as Result; + + const results = [result2, result1]; + results.sort((a, b) => comp['resultSortFunction'](a, b)); + + expect(results).toEqual([result2, result1]); + }); + it('should handle new programming exercise', () => { const courseId = programmingExercise.course!.id!; comp.courseId = courseId; @@ -343,7 +392,6 @@ describe('CourseExerciseDetailsComponent', () => { expect(comp.baseResource).toBe(`/course-management/${courseId}/${programmingExercise.type}-exercises/${programmingExercise.id}/`); expect(comp.allowComplaintsForAutomaticAssessments).toBeTrue(); expect(comp.submissionPolicy).toEqual(submissionPolicy); - expect(childComponent.exercise).toEqual(programmingExercise); }); it('should handle error when getting latest rated result', fakeAsync(() => { @@ -432,4 +480,25 @@ describe('CourseExerciseDetailsComponent', () => { fixture.detectChanges(); expect(logEventStub).toHaveBeenCalledExactlyOnceWith(ScienceEventType.EXERCISE__OPEN, exercise.id); }); + + it('should not show discussion section when communication is disabled', fakeAsync(() => { + const newExercise = { + ...exercise, + course: { id: 1, courseInformationSharingConfiguration: CourseInformationSharingConfiguration.DISABLED }, + }; + getExerciseDetailsMock.mockReturnValue(of({ body: newExercise })); + + comp.handleNewExercise({ exercise }); + + const discussionSection = fixture.nativeElement.querySelector('jhi-discussion-section'); + expect(discussionSection).toBeFalsy(); + })); + + it('should show discussion section when communication is enabled', fakeAsync(() => { + fixture.detectChanges(); + tick(500); + + const discussionSection = fixture.nativeElement.querySelector('jhi-discussion-section'); + expect(discussionSection).toBeTruthy(); + })); });