From 76c02434655f67b07e6a1fe8a84ff50b63e1b5b9 Mon Sep 17 00:00:00 2001 From: "Felix T.J. Dietrich" Date: Mon, 26 Aug 2024 18:05:35 +0200 Subject: [PATCH 01/24] Update theme components to use signals --- src/main/webapp/app/app.module.ts | 4 +- .../core/theme/theme-switch.component.html | 10 +- .../app/core/theme/theme-switch.component.ts | 79 ++++++----- .../webapp/app/core/theme/theme.module.ts | 12 -- .../webapp/app/core/theme/theme.service.ts | 125 +++++++----------- .../example-modeling-submission.component.ts | 31 ++--- ...gramming-exercise-instruction.component.ts | 3 +- .../programming-exercise-plant-uml.service.ts | 8 +- .../progress-bar/progress-bar.component.ts | 13 +- .../ace-editor/ace-editor.component.ts | 6 +- .../metis/emoji/emoji-picker.component.ts | 3 +- .../app/shared/metis/emoji/emoji.component.ts | 3 +- .../monaco-diff-editor.component.ts | 3 +- .../monaco-editor/monaco-editor.component.ts | 4 +- 14 files changed, 136 insertions(+), 168 deletions(-) delete mode 100644 src/main/webapp/app/core/theme/theme.module.ts diff --git a/src/main/webapp/app/app.module.ts b/src/main/webapp/app/app.module.ts index 0a415aa50d80..c8b74314e07a 100644 --- a/src/main/webapp/app/app.module.ts +++ b/src/main/webapp/app/app.module.ts @@ -22,7 +22,7 @@ import { OrionOutdatedComponent } from 'app/shared/orion/outdated-plugin-warning import { LoadingNotificationComponent } from 'app/shared/notification/loading-notification/loading-notification.component'; import { NotificationPopupComponent } from 'app/shared/notification/notification-popup/notification-popup.component'; import { UserSettingsModule } from 'app/shared/user-settings/user-settings.module'; -import { ThemeModule } from 'app/core/theme/theme.module'; +import { ThemeSwitchComponent } from 'app/core/theme/theme-switch.component'; import { ArtemisSharedComponentModule } from 'app/shared/components/shared-component.module'; import { FaIconLibrary } from '@fortawesome/angular-fontawesome'; import { artemisIconPack } from 'src/main/webapp/content/icons/icons'; @@ -44,7 +44,7 @@ import { ScrollingModule } from '@angular/cdk/scrolling'; ArtemisComplaintsModule, ArtemisHeaderExercisePageWithDetailsModule, UserSettingsModule, - ThemeModule, + ThemeSwitchComponent, ArtemisSharedComponentModule, ScrollingModule, ], diff --git a/src/main/webapp/app/core/theme/theme-switch.component.html b/src/main/webapp/app/core/theme/theme-switch.component.html index 24772ad09ea6..d7dd855f628b 100644 --- a/src/main/webapp/app/core/theme/theme-switch.component.html +++ b/src/main/webapp/app/core/theme/theme-switch.component.html @@ -2,9 +2,9 @@
-
{{ 'artemisApp.theme.sync' | artemisTranslate }}
+
- +
@@ -17,10 +17,10 @@ [triggers]="''" #popover="ngbPopover" [autoClose]="false" - [animation]="animate" - [placement]="popoverPlacement" + [animation]="animate()" + [placement]="popoverPlacement()" > -
+
diff --git a/src/main/webapp/app/core/theme/theme-switch.component.ts b/src/main/webapp/app/core/theme/theme-switch.component.ts index 3685a808d656..d94419aa6fea 100644 --- a/src/main/webapp/app/core/theme/theme-switch.component.ts +++ b/src/main/webapp/app/core/theme/theme-switch.component.ts @@ -1,13 +1,12 @@ -import { ChangeDetectionStrategy, Component, OnDestroy, OnInit, computed, inject, input, signal, viewChild } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnInit, computed, inject, input, viewChild } from '@angular/core'; import { CommonModule } from '@angular/common'; import { TranslateModule } from '@ngx-translate/core'; import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'; import { PlacementArray } from '@ng-bootstrap/ng-bootstrap/util/positioning'; import { Theme, ThemeService } from 'app/core/theme/theme.service'; -import { Subscription, delay, filter, fromEvent, tap, timer } from 'rxjs'; +import { fromEvent } from 'rxjs'; import { faSync } from '@fortawesome/free-solid-svg-icons'; import { ArtemisSharedModule } from 'app/shared/shared.module'; -import { toObservable } from '@angular/core/rxjs-interop'; /** * Displays a sun or a moon in the navbar, depending on the current theme. @@ -22,7 +21,7 @@ import { toObservable } from '@angular/core/rxjs-interop'; changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, }) -export class ThemeSwitchComponent implements OnInit, OnDestroy { +export class ThemeSwitchComponent implements OnInit { protected readonly faSync = faSync; private readonly themeService = inject(ThemeService); @@ -33,18 +32,7 @@ export class ThemeSwitchComponent implements OnInit, OnDestroy { isDarkTheme = computed(() => this.themeService.currentTheme() === Theme.DARK); isSyncedWithOS = computed(() => this.themeService.userPreference() === undefined); - animate = signal(true); - openPopupAfterNextChange = signal(false); - - private closeTimerSubscription: Subscription | undefined; - private reopenPopupSubscription = toObservable(this.themeService.currentTheme) - .pipe( - tap(() => this.animate.set(true)), - filter(() => this.openPopupAfterNextChange()), - tap(() => this.openPopupAfterNextChange.set(false)), - delay(250), - ) - .subscribe(() => this.openPopover()); + closeTimeout: any; ngOnInit() { // Workaround as we can't dynamically change the "autoClose" property on popovers @@ -56,33 +44,27 @@ export class ThemeSwitchComponent implements OnInit, OnDestroy { }); } - ngOnDestroy() { - this.reopenPopupSubscription.unsubscribe(); - this.closeTimerSubscription?.unsubscribe(); - } - openPopover() { this.popover().open(); - this.closeTimerSubscription?.unsubscribe(); + clearTimeout(this.closeTimeout); } closePopover() { + clearTimeout(this.closeTimeout); this.popover().close(); - this.closeTimerSubscription?.unsubscribe(); } mouseLeave() { - this.closeTimerSubscription?.unsubscribe(); - this.closeTimerSubscription = timer(250).subscribe(() => this.closePopover()); + clearTimeout(this.closeTimeout); + this.closeTimeout = setTimeout(() => this.closePopover(), 250); } /** * Changes the theme to the currently not active theme. */ toggleTheme() { - this.animate.set(false); - this.openPopupAfterNextChange.set(true); this.themeService.applyThemePreference(this.isDarkTheme() ? Theme.LIGHT : Theme.DARK); + setTimeout(() => this.openPopover(), 250); } /** diff --git a/src/test/javascript/spec/component/theme/theme-switch.component.spec.ts b/src/test/javascript/spec/component/theme/theme-switch.component.spec.ts index 1600163033d6..3f407244334b 100644 --- a/src/test/javascript/spec/component/theme/theme-switch.component.spec.ts +++ b/src/test/javascript/spec/component/theme/theme-switch.component.spec.ts @@ -31,35 +31,33 @@ describe('ThemeSwitchComponent', () => { afterEach(() => jest.restoreAllMocks()); - it('theme toggles correctly', fakeAsync(async () => { - const applyThemeSpy = jest.spyOn(themeService, 'applyThemePreference'); + it('theme toggles correctly', fakeAsync(() => { + const applyThemePreferenceSpy = jest.spyOn(themeService, 'applyThemePreference'); component.ngOnInit(); component.toggleTheme(); + TestBed.flushEffects(); - expect(applyThemeSpy).toHaveBeenCalledWith(Theme.DARK); - expect(component.animate()).toBeFalse(); - expect(component.openPopupAfterNextChange()).toBeTrue(); + expect(applyThemePreferenceSpy).toHaveBeenCalledWith(Theme.DARK); - tick(); - await fixture.whenStable(); - - expectSwitchToDark(); + expect(component.isDarkTheme()).toBeTrue(); + tick(250); + expect(component.popover().open).toHaveBeenCalledOnce(); flush(); })); it('os sync toggles correctly', fakeAsync(() => { - const applyThemeSpy = jest.spyOn(themeService, 'applyThemePreference'); + const applyThemePreferenceSpy = jest.spyOn(themeService, 'applyThemePreference'); component.ngOnInit(); component.toggleSynced(); - expect(applyThemeSpy).toHaveBeenCalledWith(Theme.LIGHT); + expect(applyThemePreferenceSpy).toHaveBeenCalledWith(Theme.LIGHT); expect(component.isSyncedWithOS()).toBeFalse(); component.toggleSynced(); - expect(applyThemeSpy).toHaveBeenCalledWith(undefined); + expect(applyThemePreferenceSpy).toHaveBeenCalledWith(undefined); expect(component.isSyncedWithOS()).toBeTrue(); })); @@ -78,14 +76,4 @@ describe('ThemeSwitchComponent', () => { tick(250); expect(component.popover().close).toHaveBeenCalledOnce(); })); - - function expectSwitchToDark() { - expect(component.isDarkTheme()).toBeTrue(); - expect(component.animate()).toBeTrue(); - expect(component.openPopupAfterNextChange()).toBeFalse(); - - tick(250); - - expect(component.popover().open).toHaveBeenCalledOnce(); - } }); diff --git a/src/test/javascript/spec/helpers/mocks/service/mock-theme.service.ts b/src/test/javascript/spec/helpers/mocks/service/mock-theme.service.ts index f65eb1cdb0da..a5ff955560b7 100644 --- a/src/test/javascript/spec/helpers/mocks/service/mock-theme.service.ts +++ b/src/test/javascript/spec/helpers/mocks/service/mock-theme.service.ts @@ -5,12 +5,12 @@ export class MockThemeService { private _currentTheme = signal(Theme.LIGHT); public readonly currentTheme = this._currentTheme.asReadonly(); - private _preference = signal(undefined); - public readonly preference = this._preference.asReadonly(); + private _userPreference = signal(undefined); + public readonly userPreference = this._userPreference.asReadonly(); - public applyTheme(theme: Theme | undefined) { - this._preference.set(theme); - this._currentTheme.set(theme ?? Theme.LIGHT); + public applyThemePreference(preference: Theme | undefined) { + this._userPreference.set(preference); + this._currentTheme.set(preference ?? Theme.LIGHT); } public print() {} From 78010e0ecf7137dbe81205350ba23f30a7dca8a7 Mon Sep 17 00:00:00 2001 From: "Felix T.J. Dietrich" Date: Fri, 13 Sep 2024 09:57:46 +0200 Subject: [PATCH 12/24] fix more tests and simplify emoji stuff --- ...gramming-exercise-instruction.component.ts | 11 ++--- .../metis/emoji/emoji-picker.component.html | 4 +- .../metis/emoji/emoji-picker.component.ts | 25 +++-------- .../shared/metis/emoji/emoji.component.html | 5 +-- .../app/shared/metis/emoji/emoji.component.ts | 22 +++------- .../emoji/emoji-picker.component.spec.ts | 17 +++----- .../component/emoji/emoji.component.spec.ts | 42 ------------------- ...ing-exercise-instruction.component.spec.ts | 2 + .../monaco-diff-editor.component.spec.ts | 8 ++-- .../monaco-editor.component.spec.ts | 13 ++---- .../shared/progress-bar.component.spec.ts | 2 +- 11 files changed, 36 insertions(+), 115 deletions(-) delete mode 100644 src/test/javascript/spec/component/emoji/emoji.component.spec.ts diff --git a/src/main/webapp/app/exercises/programming/shared/instructions-render/programming-exercise-instruction.component.ts b/src/main/webapp/app/exercises/programming/shared/instructions-render/programming-exercise-instruction.component.ts index 6af198189b69..f65b7e7908aa 100644 --- a/src/main/webapp/app/exercises/programming/shared/instructions-render/programming-exercise-instruction.component.ts +++ b/src/main/webapp/app/exercises/programming/shared/instructions-render/programming-exercise-instruction.component.ts @@ -11,6 +11,7 @@ import { ViewChild, ViewContainerRef, createComponent, + inject, } from '@angular/core'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { ThemeService } from 'app/core/theme/theme.service'; @@ -47,6 +48,8 @@ import { toObservable } from '@angular/core/rxjs-interop'; styleUrls: ['./programming-exercise-instruction.scss'], }) export class ProgrammingExerciseInstructionComponent implements OnChanges, OnDestroy { + private themeService = inject(ThemeService); + @Input() public exercise: ProgrammingExercise; @Input() public participation: Participation; @Input() generateHtmlEvents: Observable; @@ -85,8 +88,10 @@ export class ProgrammingExerciseInstructionComponent implements OnChanges, OnDes private injectableContentFoundSubscription: Subscription; private tasksSubscription: Subscription; private generateHtmlSubscription: Subscription; - private themeChangeSubscription: Subscription; private testCases?: ProgrammingExerciseTestCase[]; + private themeChangeSubscription = toObservable(this.themeService.currentTheme).subscribe(() => { + this.updateMarkdown(); + }); // Icons faSpinner = faSpinner; @@ -99,16 +104,12 @@ export class ProgrammingExerciseInstructionComponent implements OnChanges, OnDes private programmingExercisePlantUmlWrapper: ProgrammingExercisePlantUmlExtensionWrapper, private programmingExerciseParticipationService: ProgrammingExerciseParticipationService, private programmingExerciseGradingService: ProgrammingExerciseGradingService, - themeService: ThemeService, private sanitizer: DomSanitizer, private programmingExerciseInstructionService: ProgrammingExerciseInstructionService, private appRef: ApplicationRef, private injector: EnvironmentInjector, ) { this.programmingExerciseTaskWrapper.viewContainerRef = this.viewContainerRef; - this.themeChangeSubscription = toObservable(themeService.currentTheme).subscribe(() => { - this.updateMarkdown(); - }); } /** diff --git a/src/main/webapp/app/shared/metis/emoji/emoji-picker.component.html b/src/main/webapp/app/shared/metis/emoji/emoji-picker.component.html index 8566cb951bff..08cceca47792 100644 --- a/src/main/webapp/app/shared/metis/emoji/emoji-picker.component.html +++ b/src/main/webapp/app/shared/metis/emoji/emoji-picker.component.html @@ -7,8 +7,8 @@ [color]="'var(--primary)'" [recent]="recent" [i18n]="{ search: 'artemisApp.metis.searchEmoji' | artemisTranslate, categories: { recent: 'artemisApp.metis.courseEmojiSelectionCategory' | artemisTranslate } }" - [darkMode]="dark" - [imageUrlFn]="singleImageFunction" + [darkMode]="dark()" + [imageUrlFn]="singleImageFunction()" [backgroundImageFn]="utils.EMOJI_SHEET_URL" (emojiSelect)="onEmojiSelect($event)" /> diff --git a/src/main/webapp/app/shared/metis/emoji/emoji-picker.component.ts b/src/main/webapp/app/shared/metis/emoji/emoji-picker.component.ts index 3f180a6bbdc8..e37e83152d1a 100644 --- a/src/main/webapp/app/shared/metis/emoji/emoji-picker.component.ts +++ b/src/main/webapp/app/shared/metis/emoji/emoji-picker.component.ts @@ -1,36 +1,23 @@ -import { Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core'; +import { Component, EventEmitter, Input, Output, computed, inject } from '@angular/core'; import { Theme, ThemeService } from 'app/core/theme/theme.service'; -import { Subscription } from 'rxjs'; import { EmojiUtils } from 'app/shared/metis/emoji/emoji.utils'; import { EmojiData } from '@ctrl/ngx-emoji-mart/ngx-emoji'; -import { toObservable } from '@angular/core/rxjs-interop'; @Component({ selector: 'jhi-emoji-picker', templateUrl: './emoji-picker.component.html', }) -export class EmojiPickerComponent implements OnDestroy { +export class EmojiPickerComponent { + private themeService = inject(ThemeService); + @Input() emojisToShowFilter: (emoji: string | EmojiData) => boolean; @Input() categoriesIcons: { [key: string]: string }; @Input() recent: string[]; @Output() emojiSelect: EventEmitter = new EventEmitter(); utils = EmojiUtils; - singleImageFunction: (emoji: EmojiData | null) => string; - - dark = false; - themeSubscription: Subscription; - - constructor(private themeService: ThemeService) { - this.themeSubscription = toObservable(themeService.currentTheme).subscribe((theme) => { - this.dark = theme === Theme.DARK; - this.singleImageFunction = this.dark ? EmojiUtils.singleDarkModeEmojiUrlFn : () => ''; - }); - } - - ngOnDestroy(): void { - this.themeSubscription.unsubscribe(); - } + dark = computed(() => this.themeService.currentTheme() === Theme.DARK); + singleImageFunction = computed(() => (this.dark() ? EmojiUtils.singleDarkModeEmojiUrlFn : () => '')); onEmojiSelect(event: any) { this.emojiSelect.emit(event); diff --git a/src/main/webapp/app/shared/metis/emoji/emoji.component.html b/src/main/webapp/app/shared/metis/emoji/emoji.component.html index 537b14712a91..ac71d2e4826a 100644 --- a/src/main/webapp/app/shared/metis/emoji/emoji.component.html +++ b/src/main/webapp/app/shared/metis/emoji/emoji.component.html @@ -1,7 +1,6 @@ -@if (!dark) { +@if (!dark()) { -} -@if (dark) { +} @else { } diff --git a/src/main/webapp/app/shared/metis/emoji/emoji.component.ts b/src/main/webapp/app/shared/metis/emoji/emoji.component.ts index 6b5d3749a41f..00e651bf19b7 100644 --- a/src/main/webapp/app/shared/metis/emoji/emoji.component.ts +++ b/src/main/webapp/app/shared/metis/emoji/emoji.component.ts @@ -1,29 +1,17 @@ -import { Component, Input, OnDestroy } from '@angular/core'; +import { Component, Input, computed, inject } from '@angular/core'; import { Theme, ThemeService } from 'app/core/theme/theme.service'; -import { Subscription } from 'rxjs'; import { EmojiUtils } from 'app/shared/metis/emoji/emoji.utils'; -import { toObservable } from '@angular/core/rxjs-interop'; @Component({ selector: 'jhi-emoji', templateUrl: './emoji.component.html', styleUrls: ['./emoji.component.scss'], }) -export class EmojiComponent implements OnDestroy { - utils = EmojiUtils; +export class EmojiComponent { + private themeService = inject(ThemeService); + utils = EmojiUtils; @Input() emoji: string; - dark = false; - themeSubscription: Subscription; - - constructor(private themeService: ThemeService) { - this.themeSubscription = toObservable(themeService.currentTheme).subscribe((theme) => { - this.dark = theme === Theme.DARK; - }); - } - - ngOnDestroy(): void { - this.themeSubscription.unsubscribe(); - } + dark = computed(() => this.themeService.currentTheme() === Theme.DARK); } diff --git a/src/test/javascript/spec/component/emoji/emoji-picker.component.spec.ts b/src/test/javascript/spec/component/emoji/emoji-picker.component.spec.ts index 6f3d2096d1d5..4e4d76f5a019 100644 --- a/src/test/javascript/spec/component/emoji/emoji-picker.component.spec.ts +++ b/src/test/javascript/spec/component/emoji/emoji-picker.component.spec.ts @@ -11,7 +11,6 @@ describe('EmojiPickerComponent', () => { let fixture: ComponentFixture; let comp: EmojiPickerComponent; let mockThemeService: ThemeService; - let themeSpy: jest.SpyInstance; beforeEach(() => { TestBed.configureTestingModule({ @@ -21,7 +20,6 @@ describe('EmojiPickerComponent', () => { .compileComponents() .then(() => { mockThemeService = TestBed.inject(ThemeService); - themeSpy = jest.spyOn(mockThemeService, 'currentTheme'); fixture = TestBed.createComponent(EmojiPickerComponent); comp = fixture.componentInstance; }); @@ -31,19 +29,14 @@ describe('EmojiPickerComponent', () => { jest.restoreAllMocks(); }); - it('should subscribe and unsubscribe to the theme service and react to changes', () => { - expect(themeSpy).toHaveBeenCalledOnce(); - expect(comp.dark).toBeFalse(); - expect(comp.themeSubscription).toBeDefined(); + it('should react to theme changes', () => { + expect(comp.dark()).toBeFalse(); + expect(comp.singleImageFunction()({ unified: '1F519' } as EmojiData)).toBe(''); - expect(comp.singleImageFunction({ unified: '1F519' } as EmojiData)).toBe(''); mockThemeService.applyThemePreference(Theme.DARK); - expect(comp.singleImageFunction({ unified: '1F519' } as EmojiData)).toBe('public/emoji/1f519.png'); - const subSpy = jest.spyOn(comp.themeSubscription, 'unsubscribe'); - - comp.ngOnDestroy(); - expect(subSpy).toHaveBeenCalledOnce(); + expect(comp.dark()).toBeTrue(); + expect(comp.singleImageFunction()({ unified: '1F519' } as EmojiData)).toBe('public/emoji/1f519.png'); }); it('should emit an event on emoji select', () => { diff --git a/src/test/javascript/spec/component/emoji/emoji.component.spec.ts b/src/test/javascript/spec/component/emoji/emoji.component.spec.ts deleted file mode 100644 index 887a27a7686d..000000000000 --- a/src/test/javascript/spec/component/emoji/emoji.component.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ArtemisTestModule } from '../../test.module'; -import { EmojiComponent } from 'app/shared/metis/emoji/emoji.component'; -import { Theme, ThemeService } from 'app/core/theme/theme.service'; -import { of } from 'rxjs'; - -describe('EmojiComponent', () => { - let fixture: ComponentFixture; - let comp: EmojiComponent; - let mockThemeService: ThemeService; - let themeSpy: jest.SpyInstance; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [ArtemisTestModule], - declarations: [], - providers: [], - }) - .compileComponents() - .then(() => { - mockThemeService = TestBed.inject(ThemeService); - themeSpy = jest.spyOn(mockThemeService, 'getCurrentThemeObservable').mockReturnValue(of(Theme.DARK)); - fixture = TestBed.createComponent(EmojiComponent); - comp = fixture.componentInstance; - }); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - it('should subscribe and unsubscribe to the theme service and set dark flag', () => { - expect(themeSpy).toHaveBeenCalledOnce(); - expect(comp.dark).toBeTrue(); - expect(comp.themeSubscription).toBeDefined(); - - const subSpy = jest.spyOn(comp.themeSubscription, 'unsubscribe'); - - comp.ngOnDestroy(); - expect(subSpy).toHaveBeenCalledOnce(); - }); -}); diff --git a/src/test/javascript/spec/component/programming-exercise/programming-exercise-instruction.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/programming-exercise-instruction.component.spec.ts index 4fe7aebe7ead..8040940c9bc3 100644 --- a/src/test/javascript/spec/component/programming-exercise/programming-exercise-instruction.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/programming-exercise-instruction.component.spec.ts @@ -448,6 +448,8 @@ describe('ProgrammingExerciseInstructionComponent', () => { it('should update the markdown on a theme change', () => { const updateMarkdownStub = jest.spyOn(comp, 'updateMarkdown'); themeService.applyThemePreference(Theme.DARK); + TestBed.flushEffects(); + expect(updateMarkdownStub).toHaveBeenCalledOnce(); }); diff --git a/src/test/javascript/spec/component/shared/monaco-editor/monaco-diff-editor.component.spec.ts b/src/test/javascript/spec/component/shared/monaco-editor/monaco-diff-editor.component.spec.ts index 350a6ed235d3..a7fd5547430e 100644 --- a/src/test/javascript/spec/component/shared/monaco-editor/monaco-diff-editor.component.spec.ts +++ b/src/test/javascript/spec/component/shared/monaco-editor/monaco-diff-editor.component.spec.ts @@ -4,7 +4,6 @@ import { ArtemisTestModule } from '../../../test.module'; import { MonacoEditorModule } from 'app/shared/monaco-editor/monaco-editor.module'; import { MockResizeObserver } from '../../../helpers/mocks/service/mock-resize-observer'; import { MonacoDiffEditorComponent } from 'app/shared/monaco-editor/monaco-diff-editor.component'; -import { BehaviorSubject } from 'rxjs'; describe('MonacoDiffEditorComponent', () => { let fixture: ComponentFixture; @@ -33,12 +32,11 @@ describe('MonacoDiffEditorComponent', () => { }); it('should adjust its theme to the global theme', () => { - const themeSubject = new BehaviorSubject(Theme.LIGHT); - const subscribeStub = jest.spyOn(mockThemeService, 'getCurrentThemeObservable').mockReturnValue(themeSubject.asObservable()); const changeThemeSpy = jest.spyOn(comp, 'changeTheme'); fixture.detectChanges(); - themeSubject.next(Theme.DARK); - expect(subscribeStub).toHaveBeenCalledOnce(); + mockThemeService.applyThemePreference(Theme.DARK); + TestBed.flushEffects(); + expect(changeThemeSpy).toHaveBeenCalledTimes(2); expect(changeThemeSpy).toHaveBeenNthCalledWith(1, Theme.LIGHT); expect(changeThemeSpy).toHaveBeenNthCalledWith(2, Theme.DARK); diff --git a/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor.component.spec.ts b/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor.component.spec.ts index ae34f743a3a7..19b6a02d4b63 100644 --- a/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor.component.spec.ts +++ b/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor.component.spec.ts @@ -4,7 +4,6 @@ import { MonacoEditorModule } from 'app/shared/monaco-editor/monaco-editor.modul import { MonacoEditorComponent } from 'app/shared/monaco-editor/monaco-editor.component'; import { MockResizeObserver } from '../../../helpers/mocks/service/mock-resize-observer'; import { Theme, ThemeService } from 'app/core/theme/theme.service'; -import { BehaviorSubject } from 'rxjs'; import { MonacoEditorBuildAnnotationType } from 'app/shared/monaco-editor/model/monaco-editor-build-annotation.model'; import { MonacoCodeEditorElement } from 'app/shared/monaco-editor/model/monaco-code-editor-element.model'; import { MonacoEditorLineDecorationsHoverButton } from 'app/shared/monaco-editor/model/monaco-editor-line-decorations-hover-button.model'; @@ -79,24 +78,20 @@ describe('MonacoEditorComponent', () => { }); it('should adjust its theme to the global theme', () => { - const themeSubject = new BehaviorSubject(Theme.LIGHT); - const subscribeStub = jest.spyOn(mockThemeService, 'getCurrentThemeObservable').mockReturnValue(themeSubject.asObservable()); const changeThemeSpy = jest.spyOn(comp, 'changeTheme'); fixture.detectChanges(); - themeSubject.next(Theme.DARK); - expect(subscribeStub).toHaveBeenCalledOnce(); + + mockThemeService.applyThemePreference(Theme.DARK); + TestBed.flushEffects(); + expect(changeThemeSpy).toHaveBeenCalledTimes(2); expect(changeThemeSpy).toHaveBeenNthCalledWith(1, Theme.LIGHT); expect(changeThemeSpy).toHaveBeenNthCalledWith(2, Theme.DARK); }); it('should unsubscribe from the global theme when destroyed', () => { - const themeSubject = new BehaviorSubject(Theme.LIGHT); - const subscribeStub = jest.spyOn(mockThemeService, 'getCurrentThemeObservable').mockReturnValue(themeSubject.asObservable()); - fixture.detectChanges(); const unsubscribeStub = jest.spyOn(comp.themeSubscription!, 'unsubscribe').mockImplementation(); comp.ngOnDestroy(); - expect(subscribeStub).toHaveBeenCalledOnce(); expect(unsubscribeStub).toHaveBeenCalledOnce(); }); diff --git a/src/test/javascript/spec/component/shared/progress-bar.component.spec.ts b/src/test/javascript/spec/component/shared/progress-bar.component.spec.ts index ff532f7660f7..26280ca71b70 100644 --- a/src/test/javascript/spec/component/shared/progress-bar.component.spec.ts +++ b/src/test/javascript/spec/component/shared/progress-bar.component.spec.ts @@ -49,7 +49,7 @@ describe('ProgressBarComponent', () => { component.ngOnChanges({ percentage: {} as SimpleChange }); expect(component.foregroundColorClass).toBe('text-dark'); - themeService.applyTheme(Theme.DARK); + themeService.applyThemePreference(Theme.DARK); expect(component.foregroundColorClass).toBe('text-white'); }); From ea144b37b70047d6a9ea4731fa6d60de151c2068 Mon Sep 17 00:00:00 2001 From: Johannes Wiest Date: Wed, 18 Sep 2024 12:31:34 +0200 Subject: [PATCH 13/24] fix theme-switch.component.spec.ts --- .../theme/theme-switch.component.spec.ts | 60 ++++++++++--------- 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/src/test/javascript/spec/component/theme/theme-switch.component.spec.ts b/src/test/javascript/spec/component/theme/theme-switch.component.spec.ts index 3f407244334b..4fccadf8b4e0 100644 --- a/src/test/javascript/spec/component/theme/theme-switch.component.spec.ts +++ b/src/test/javascript/spec/component/theme/theme-switch.component.spec.ts @@ -1,32 +1,43 @@ import { ArtemisTestModule } from '../../test.module'; import { ThemeSwitchComponent } from 'app/core/theme/theme-switch.component'; -import { ComponentFixture, TestBed, fakeAsync, flush, tick } from '@angular/core/testing'; +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { Theme, ThemeService } from 'app/core/theme/theme.service'; import { MockLocalStorageService } from '../../helpers/mocks/service/mock-local-storage.service'; import { LocalStorageService } from 'ngx-webstorage'; import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'; import { MockDirective } from 'ng-mocks'; +import { MockThemeService } from '../../helpers/mocks/service/mock-theme.service'; describe('ThemeSwitchComponent', () => { let component: ThemeSwitchComponent; let fixture: ComponentFixture; let themeService: ThemeService; - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [ArtemisTestModule, MockDirective(NgbPopover)], - declarations: [ThemeSwitchComponent], - providers: [{ provide: LocalStorageService, useClass: MockLocalStorageService }], - }) - .compileComponents() - .then(() => { - fixture = TestBed.createComponent(ThemeSwitchComponent); - themeService = TestBed.inject(ThemeService); - fixture.componentRef.setInput('popoverPlacement', ['bottom']); - component = fixture.componentInstance; - // @ts-ignore - component.popover = { open: jest.fn(), close: jest.fn() }; - }); + let openSpy: jest.SpyInstance; + let closeSpy: jest.SpyInstance; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ArtemisTestModule, ThemeSwitchComponent, MockDirective(NgbPopover)], + declarations: [], + providers: [ + { provide: LocalStorageService, useClass: MockLocalStorageService }, + { + provide: ThemeService, + useClass: MockThemeService, + }, + ], + }).compileComponents(); + + themeService = TestBed.inject(ThemeService); + + fixture = TestBed.createComponent(ThemeSwitchComponent); + component = fixture.componentInstance; + + openSpy = jest.spyOn(component.popover(), 'open'); + closeSpy = jest.spyOn(component.popover(), 'close'); + + fixture.componentRef.setInput('popoverPlacement', ['bottom']); }); afterEach(() => jest.restoreAllMocks()); @@ -34,23 +45,18 @@ describe('ThemeSwitchComponent', () => { it('theme toggles correctly', fakeAsync(() => { const applyThemePreferenceSpy = jest.spyOn(themeService, 'applyThemePreference'); - component.ngOnInit(); component.toggleTheme(); - TestBed.flushEffects(); expect(applyThemePreferenceSpy).toHaveBeenCalledWith(Theme.DARK); expect(component.isDarkTheme()).toBeTrue(); tick(250); - expect(component.popover().open).toHaveBeenCalledOnce(); - - flush(); + expect(openSpy).toHaveBeenCalledOnce(); })); it('os sync toggles correctly', fakeAsync(() => { const applyThemePreferenceSpy = jest.spyOn(themeService, 'applyThemePreference'); - component.ngOnInit(); component.toggleSynced(); expect(applyThemePreferenceSpy).toHaveBeenCalledWith(Theme.LIGHT); @@ -63,17 +69,17 @@ describe('ThemeSwitchComponent', () => { it('opens and closes the popover', fakeAsync(() => { component.openPopover(); - expect(component.popover().open).toHaveBeenCalledOnce(); + expect(openSpy).toHaveBeenCalledOnce(); component.closePopover(); - expect(component.popover().close).toHaveBeenCalledOnce(); + expect(closeSpy).toHaveBeenCalledOnce(); })); it('closes on mouse leave after 200ms', fakeAsync(() => { component.openPopover(); - expect(component.popover().open).toHaveBeenCalledOnce(); + expect(openSpy).toHaveBeenCalledOnce(); component.mouseLeave(); - expect(component.popover().close).not.toHaveBeenCalled(); + expect(closeSpy).not.toHaveBeenCalled(); tick(250); - expect(component.popover().close).toHaveBeenCalledOnce(); + expect(closeSpy).toHaveBeenCalledOnce(); })); }); From 3438c376a191e98e078de2cea89a140e54e6c104 Mon Sep 17 00:00:00 2001 From: Johannes Wiest Date: Mon, 7 Oct 2024 09:26:43 +0200 Subject: [PATCH 14/24] change signal logic slightly & update client tests --- .../webapp/app/core/theme/theme.service.ts | 21 +++++----- ...example-modeling-submission.component.html | 6 +-- .../example-modeling-submission.component.ts | 38 +++++++++---------- .../programming-exercise-plant-uml.service.ts | 21 +++++----- ...ing-exercise-instruction.component.spec.ts | 6 ++- .../component/shared/navbar.component.spec.ts | 8 ++-- .../shared/progress-bar.component.spec.ts | 10 +++++ .../spec/service/theme.service.spec.ts | 8 ++-- 8 files changed, 64 insertions(+), 54 deletions(-) diff --git a/src/main/webapp/app/core/theme/theme.service.ts b/src/main/webapp/app/core/theme/theme.service.ts index 3bcdafbb2c4c..ff40b2797277 100644 --- a/src/main/webapp/app/core/theme/theme.service.ts +++ b/src/main/webapp/app/core/theme/theme.service.ts @@ -1,8 +1,6 @@ -import { Injectable, inject, signal } from '@angular/core'; -import { toObservable, toSignal } from '@angular/core/rxjs-interop'; +import { Injectable, computed, effect, inject, signal, untracked } from '@angular/core'; import { IconDefinition, faMoon, faSun } from '@fortawesome/free-solid-svg-icons'; import { LocalStorageService } from 'ngx-webstorage'; -import { combineLatest, distinctUntilChanged, map, tap } from 'rxjs'; export const THEME_LOCAL_STORAGE_KEY = 'artemisapp.theme.preference'; export const THEME_OVERRIDE_ID = 'artemis-theme-override'; @@ -64,19 +62,20 @@ export class ThemeService { /** * The currently applied theme as Signal. */ - public currentTheme = toSignal( - combineLatest([toObservable(this.userPreference), toObservable(this.systemPreference)]).pipe( - map(([preference, systemPreference]) => preference ?? systemPreference), - distinctUntilChanged(), - tap((theme) => this.applyTheme(theme)), - ), - { initialValue: Theme.LIGHT }, - ); + public currentTheme = computed(() => this.userPreference() ?? this.systemPreference()); private localStorageService = inject(LocalStorageService); private darkSchemeMediaQuery: MediaQueryList; + constructor() { + effect(() => { + // Apply the theme as soon as the currentTheme changes + const currentTheme = this.currentTheme(); + untracked(() => this.applyTheme(currentTheme)); + }); + } + /** * Should be called once on application startup. * Sets up the system preference listener and applies the theme initially diff --git a/src/main/webapp/app/exercises/modeling/manage/example-modeling/example-modeling-submission.component.html b/src/main/webapp/app/exercises/modeling/manage/example-modeling/example-modeling-submission.component.html index 8f414f19509e..1d47cb3b1c33 100644 --- a/src/main/webapp/app/exercises/modeling/manage/example-modeling/example-modeling-submission.component.html +++ b/src/main/webapp/app/exercises/modeling/manage/example-modeling/example-modeling-submission.component.html @@ -129,9 +129,9 @@
- @if (highlightedElements && highlightedElements.size > 0) { + @if (highlightedElements() && highlightedElements().size > 0) {
-
+
@@ -160,7 +160,7 @@
} diff --git a/src/main/webapp/app/exercises/modeling/manage/example-modeling/example-modeling-submission.component.ts b/src/main/webapp/app/exercises/modeling/manage/example-modeling/example-modeling-submission.component.ts index a6e992ec0e37..27d7cd03894d 100644 --- a/src/main/webapp/app/exercises/modeling/manage/example-modeling/example-modeling-submission.component.ts +++ b/src/main/webapp/app/exercises/modeling/manage/example-modeling/example-modeling-submission.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core'; +import { Component, OnInit, ViewChild, computed, effect, inject, signal, untracked } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { AlertService } from 'app/core/util/alert.service'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; @@ -28,7 +28,6 @@ import { forkJoin } from 'rxjs'; import { filterInvalidFeedback } from 'app/exercises/modeling/assess/modeling-assessment.util'; import { Theme, ThemeService } from 'app/core/theme/theme.service'; import { scrollToTopOfPage } from 'app/shared/util/utils'; -import { toObservable } from '@angular/core/rxjs-interop'; @Component({ selector: 'jhi-example-modeling-submission', @@ -41,6 +40,8 @@ export class ExampleModelingSubmissionComponent implements OnInit, FeedbackMarke @ViewChild(ModelingAssessmentComponent, { static: false }) assessmentEditor: ModelingAssessmentComponent; + private readonly themeService = inject(ThemeService); + isNewSubmission: boolean; assessmentMode = false; exerciseId: number; @@ -94,15 +95,14 @@ export class ExampleModelingSubmissionComponent implements OnInit, FeedbackMarke return [...this.referencedFeedback, ...this.unreferencedFeedback]; } - highlightedElements = new Map(); + highlightedElements = signal>(new Map()); referencedExampleFeedback: Feedback[] = []; - highlightColor = 'lightblue'; + highlightColor = computed(() => (this.themeService.userPreference() === Theme.DARK ? 'darkblue' : 'lightblue')); // Icons faSave = faSave; faCircle = faCircle; faInfoCircle = faInfoCircle; - faExclamation = faExclamation; faCodeBranch = faCodeBranch; faChalkboardTeacher = faChalkboardTeacher; @@ -115,21 +115,17 @@ export class ExampleModelingSubmissionComponent implements OnInit, FeedbackMarke private route: ActivatedRoute, private router: Router, private navigationUtilService: ArtemisNavigationUtilService, - private changeDetector: ChangeDetectorRef, - private themeService: ThemeService, ) { - toObservable(this.themeService.userPreference).subscribe((themeOrUndefined) => { - if (themeOrUndefined === Theme.DARK) { - this.highlightColor = 'darkblue'; - } else { - this.highlightColor = 'lightblue'; - } - - const updatedHighlights = new Map(); - this.highlightedElements.forEach((_, key) => { - updatedHighlights.set(key, this.highlightColor); + effect(() => { + // Update highlighted elements as soon as current theme changes + const highlightColor = this.highlightColor(); + untracked(() => { + const updatedHighlights = new Map(); + this.highlightedElements().forEach((_, key) => { + updatedHighlights.set(key, highlightColor); + }); + this.highlightedElements.set(updatedHighlights); }); - this.highlightedElements = updatedHighlights; }); } @@ -479,11 +475,11 @@ export class ExampleModelingSubmissionComponent implements OnInit, FeedbackMarke const missedReferencedExampleFeedbacks = this.referencedExampleFeedback.filter( (feedback) => !this.referencedFeedback.some((referencedFeedback) => referencedFeedback.reference === feedback.reference), ); - this.highlightedElements = new Map(); + const highlightedElements = new Map(); for (const feedback of missedReferencedExampleFeedbacks) { - this.highlightedElements.set(feedback.referenceId!, this.highlightColor); + highlightedElements.set(feedback.referenceId!, this.highlightColor()); } - this.changeDetector.detectChanges(); + this.highlightedElements.set(highlightedElements); } readAndUnderstood() { diff --git a/src/main/webapp/app/exercises/programming/shared/instructions-render/service/programming-exercise-plant-uml.service.ts b/src/main/webapp/app/exercises/programming/shared/instructions-render/service/programming-exercise-plant-uml.service.ts index c6fa278c336b..470c54d934dd 100644 --- a/src/main/webapp/app/exercises/programming/shared/instructions-render/service/programming-exercise-plant-uml.service.ts +++ b/src/main/webapp/app/exercises/programming/shared/instructions-render/service/programming-exercise-plant-uml.service.ts @@ -1,10 +1,9 @@ -import { Injectable } from '@angular/core'; +import { Injectable, effect, inject } from '@angular/core'; import { HttpClient, HttpParameterCodec, HttpParams } from '@angular/common/http'; import { Cacheable } from 'ts-cacheable'; import { Observable, Subject } from 'rxjs'; -import { map, tap } from 'rxjs/operators'; +import { map } from 'rxjs/operators'; import { Theme, ThemeService } from 'app/core/theme/theme.service'; -import { toObservable } from '@angular/core/rxjs-interop'; const themeChangedSubject = new Subject(); @@ -13,18 +12,20 @@ export class ProgrammingExercisePlantUmlService { private resourceUrl = 'api/plantuml'; private encoder: HttpParameterCodec; + private readonly themeService = inject(ThemeService); + private readonly http = inject(HttpClient); + /** * Cacheable configuration */ - constructor( - private http: HttpClient, - private themeService: ThemeService, - ) { + constructor() { this.encoder = new HttpUrlCustomEncoder(); - toObservable(this.themeService.currentTheme) - .pipe(tap(() => themeChangedSubject.next())) - .subscribe(); + effect(() => { + // Apply the theme as soon as the currentTheme changes + this.themeService.currentTheme(); + themeChangedSubject.next(); + }); } /** diff --git a/src/test/javascript/spec/component/programming-exercise/programming-exercise-instruction.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/programming-exercise-instruction.component.spec.ts index 8040940c9bc3..897ed506f126 100644 --- a/src/test/javascript/spec/component/programming-exercise/programming-exercise-instruction.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/programming-exercise-instruction.component.spec.ts @@ -210,6 +210,7 @@ describe('ProgrammingExerciseInstructionComponent', () => { comp.participation = participation; comp.isInitial = false; triggerChanges(comp, { property: 'exercise', currentValue: { ...comp.exercise, problemStatement: newProblemStatement }, firstChange: false }); + fixture.detectChanges(); expect(comp.markdownExtensions).toHaveLength(2); expect(updateMarkdownStub).toHaveBeenCalledOnce(); expect(loadInitialResult).not.toHaveBeenCalled(); @@ -365,6 +366,7 @@ describe('ProgrammingExerciseInstructionComponent', () => { expect(debugElement.query(By.css('.stepwizard'))).not.toBeNull(); expect(debugElement.queryAll(By.css('.btn-circle'))).toHaveLength(2); + fixture.detectChanges(); tick(); fixture.detectChanges(); @@ -426,7 +428,6 @@ describe('ProgrammingExerciseInstructionComponent', () => { comp.updateMarkdown(); - fixture.detectChanges(); tick(); // first test should be green (successful), second red (failed) @@ -448,7 +449,8 @@ describe('ProgrammingExerciseInstructionComponent', () => { it('should update the markdown on a theme change', () => { const updateMarkdownStub = jest.spyOn(comp, 'updateMarkdown'); themeService.applyThemePreference(Theme.DARK); - TestBed.flushEffects(); + + fixture.detectChanges(); expect(updateMarkdownStub).toHaveBeenCalledOnce(); }); diff --git a/src/test/javascript/spec/component/shared/navbar.component.spec.ts b/src/test/javascript/spec/component/shared/navbar.component.spec.ts index b02e161d1f22..8b37f25b6f19 100644 --- a/src/test/javascript/spec/component/shared/navbar.component.spec.ts +++ b/src/test/javascript/spec/component/shared/navbar.component.spec.ts @@ -1,4 +1,4 @@ -import { HttpResponse } from '@angular/common/http'; +import { HttpResponse, provideHttpClient } from '@angular/common/http'; import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { ActivatedRoute, Router, UrlSerializer } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; @@ -37,7 +37,7 @@ import { SystemNotificationComponent } from 'app/shared/notification/system-noti import { NgbTooltipMocksModule } from '../../helpers/mocks/directive/ngbTooltipMocks.module'; import { NgbCollapseMocksModule } from '../../helpers/mocks/directive/ngbCollapseMocks.module'; import { NgbDropdownMocksModule } from '../../helpers/mocks/directive/ngbDropdownMocks.module'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { provideHttpClientTesting } from '@angular/common/http/testing'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; class MockBreadcrumb { @@ -93,7 +93,7 @@ describe('NavbarComponent', () => { beforeEach(() => { return TestBed.configureTestingModule({ - imports: [HttpClientTestingModule, NgbTooltipMocksModule, NgbCollapseMocksModule, NgbDropdownMocksModule], + imports: [NgbTooltipMocksModule, NgbCollapseMocksModule, NgbDropdownMocksModule], declarations: [ NavbarComponent, MockDirective(HasAnyAuthorityDirective), @@ -112,6 +112,8 @@ describe('NavbarComponent', () => { MockComponent(FaIconComponent), ], providers: [ + provideHttpClient(), + provideHttpClientTesting(), MockProvider(UrlSerializer), { provide: AccountService, diff --git a/src/test/javascript/spec/component/shared/progress-bar.component.spec.ts b/src/test/javascript/spec/component/shared/progress-bar.component.spec.ts index 26280ca71b70..e7d1805953df 100644 --- a/src/test/javascript/spec/component/shared/progress-bar.component.spec.ts +++ b/src/test/javascript/spec/component/shared/progress-bar.component.spec.ts @@ -5,6 +5,7 @@ import { ArtemisTestModule } from '../../test.module'; import { SimpleChange } from '@angular/core'; import { MockDirective } from 'ng-mocks'; import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; +import { MockThemeService } from '../../helpers/mocks/service/mock-theme.service'; describe('ProgressBarComponent', () => { let fixture: ComponentFixture; @@ -14,6 +15,12 @@ describe('ProgressBarComponent', () => { TestBed.configureTestingModule({ imports: [ArtemisTestModule, MockDirective(NgbTooltip)], declarations: [ProgressBarComponent], + providers: [ + { + class: ThemeService, + useClass: MockThemeService, + }, + ], }) .compileComponents() .then(() => { @@ -50,6 +57,9 @@ describe('ProgressBarComponent', () => { expect(component.foregroundColorClass).toBe('text-dark'); themeService.applyThemePreference(Theme.DARK); + + fixture.detectChanges(); + expect(component.foregroundColorClass).toBe('text-white'); }); diff --git a/src/test/javascript/spec/service/theme.service.spec.ts b/src/test/javascript/spec/service/theme.service.spec.ts index 20356458e221..3ff8f86499ba 100644 --- a/src/test/javascript/spec/service/theme.service.spec.ts +++ b/src/test/javascript/spec/service/theme.service.spec.ts @@ -10,7 +10,7 @@ describe('ThemeService', () => { let linkElement: HTMLElement; let documentGetElementMock: jest.SpyInstance; let headElement: HTMLElement; - let documentgetElementsByTagNameMock: jest.SpyInstance; + let documentGetElementsByTagNameMock: jest.SpyInstance; let newElement: HTMLLinkElement; let documentCreateElementMock: jest.SpyInstance; let storeSpy: jest.SpyInstance; @@ -36,7 +36,7 @@ describe('ThemeService', () => { getElementsByTagName: jest.fn().mockReturnValue([{}, {}]), insertBefore: jest.fn(), } as any as HTMLElement; - documentgetElementsByTagNameMock = jest.spyOn(document, 'getElementsByTagName').mockReturnValue([headElement] as unknown as HTMLCollectionOf); + documentGetElementsByTagNameMock = jest.spyOn(document, 'getElementsByTagName').mockReturnValue([headElement] as unknown as HTMLCollectionOf); newElement = {} as HTMLLinkElement; documentCreateElementMock = jest.spyOn(document, 'createElement').mockReturnValue(newElement); @@ -70,8 +70,8 @@ describe('ThemeService', () => { expect(documentGetElementMock).toHaveBeenCalledTimes(2); expect(documentGetElementMock).toHaveBeenCalledWith(THEME_OVERRIDE_ID); - expect(documentgetElementsByTagNameMock).toHaveBeenCalledOnce(); - expect(documentgetElementsByTagNameMock).toHaveBeenCalledWith('head'); + expect(documentGetElementsByTagNameMock).toHaveBeenCalledOnce(); + expect(documentGetElementsByTagNameMock).toHaveBeenCalledWith('head'); expect(documentCreateElementMock).toHaveBeenCalledOnce(); expect(documentCreateElementMock).toHaveBeenCalledWith('link'); From 8ce085833ef9b36ac7bef3a9767df898a020a707 Mon Sep 17 00:00:00 2001 From: Johannes Wiest Date: Mon, 7 Oct 2024 13:18:39 +0200 Subject: [PATCH 15/24] fix more client tests --- .../app/core/theme/theme-switch.component.ts | 6 +- .../component/shared/navbar.component.spec.ts | 271 +++++++++++++++--- 2 files changed, 229 insertions(+), 48 deletions(-) diff --git a/src/main/webapp/app/core/theme/theme-switch.component.ts b/src/main/webapp/app/core/theme/theme-switch.component.ts index d94419aa6fea..06d60d3fc433 100644 --- a/src/main/webapp/app/core/theme/theme-switch.component.ts +++ b/src/main/webapp/app/core/theme/theme-switch.component.ts @@ -1,12 +1,10 @@ import { ChangeDetectionStrategy, Component, OnInit, computed, inject, input, viewChild } from '@angular/core'; -import { CommonModule } from '@angular/common'; import { TranslateModule } from '@ngx-translate/core'; -import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'; +import { NgbModule, NgbPopover } from '@ng-bootstrap/ng-bootstrap'; import { PlacementArray } from '@ng-bootstrap/ng-bootstrap/util/positioning'; import { Theme, ThemeService } from 'app/core/theme/theme.service'; import { fromEvent } from 'rxjs'; import { faSync } from '@fortawesome/free-solid-svg-icons'; -import { ArtemisSharedModule } from 'app/shared/shared.module'; /** * Displays a sun or a moon in the navbar, depending on the current theme. @@ -17,7 +15,7 @@ import { ArtemisSharedModule } from 'app/shared/shared.module'; selector: 'jhi-theme-switch', templateUrl: './theme-switch.component.html', styleUrls: ['theme-switch.component.scss'], - imports: [TranslateModule, CommonModule, ArtemisSharedModule], + imports: [TranslateModule, NgbModule], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, }) diff --git a/src/test/javascript/spec/component/shared/navbar.component.spec.ts b/src/test/javascript/spec/component/shared/navbar.component.spec.ts index 8b37f25b6f19..1afb333a8c12 100644 --- a/src/test/javascript/spec/component/shared/navbar.component.spec.ts +++ b/src/test/javascript/spec/component/shared/navbar.component.spec.ts @@ -26,7 +26,6 @@ import { JhiConnectionWarningComponent } from 'app/shared/connection-warning/con import { AccountService } from 'app/core/auth/account.service'; import { MockAccountService } from '../../helpers/mocks/service/mock-account.service'; import { EntityTitleService, EntityType } from 'app/shared/layouts/navbar/entity-title.service'; -import { ThemeSwitchComponent } from 'app/core/theme/theme-switch.component'; import { Authority } from 'app/shared/constants/authority.constants'; import { User } from 'app/core/user/user.model'; import { ExamParticipationService } from 'app/exam/participate/exam-participation.service'; @@ -39,6 +38,8 @@ import { NgbCollapseMocksModule } from '../../helpers/mocks/directive/ngbCollaps import { NgbDropdownMocksModule } from '../../helpers/mocks/directive/ngbDropdownMocks.module'; import { provideHttpClientTesting } from '@angular/common/http/testing'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { GuidedTourService } from 'app/guided-tour/guided-tour.service'; +import { ThemeSwitchComponent } from 'app/core/theme/theme-switch.component'; class MockBreadcrumb { label: string; @@ -107,9 +108,9 @@ describe('NavbarComponent', () => { MockComponent(GuidedTourComponent), MockComponent(LoadingNotificationComponent), MockComponent(JhiConnectionWarningComponent), - MockComponent(ThemeSwitchComponent), MockComponent(SystemNotificationComponent), MockComponent(FaIconComponent), + ThemeSwitchComponent, ], providers: [ provideHttpClient(), @@ -123,6 +124,12 @@ describe('NavbarComponent', () => { { provide: SessionStorageService, useClass: MockSyncStorage }, { provide: TranslateService, useClass: MockTranslateService }, { provide: Router, useValue: router }, + { + provide: GuidedTourService, + useValue: { + getGuidedTourAvailabilityStream: () => of(false), + }, + }, { provide: ActivatedRoute, useValue: new MockActivatedRoute({ id: 123 }), @@ -216,10 +223,22 @@ describe('NavbarComponent', () => { expect(component.breadcrumbs).toHaveLength(3); - const systemBreadcrumb = { label: 'artemisApp.systemNotification.systemNotifications', translate: true, uri: '/admin/system-notification-management/' } as MockBreadcrumb; + const systemBreadcrumb = { + label: 'artemisApp.systemNotification.systemNotifications', + translate: true, + uri: '/admin/system-notification-management/', + } as MockBreadcrumb; expect(component.breadcrumbs[0]).toEqual(systemBreadcrumb); - expect(component.breadcrumbs[1]).toEqual({ label: '1', translate: false, uri: '/admin/system-notification-management/1/' } as MockBreadcrumb); - expect(component.breadcrumbs[2]).toEqual({ label: 'global.generic.edit', translate: true, uri: '/admin/system-notification-management/1/edit/' } as MockBreadcrumb); + expect(component.breadcrumbs[1]).toEqual({ + label: '1', + translate: false, + uri: '/admin/system-notification-management/1/', + } as MockBreadcrumb); + expect(component.breadcrumbs[2]).toEqual({ + label: 'global.generic.edit', + translate: true, + uri: '/admin/system-notification-management/1/edit/', + } as MockBreadcrumb); }); it('should build breadcrumbs for user management', () => { @@ -230,8 +249,16 @@ describe('NavbarComponent', () => { expect(component.breadcrumbs).toHaveLength(2); - expect(component.breadcrumbs[0]).toEqual({ label: 'artemisApp.userManagement.home.title', translate: true, uri: '/admin/user-management/' } as MockBreadcrumb); - expect(component.breadcrumbs[1]).toEqual({ label: 'test_user', translate: false, uri: '/admin/user-management/test_user/' } as MockBreadcrumb); + expect(component.breadcrumbs[0]).toEqual({ + label: 'artemisApp.userManagement.home.title', + translate: true, + uri: '/admin/user-management/', + } as MockBreadcrumb); + expect(component.breadcrumbs[1]).toEqual({ + label: 'test_user', + translate: false, + uri: '/admin/user-management/test_user/', + } as MockBreadcrumb); }); it('should build breadcrumbs for organization management', () => { @@ -244,8 +271,16 @@ describe('NavbarComponent', () => { expect(entityTitleServiceStub).toHaveBeenCalledWith(EntityType.ORGANIZATION, [1]); expect(component.breadcrumbs).toHaveLength(2); - expect(component.breadcrumbs[0]).toEqual({ label: 'artemisApp.organizationManagement.title', translate: true, uri: '/admin/organization-management/' } as MockBreadcrumb); - expect(component.breadcrumbs[1]).toEqual({ label: 'Test Organization', translate: false, uri: '/admin/organization-management/1/' } as MockBreadcrumb); + expect(component.breadcrumbs[0]).toEqual({ + label: 'artemisApp.organizationManagement.title', + translate: true, + uri: '/admin/organization-management/', + } as MockBreadcrumb); + expect(component.breadcrumbs[1]).toEqual({ + label: 'Test Organization', + translate: false, + uri: '/admin/organization-management/1/', + } as MockBreadcrumb); }); it('should not error without translation', () => { @@ -256,7 +291,11 @@ describe('NavbarComponent', () => { expect(component.breadcrumbs).toHaveLength(1); - expect(component.breadcrumbs[0]).toEqual({ label: 'route-without-translation', translate: false, uri: '/admin/route-without-translation/' } as MockBreadcrumb); + expect(component.breadcrumbs[0]).toEqual({ + label: 'route-without-translation', + translate: false, + uri: '/admin/route-without-translation/', + } as MockBreadcrumb); }); it('should hide breadcrumb when exam is started', () => { @@ -358,7 +397,11 @@ describe('NavbarComponent', () => { expect(component.breadcrumbs[0]).toEqual(courseManagementCrumb); expect(component.breadcrumbs[1]).toEqual(testCourseCrumb); expect(component.breadcrumbs[2]).toEqual(programmingExercisesCrumb); - expect(component.breadcrumbs[3]).toEqual({ label: 'Test Exercise', translate: false, uri: '/course-management/1/programming-exercises/2/' } as MockBreadcrumb); + expect(component.breadcrumbs[3]).toEqual({ + label: 'Test Exercise', + translate: false, + uri: '/course-management/1/programming-exercises/2/', + } as MockBreadcrumb); expect(component.breadcrumbs[4]).toEqual(gradingCrumb); }); @@ -383,7 +426,11 @@ describe('NavbarComponent', () => { expect(component.breadcrumbs[0]).toEqual(courseManagementCrumb); expect(component.breadcrumbs[1]).toEqual(testCourseCrumb); expect(component.breadcrumbs[2]).toEqual(programmingExercisesCrumb); - expect(component.breadcrumbs[3]).toEqual({ label: 'Test Exercise', translate: false, uri: '/course-management/1/programming-exercises/2/' } as MockBreadcrumb); + expect(component.breadcrumbs[3]).toEqual({ + label: 'Test Exercise', + translate: false, + uri: '/course-management/1/programming-exercises/2/', + } as MockBreadcrumb); expect(component.breadcrumbs[4]).toEqual(assessmentCrumb); }); @@ -391,9 +438,14 @@ describe('NavbarComponent', () => { const testUrl = '/course-management/1/exercises/2/exercise-hints/3'; router.setUrl(testUrl); - const findStub = jest - .spyOn(exerciseService, 'find') - .mockReturnValue(of({ body: { title: 'Test Exercise', type: ExerciseType.PROGRAMMING } } as HttpResponse)); + const findStub = jest.spyOn(exerciseService, 'find').mockReturnValue( + of({ + body: { + title: 'Test Exercise', + type: ExerciseType.PROGRAMMING, + }, + } as HttpResponse), + ); fixture.detectChanges(); @@ -419,8 +471,16 @@ describe('NavbarComponent', () => { expect(component.breadcrumbs[0]).toEqual(courseManagementCrumb); expect(component.breadcrumbs[1]).toEqual(testCourseCrumb); - expect(component.breadcrumbs[2]).toEqual({ label: 'artemisApp.course.exercises', translate: true, uri: '/course-management/1/exercises/' } as MockBreadcrumb); - expect(component.breadcrumbs[3]).toEqual({ label: 'Test Exercise', translate: false, uri: '/course-management/1/programming-exercises/2/' } as MockBreadcrumb); + expect(component.breadcrumbs[2]).toEqual({ + label: 'artemisApp.course.exercises', + translate: true, + uri: '/course-management/1/exercises/', + } as MockBreadcrumb); + expect(component.breadcrumbs[3]).toEqual({ + label: 'Test Exercise', + translate: false, + uri: '/course-management/1/programming-exercises/2/', + } as MockBreadcrumb); expect(component.breadcrumbs[4]).toEqual(hintsCrumb); expect(component.breadcrumbs[5]).toEqual(hintCrumb); }); @@ -484,7 +544,11 @@ describe('NavbarComponent', () => { translate: true, uri: '/course-management/1/modeling-exercises/', } as MockBreadcrumb); - expect(component.breadcrumbs[3]).toEqual({ label: 'Test Exercise', translate: false, uri: '/course-management/1/modeling-exercises/2/' } as MockBreadcrumb); + expect(component.breadcrumbs[3]).toEqual({ + label: 'Test Exercise', + translate: false, + uri: '/course-management/1/modeling-exercises/2/', + } as MockBreadcrumb); expect(component.breadcrumbs[4]).toEqual(submissionCrumb); expect(component.breadcrumbs[5]).toEqual(editorSubmissionCrumb); }); @@ -520,7 +584,11 @@ describe('NavbarComponent', () => { translate: true, uri: '/course-management/1/modeling-exercises/', } as MockBreadcrumb); - expect(component.breadcrumbs[3]).toEqual({ label: 'Test Exercise', translate: false, uri: '/course-management/1/modeling-exercises/2/' } as MockBreadcrumb); + expect(component.breadcrumbs[3]).toEqual({ + label: 'Test Exercise', + translate: false, + uri: '/course-management/1/modeling-exercises/2/', + } as MockBreadcrumb); expect(component.breadcrumbs[4]).toEqual(submissionCrumb); expect(component.breadcrumbs[5]).toEqual(editorSubmissionCrumb); }); @@ -551,8 +619,16 @@ describe('NavbarComponent', () => { expect(component.breadcrumbs[0]).toEqual(courseManagementCrumb); expect(component.breadcrumbs[1]).toEqual(testCourseCrumb); - expect(component.breadcrumbs[2]).toEqual({ label: 'artemisApp.lecture.home.title', translate: true, uri: '/course-management/1/lectures/' } as MockBreadcrumb); - expect(component.breadcrumbs[3]).toEqual({ label: 'Test Lecture', translate: false, uri: '/course-management/1/lectures/2/' } as MockBreadcrumb); + expect(component.breadcrumbs[2]).toEqual({ + label: 'artemisApp.lecture.home.title', + translate: true, + uri: '/course-management/1/lectures/', + } as MockBreadcrumb); + expect(component.breadcrumbs[3]).toEqual({ + label: 'Test Lecture', + translate: false, + uri: '/course-management/1/lectures/2/', + } as MockBreadcrumb); expect(component.breadcrumbs[4]).toEqual(unitManagementCrumb); expect(component.breadcrumbs[5]).toEqual(createCrumb); }); @@ -576,7 +652,11 @@ describe('NavbarComponent', () => { translate: true, uri: '/course-management/1/apollon-diagrams/', } as MockBreadcrumb); - expect(component.breadcrumbs[3]).toEqual({ label: 'Test Diagram', translate: false, uri: '/course-management/1/apollon-diagrams/2/' } as MockBreadcrumb); + expect(component.breadcrumbs[3]).toEqual({ + label: 'Test Diagram', + translate: false, + uri: '/course-management/1/apollon-diagrams/2/', + } as MockBreadcrumb); }); it('exam exercise groups', () => { @@ -604,8 +684,16 @@ describe('NavbarComponent', () => { expect(component.breadcrumbs[0]).toEqual(courseManagementCrumb); expect(component.breadcrumbs[1]).toEqual(testCourseCrumb); - expect(component.breadcrumbs[2]).toEqual({ label: 'artemisApp.examManagement.title', translate: true, uri: '/course-management/1/exams/' } as MockBreadcrumb); - expect(component.breadcrumbs[3]).toEqual({ label: 'Test Exam', translate: false, uri: '/course-management/1/exams/2/' } as MockBreadcrumb); + expect(component.breadcrumbs[2]).toEqual({ + label: 'artemisApp.examManagement.title', + translate: true, + uri: '/course-management/1/exams/', + } as MockBreadcrumb); + expect(component.breadcrumbs[3]).toEqual({ + label: 'Test Exam', + translate: false, + uri: '/course-management/1/exams/2/', + } as MockBreadcrumb); expect(component.breadcrumbs[4]).toEqual(exerciseGroupsCrumb); expect(component.breadcrumbs[5]).toEqual(createCrumb); }); @@ -641,8 +729,16 @@ describe('NavbarComponent', () => { expect(component.breadcrumbs[0]).toEqual(courseManagementCrumb); expect(component.breadcrumbs[1]).toEqual(testCourseCrumb); - expect(component.breadcrumbs[2]).toEqual({ label: 'artemisApp.examManagement.title', translate: true, uri: '/course-management/1/exams/' } as MockBreadcrumb); - expect(component.breadcrumbs[3]).toEqual({ label: 'Test Exam', translate: false, uri: '/course-management/1/exams/2/' } as MockBreadcrumb); + expect(component.breadcrumbs[2]).toEqual({ + label: 'artemisApp.examManagement.title', + translate: true, + uri: '/course-management/1/exams/', + } as MockBreadcrumb); + expect(component.breadcrumbs[3]).toEqual({ + label: 'Test Exam', + translate: false, + uri: '/course-management/1/exams/2/', + } as MockBreadcrumb); expect(component.breadcrumbs[4]).toEqual(exerciseGroupsCrumb); expect(component.breadcrumbs[5]).toEqual(exerciseCrumb); expect(component.breadcrumbs[6]).toEqual(plagiarismCrumb); @@ -663,28 +759,111 @@ describe('NavbarComponent', () => { expect(component.breadcrumbs).toHaveLength(4); expect(component.breadcrumbs[0]).toMatchObject({ uri: '/courses/', label: 'artemisApp.course.home.title' }); expect(component.breadcrumbs[1]).toMatchObject({ uri: '/courses/1/', label: 'Test Course' }); - expect(component.breadcrumbs[2]).toMatchObject({ uri: '/courses/1/exercises/', label: 'artemisApp.courseOverview.menu.exercises' }); + expect(component.breadcrumbs[2]).toMatchObject({ + uri: '/courses/1/exercises/', + label: 'artemisApp.courseOverview.menu.exercises', + }); expect(component.breadcrumbs[3]).toMatchObject({ uri: '/courses/1/exercises/2/', label: 'Test Exercise' }); }); }); it.each([ - { width: 1200, account: { login: 'test' }, roles: [Authority.ADMIN], expected: { isCollapsed: false, isNavbarNavVertical: false, iconsMovedToMenu: false } }, - { width: 1100, account: { login: 'test' }, roles: [Authority.ADMIN], expected: { isCollapsed: true, isNavbarNavVertical: false, iconsMovedToMenu: false } }, - { width: 600, account: { login: 'test' }, roles: [Authority.ADMIN], expected: { isCollapsed: true, isNavbarNavVertical: false, iconsMovedToMenu: true } }, - { width: 550, account: { login: 'test' }, roles: [Authority.ADMIN], expected: { isCollapsed: true, isNavbarNavVertical: true, iconsMovedToMenu: true } }, - { width: 1000, account: { login: 'test' }, roles: [Authority.INSTRUCTOR], expected: { isCollapsed: false, isNavbarNavVertical: false, iconsMovedToMenu: false } }, - { width: 850, account: { login: 'test' }, roles: [Authority.INSTRUCTOR], expected: { isCollapsed: true, isNavbarNavVertical: false, iconsMovedToMenu: false } }, - { width: 600, account: { login: 'test' }, roles: [Authority.INSTRUCTOR], expected: { isCollapsed: true, isNavbarNavVertical: false, iconsMovedToMenu: true } }, - { width: 470, account: { login: 'test' }, roles: [Authority.INSTRUCTOR], expected: { isCollapsed: true, isNavbarNavVertical: true, iconsMovedToMenu: true } }, - { width: 800, account: { login: 'test' }, roles: [Authority.USER], expected: { isCollapsed: false, isNavbarNavVertical: false, iconsMovedToMenu: false } }, - { width: 650, account: { login: 'test' }, roles: [Authority.USER], expected: { isCollapsed: true, isNavbarNavVertical: false, iconsMovedToMenu: false } }, - { width: 600, account: { login: 'test' }, roles: [Authority.USER], expected: { isCollapsed: true, isNavbarNavVertical: false, iconsMovedToMenu: true } }, - { width: 470, account: { login: 'test' }, roles: [Authority.USER], expected: { isCollapsed: true, isNavbarNavVertical: true, iconsMovedToMenu: true } }, - { width: 520, account: undefined, roles: [], expected: { isCollapsed: false, isNavbarNavVertical: false, iconsMovedToMenu: false } }, - { width: 500, account: undefined, roles: [], expected: { isCollapsed: true, isNavbarNavVertical: false, iconsMovedToMenu: false } }, - { width: 450, account: undefined, roles: [], expected: { isCollapsed: true, isNavbarNavVertical: true, iconsMovedToMenu: false } }, - { width: 400, account: undefined, roles: [], expected: { isCollapsed: true, isNavbarNavVertical: true, iconsMovedToMenu: true } }, + { + width: 1200, + account: { login: 'test' }, + roles: [Authority.ADMIN], + expected: { isCollapsed: false, isNavbarNavVertical: false, iconsMovedToMenu: false }, + }, + { + width: 1100, + account: { login: 'test' }, + roles: [Authority.ADMIN], + expected: { isCollapsed: true, isNavbarNavVertical: false, iconsMovedToMenu: false }, + }, + { + width: 600, + account: { login: 'test' }, + roles: [Authority.ADMIN], + expected: { isCollapsed: true, isNavbarNavVertical: false, iconsMovedToMenu: true }, + }, + { + width: 550, + account: { login: 'test' }, + roles: [Authority.ADMIN], + expected: { isCollapsed: true, isNavbarNavVertical: true, iconsMovedToMenu: true }, + }, + { + width: 1000, + account: { login: 'test' }, + roles: [Authority.INSTRUCTOR], + expected: { isCollapsed: false, isNavbarNavVertical: false, iconsMovedToMenu: false }, + }, + { + width: 850, + account: { login: 'test' }, + roles: [Authority.INSTRUCTOR], + expected: { isCollapsed: true, isNavbarNavVertical: false, iconsMovedToMenu: false }, + }, + { + width: 600, + account: { login: 'test' }, + roles: [Authority.INSTRUCTOR], + expected: { isCollapsed: true, isNavbarNavVertical: false, iconsMovedToMenu: true }, + }, + { + width: 470, + account: { login: 'test' }, + roles: [Authority.INSTRUCTOR], + expected: { isCollapsed: true, isNavbarNavVertical: true, iconsMovedToMenu: true }, + }, + { + width: 800, + account: { login: 'test' }, + roles: [Authority.USER], + expected: { isCollapsed: false, isNavbarNavVertical: false, iconsMovedToMenu: false }, + }, + { + width: 650, + account: { login: 'test' }, + roles: [Authority.USER], + expected: { isCollapsed: true, isNavbarNavVertical: false, iconsMovedToMenu: false }, + }, + { + width: 600, + account: { login: 'test' }, + roles: [Authority.USER], + expected: { isCollapsed: true, isNavbarNavVertical: false, iconsMovedToMenu: true }, + }, + { + width: 470, + account: { login: 'test' }, + roles: [Authority.USER], + expected: { isCollapsed: true, isNavbarNavVertical: true, iconsMovedToMenu: true }, + }, + { + width: 520, + account: undefined, + roles: [], + expected: { isCollapsed: false, isNavbarNavVertical: false, iconsMovedToMenu: false }, + }, + { + width: 500, + account: undefined, + roles: [], + expected: { isCollapsed: true, isNavbarNavVertical: false, iconsMovedToMenu: false }, + }, + { + width: 450, + account: undefined, + roles: [], + expected: { isCollapsed: true, isNavbarNavVertical: true, iconsMovedToMenu: false }, + }, + { + width: 400, + account: undefined, + roles: [], + expected: { isCollapsed: true, isNavbarNavVertical: true, iconsMovedToMenu: true }, + }, ])('should calculate correct breakpoints', ({ width, account, roles, expected }) => { const accountService = TestBed.inject(AccountService); jest.spyOn(accountService, 'hasAnyAuthorityDirect').mockImplementation((authArray) => authArray.some((auth) => (roles as string[]).includes(auth))); @@ -694,6 +873,10 @@ describe('NavbarComponent', () => { component.onResize(); - expect({ isCollapsed: component.isCollapsed, isNavbarNavVertical: component.isNavbarNavVertical, iconsMovedToMenu: component.iconsMovedToMenu }).toEqual(expected); + expect({ + isCollapsed: component.isCollapsed, + isNavbarNavVertical: component.isNavbarNavVertical, + iconsMovedToMenu: component.iconsMovedToMenu, + }).toEqual(expected); }); }); From 7d78fdd326bb83c679bb47ab3de581bbf0a029a0 Mon Sep 17 00:00:00 2001 From: Johannes Wiest Date: Mon, 7 Oct 2024 15:14:36 +0200 Subject: [PATCH 16/24] fix more client tests --- src/main/webapp/app/core/theme/theme-switch.component.ts | 3 ++- .../spec/component/shared/navbar.component.spec.ts | 7 ++++++- .../guided-tour/guided-tour.integration.spec.ts | 3 ++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main/webapp/app/core/theme/theme-switch.component.ts b/src/main/webapp/app/core/theme/theme-switch.component.ts index 06d60d3fc433..b0856be1e77f 100644 --- a/src/main/webapp/app/core/theme/theme-switch.component.ts +++ b/src/main/webapp/app/core/theme/theme-switch.component.ts @@ -5,6 +5,7 @@ import { PlacementArray } from '@ng-bootstrap/ng-bootstrap/util/positioning'; import { Theme, ThemeService } from 'app/core/theme/theme.service'; import { fromEvent } from 'rxjs'; import { faSync } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; /** * Displays a sun or a moon in the navbar, depending on the current theme. @@ -15,7 +16,7 @@ import { faSync } from '@fortawesome/free-solid-svg-icons'; selector: 'jhi-theme-switch', templateUrl: './theme-switch.component.html', styleUrls: ['theme-switch.component.scss'], - imports: [TranslateModule, NgbModule], + imports: [TranslateModule, NgbModule, FontAwesomeModule], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, }) diff --git a/src/test/javascript/spec/component/shared/navbar.component.spec.ts b/src/test/javascript/spec/component/shared/navbar.component.spec.ts index 1afb333a8c12..342ccd3ef2a5 100644 --- a/src/test/javascript/spec/component/shared/navbar.component.spec.ts +++ b/src/test/javascript/spec/component/shared/navbar.component.spec.ts @@ -110,7 +110,7 @@ describe('NavbarComponent', () => { MockComponent(JhiConnectionWarningComponent), MockComponent(SystemNotificationComponent), MockComponent(FaIconComponent), - ThemeSwitchComponent, + MockComponent(ThemeSwitchComponent), ], providers: [ provideHttpClient(), @@ -136,6 +136,11 @@ describe('NavbarComponent', () => { }, ], }) + .overrideComponent(NavbarComponent, { + remove: { + imports: [ThemeSwitchComponent], + }, + }) .compileComponents() .then(() => { fixture = TestBed.createComponent(NavbarComponent); diff --git a/src/test/javascript/spec/integration/guided-tour/guided-tour.integration.spec.ts b/src/test/javascript/spec/integration/guided-tour/guided-tour.integration.spec.ts index 57b934b9497c..2dbbe67b9f26 100644 --- a/src/test/javascript/spec/integration/guided-tour/guided-tour.integration.spec.ts +++ b/src/test/javascript/spec/integration/guided-tour/guided-tour.integration.spec.ts @@ -85,7 +85,8 @@ describe('Guided tour integration', () => { MockComponent(CoursesComponent), MockComponent(SecuredImageComponent), MockComponent(SystemNotificationComponent), - MockComponent(ThemeSwitchComponent), + // Component can not be mocked at the moment https://github.com/help-me-mom/ng-mocks/issues/8634 + ThemeSwitchComponent, MockComponent(DocumentationButtonComponent), MockPipe(ArtemisTranslatePipe), MockPipe(ArtemisDatePipe), From 7b76e354df798b071e8192c988003310773ae317 Mon Sep 17 00:00:00 2001 From: Johannes Wiest Date: Tue, 8 Oct 2024 13:22:32 +0200 Subject: [PATCH 17/24] fix monaco-editor.service.spec.ts --- .../monaco-editor/monaco-editor.service.ts | 3 +-- .../monaco-editor.service.spec.ts | 23 +++++++++++-------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/main/webapp/app/shared/monaco-editor/monaco-editor.service.ts b/src/main/webapp/app/shared/monaco-editor/monaco-editor.service.ts index 0d00d81300a3..48855dde6e7d 100644 --- a/src/main/webapp/app/shared/monaco-editor/monaco-editor.service.ts +++ b/src/main/webapp/app/shared/monaco-editor/monaco-editor.service.ts @@ -2,7 +2,6 @@ import { Injectable, effect, inject } from '@angular/core'; import * as monaco from 'monaco-editor'; import { CUSTOM_MARKDOWN_CONFIG, CUSTOM_MARKDOWN_LANGUAGE, CUSTOM_MARKDOWN_LANGUAGE_ID } from 'app/shared/monaco-editor/model/languages/monaco-custom-markdown.language'; import { Theme, ThemeService } from 'app/core/theme/theme.service'; -import { toSignal } from '@angular/core/rxjs-interop'; /** * Service providing shared functionality for the Monaco editor. @@ -15,7 +14,7 @@ export class MonacoEditorService { static readonly DARK_THEME_ID = 'vs-dark'; private readonly themeService: ThemeService = inject(ThemeService); - private readonly currentTheme = toSignal(this.themeService.getCurrentThemeObservable(), { requireSync: true }); + private readonly currentTheme = this.themeService.currentTheme; constructor() { monaco.languages.register({ id: CUSTOM_MARKDOWN_LANGUAGE_ID }); diff --git a/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor.service.spec.ts b/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor.service.spec.ts index 82ffd4e2b8b3..74949e5aff7f 100644 --- a/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor.service.spec.ts +++ b/src/test/javascript/spec/component/shared/monaco-editor/monaco-editor.service.spec.ts @@ -1,17 +1,17 @@ import { TestBed } from '@angular/core/testing'; import * as monaco from 'monaco-editor'; import { Theme, ThemeService } from 'app/core/theme/theme.service'; -import { MonacoEditorService } from '../../../../../../main/webapp/app/shared/monaco-editor/monaco-editor.service'; +import { MonacoEditorService } from 'app/shared/monaco-editor/monaco-editor.service'; import { ArtemisTestModule } from '../../../test.module'; import { CUSTOM_MARKDOWN_LANGUAGE_ID } from 'app/shared/monaco-editor/model/languages/monaco-custom-markdown.language'; -import { BehaviorSubject } from 'rxjs'; import { MockResizeObserver } from '../../../helpers/mocks/service/mock-resize-observer'; describe('MonacoEditorService', () => { let monacoEditorService: MonacoEditorService; let setThemeSpy: jest.SpyInstance; let registerLanguageSpy: jest.SpyInstance; - const themeSubject = new BehaviorSubject(Theme.LIGHT); + + let themeService: ThemeService; beforeEach(() => { TestBed.configureTestingModule({ @@ -23,8 +23,7 @@ describe('MonacoEditorService', () => { }); registerLanguageSpy = jest.spyOn(monaco.languages, 'register'); setThemeSpy = jest.spyOn(monaco.editor, 'setTheme'); - const themeService = TestBed.inject(ThemeService); - jest.spyOn(themeService, 'getCurrentThemeObservable').mockReturnValue(themeSubject.asObservable()); + themeService = TestBed.inject(ThemeService); monacoEditorService = TestBed.inject(MonacoEditorService); }); @@ -42,20 +41,26 @@ describe('MonacoEditorService', () => { // Initialization: The editor should be in light mode since that is what we initialized the themeSubject with expect(setThemeSpy).toHaveBeenCalledExactlyOnceWith(MonacoEditorService.LIGHT_THEME_ID); // Switch to dark theme - themeSubject.next(Theme.DARK); + themeService.applyThemePreference(Theme.DARK); TestBed.flushEffects(); expect(setThemeSpy).toHaveBeenCalledTimes(2); expect(setThemeSpy).toHaveBeenNthCalledWith(2, MonacoEditorService.DARK_THEME_ID); // Switch back to light theme - themeSubject.next(Theme.LIGHT); + themeService.applyThemePreference(Theme.LIGHT); TestBed.flushEffects(); expect(setThemeSpy).toHaveBeenCalledTimes(3); expect(setThemeSpy).toHaveBeenNthCalledWith(3, MonacoEditorService.LIGHT_THEME_ID); }); it.each([ - { className: 'monaco-editor', createFn: (element: HTMLElement) => monacoEditorService.createStandaloneCodeEditor(element) }, - { className: 'monaco-diff-editor', createFn: (element: HTMLElement) => monacoEditorService.createStandaloneDiffEditor(element) }, + { + className: 'monaco-editor', + createFn: (element: HTMLElement) => monacoEditorService.createStandaloneCodeEditor(element), + }, + { + className: 'monaco-diff-editor', + createFn: (element: HTMLElement) => monacoEditorService.createStandaloneDiffEditor(element), + }, ])( 'should insert an editor ($className) into the provided DOM element', ({ className, createFn }: { className: string; createFn: (element: HTMLElement) => monaco.editor.IStandaloneCodeEditor | monaco.editor.IStandaloneDiffEditor }) => { From ccc07c37ca676c44b88c6d39347805156f647c47 Mon Sep 17 00:00:00 2001 From: Johannes Wiest Date: Tue, 8 Oct 2024 13:34:30 +0200 Subject: [PATCH 18/24] fix code-editor-instructor.integration.spec.ts --- .../code-editor/code-editor-instructor.integration.spec.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/javascript/spec/integration/code-editor/code-editor-instructor.integration.spec.ts b/src/test/javascript/spec/integration/code-editor/code-editor-instructor.integration.spec.ts index 294c3d3a9a4e..bfe14ef786cb 100644 --- a/src/test/javascript/spec/integration/code-editor/code-editor-instructor.integration.spec.ts +++ b/src/test/javascript/spec/integration/code-editor/code-editor-instructor.integration.spec.ts @@ -3,7 +3,7 @@ import { LocalStorageService, SessionStorageService } from 'ngx-webstorage'; import { TranslateModule } from '@ngx-translate/core'; import { JhiLanguageHelper } from 'app/core/language/language.helper'; import { AccountService } from 'app/core/auth/account.service'; -import { ChangeDetectorRef, DebugElement } from '@angular/core'; +import { DebugElement } from '@angular/core'; import { ActivatedRoute, Params, Router } from '@angular/router'; import { BehaviorSubject, Subject, of, throwError } from 'rxjs'; import { ArtemisTestModule } from '../../test.module'; @@ -116,7 +116,6 @@ describe('CodeEditorInstructorIntegration', () => { ], providers: [ JhiLanguageHelper, - ChangeDetectorRef, { provide: Router, useClass: MockRouter }, { provide: AccountService, useClass: MockAccountService }, { provide: ActivatedRoute, useClass: MockActivatedRouteWithSubjects }, From 2a19411aa5635947d7ab5d87563ce45fe0fe95bd Mon Sep 17 00:00:00 2001 From: Johannes Wiest Date: Tue, 8 Oct 2024 14:11:43 +0200 Subject: [PATCH 19/24] add track expression & add fix more programming-exercise-instruction.component.spec.ts --- .../programming-exercise-instruction-step-wizard.component.html | 2 +- .../programming-exercise-instruction.component.spec.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/webapp/app/exercises/programming/shared/instructions-render/step-wizard/programming-exercise-instruction-step-wizard.component.html b/src/main/webapp/app/exercises/programming/shared/instructions-render/step-wizard/programming-exercise-instruction-step-wizard.component.html index eaf81d0a62f8..3ac4387efbfc 100644 --- a/src/main/webapp/app/exercises/programming/shared/instructions-render/step-wizard/programming-exercise-instruction-step-wizard.component.html +++ b/src/main/webapp/app/exercises/programming/shared/instructions-render/step-wizard/programming-exercise-instruction-step-wizard.component.html @@ -2,7 +2,7 @@
- @for (step of steps; track step; let i = $index) { + @for (step of steps; let i = $index; track i) {
{ expect(debugElement.query(By.css('.stepwizard'))).not.toBeNull(); expect(debugElement.queryAll(By.css('.btn-circle'))).toHaveLength(2); - fixture.detectChanges(); tick(); fixture.detectChanges(); From c333b95847936405302ac1cd6cec87e22d39a8be Mon Sep 17 00:00:00 2001 From: "Felix T.J. Dietrich" Date: Thu, 7 Nov 2024 15:06:05 +0100 Subject: [PATCH 20/24] fix tests --- .../webapp/app/lti/lti13-exercise-launch.component.ts | 2 +- .../app/shared/monaco-editor/monaco-editor.service.ts | 7 ------- ...programming-exercise-instruction.component.spec.ts | 11 +++++++---- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/main/webapp/app/lti/lti13-exercise-launch.component.ts b/src/main/webapp/app/lti/lti13-exercise-launch.component.ts index 2c939e994279..28605d6620e6 100644 --- a/src/main/webapp/app/lti/lti13-exercise-launch.component.ts +++ b/src/main/webapp/app/lti/lti13-exercise-launch.component.ts @@ -147,7 +147,7 @@ export class Lti13ExerciseLaunchComponent implements OnInit { replaceWindowLocationWrapper(url: string): void { this.ltiService.setShownViaLti(true); - this.themeService.applyThemeExplicitly(Theme.LIGHT); + this.themeService.applyThemePreference(Theme.LIGHT); const path = new URL(url).pathname; this.router.navigate([path], { replaceUrl: true }); diff --git a/src/main/webapp/app/shared/monaco-editor/monaco-editor.service.ts b/src/main/webapp/app/shared/monaco-editor/monaco-editor.service.ts index 5e624c647da5..05d9220eaafc 100644 --- a/src/main/webapp/app/shared/monaco-editor/monaco-editor.service.ts +++ b/src/main/webapp/app/shared/monaco-editor/monaco-editor.service.ts @@ -13,9 +13,6 @@ import { MONACO_DARK_THEME_DEFINITION } from 'app/shared/monaco-editor/model/the */ @Injectable({ providedIn: 'root' }) export class MonacoEditorService { - static readonly LIGHT_THEME_ID = 'vs'; - static readonly DARK_THEME_ID = 'vs-dark'; - private readonly themeService: ThemeService = inject(ThemeService); private readonly currentTheme = this.themeService.currentTheme; @@ -42,10 +39,6 @@ export class MonacoEditorService { monaco.languages.register({ id: CUSTOM_MARKDOWN_LANGUAGE_ID }); monaco.languages.setLanguageConfiguration(CUSTOM_MARKDOWN_LANGUAGE_ID, CUSTOM_MARKDOWN_CONFIG); monaco.languages.setMonarchTokensProvider(CUSTOM_MARKDOWN_LANGUAGE_ID, CUSTOM_MARKDOWN_LANGUAGE); - - effect(() => { - this.applyTheme(this.currentTheme()); - }); } /** diff --git a/src/test/javascript/spec/component/programming-exercise/programming-exercise-instruction.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/programming-exercise-instruction.component.spec.ts index 4a2e2192fd59..c54a48ecbad7 100644 --- a/src/test/javascript/spec/component/programming-exercise/programming-exercise-instruction.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/programming-exercise-instruction.component.spec.ts @@ -157,7 +157,8 @@ describe('ProgrammingExerciseInstructionComponent', () => { expect(comp.problemStatement).toBeUndefined(); expect(loadInitialResultStub).not.toHaveBeenCalled(); expect(comp.latestResult).toBeUndefined(); - expect(updateMarkdownStub).not.toHaveBeenCalled(); + // Fist call is because of effect() within toObservable + expect(updateMarkdownStub).not.toHaveBeenCalledTimes(2); expect(noInstructionsAvailableSpy).toHaveBeenCalledOnce(); expect(comp.isInitial).toBeFalse(); expect(comp.isLoading).toBeFalse(); @@ -189,7 +190,8 @@ describe('ProgrammingExerciseInstructionComponent', () => { currentValue: { ...comp.exercise, problemStatement: newProblemStatement }, firstChange: false, }); - expect(updateMarkdownStub).toHaveBeenCalledOnce(); + // Fist call is because of effect() within toObservable + expect(updateMarkdownStub).toHaveBeenCalledTimes(2); expect(loadInitialResult).not.toHaveBeenCalled(); }); @@ -212,7 +214,8 @@ describe('ProgrammingExerciseInstructionComponent', () => { triggerChanges(comp, { property: 'exercise', currentValue: { ...comp.exercise, problemStatement: newProblemStatement }, firstChange: false }); fixture.detectChanges(); expect(comp.markdownExtensions).toHaveLength(2); - expect(updateMarkdownStub).toHaveBeenCalledOnce(); + // Fist call is because of effect() within toObservable + expect(updateMarkdownStub).toHaveBeenCalledTimes(2); expect(loadInitialResult).not.toHaveBeenCalled(); }); @@ -241,7 +244,7 @@ describe('ProgrammingExerciseInstructionComponent', () => { expect(getLatestResultWithFeedbacks).toHaveBeenCalledOnce(); // result should have been fetched with the submission as this is required to show details for it expect(getLatestResultWithFeedbacks).toHaveBeenCalledWith(participation.id, true); - expect(updateMarkdownStub).toHaveBeenCalledOnce(); + expect(updateMarkdownStub).toHaveBeenCalledTimes(2); expect(comp.isInitial).toBeFalse(); expect(comp.isLoading).toBeFalse(); }); From e5add94eac906e519dd9aa83b71a60f82c550f02 Mon Sep 17 00:00:00 2001 From: "Felix T.J. Dietrich" Date: Fri, 8 Nov 2024 13:23:17 +0100 Subject: [PATCH 21/24] fix programming exercise instructions component --- .../programming-exercise-instruction.component.ts | 4 +++- ...gramming-exercise-instruction.component.spec.ts | 14 +++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main/webapp/app/exercises/programming/shared/instructions-render/programming-exercise-instruction.component.ts b/src/main/webapp/app/exercises/programming/shared/instructions-render/programming-exercise-instruction.component.ts index 89a410dd707a..d53b738b3ff1 100644 --- a/src/main/webapp/app/exercises/programming/shared/instructions-render/programming-exercise-instruction.component.ts +++ b/src/main/webapp/app/exercises/programming/shared/instructions-render/programming-exercise-instruction.component.ts @@ -90,7 +90,9 @@ export class ProgrammingExerciseInstructionComponent implements OnChanges, OnDes private generateHtmlSubscription: Subscription; private testCases?: ProgrammingExerciseTestCase[]; private themeChangeSubscription = toObservable(this.themeService.currentTheme).subscribe(() => { - this.updateMarkdown(); + if (!this.isInitial) { + this.updateMarkdown(); + } }); // Icons diff --git a/src/test/javascript/spec/component/programming-exercise/programming-exercise-instruction.component.spec.ts b/src/test/javascript/spec/component/programming-exercise/programming-exercise-instruction.component.spec.ts index c54a48ecbad7..d0233fde1827 100644 --- a/src/test/javascript/spec/component/programming-exercise/programming-exercise-instruction.component.spec.ts +++ b/src/test/javascript/spec/component/programming-exercise/programming-exercise-instruction.component.spec.ts @@ -157,8 +157,7 @@ describe('ProgrammingExerciseInstructionComponent', () => { expect(comp.problemStatement).toBeUndefined(); expect(loadInitialResultStub).not.toHaveBeenCalled(); expect(comp.latestResult).toBeUndefined(); - // Fist call is because of effect() within toObservable - expect(updateMarkdownStub).not.toHaveBeenCalledTimes(2); + expect(updateMarkdownStub).not.toHaveBeenCalled(); expect(noInstructionsAvailableSpy).toHaveBeenCalledOnce(); expect(comp.isInitial).toBeFalse(); expect(comp.isLoading).toBeFalse(); @@ -190,8 +189,7 @@ describe('ProgrammingExerciseInstructionComponent', () => { currentValue: { ...comp.exercise, problemStatement: newProblemStatement }, firstChange: false, }); - // Fist call is because of effect() within toObservable - expect(updateMarkdownStub).toHaveBeenCalledTimes(2); + expect(updateMarkdownStub).toHaveBeenCalledOnce(); expect(loadInitialResult).not.toHaveBeenCalled(); }); @@ -214,8 +212,7 @@ describe('ProgrammingExerciseInstructionComponent', () => { triggerChanges(comp, { property: 'exercise', currentValue: { ...comp.exercise, problemStatement: newProblemStatement }, firstChange: false }); fixture.detectChanges(); expect(comp.markdownExtensions).toHaveLength(2); - // Fist call is because of effect() within toObservable - expect(updateMarkdownStub).toHaveBeenCalledTimes(2); + expect(updateMarkdownStub).toHaveBeenCalledOnce(); expect(loadInitialResult).not.toHaveBeenCalled(); }); @@ -244,7 +241,7 @@ describe('ProgrammingExerciseInstructionComponent', () => { expect(getLatestResultWithFeedbacks).toHaveBeenCalledOnce(); // result should have been fetched with the submission as this is required to show details for it expect(getLatestResultWithFeedbacks).toHaveBeenCalledWith(participation.id, true); - expect(updateMarkdownStub).toHaveBeenCalledTimes(2); + expect(updateMarkdownStub).toHaveBeenCalledOnce(); expect(comp.isInitial).toBeFalse(); expect(comp.isLoading).toBeFalse(); }); @@ -450,10 +447,13 @@ describe('ProgrammingExerciseInstructionComponent', () => { it('should update the markdown on a theme change', () => { const updateMarkdownStub = jest.spyOn(comp, 'updateMarkdown'); + + comp.isInitial = false; themeService.applyThemePreference(Theme.DARK); fixture.detectChanges(); + // toObservable triggers a effect in the background on initial detectChanges expect(updateMarkdownStub).toHaveBeenCalledOnce(); }); From c4cd5fa9084a2dea0f16371e5e03efa324d44e78 Mon Sep 17 00:00:00 2001 From: "Felix T.J. Dietrich" Date: Fri, 8 Nov 2024 13:30:20 +0100 Subject: [PATCH 22/24] fix translation --- src/main/webapp/app/core/theme/theme-switch.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/app/core/theme/theme-switch.component.ts b/src/main/webapp/app/core/theme/theme-switch.component.ts index b0856be1e77f..7911e3237e32 100644 --- a/src/main/webapp/app/core/theme/theme-switch.component.ts +++ b/src/main/webapp/app/core/theme/theme-switch.component.ts @@ -1,11 +1,11 @@ import { ChangeDetectionStrategy, Component, OnInit, computed, inject, input, viewChild } from '@angular/core'; -import { TranslateModule } from '@ngx-translate/core'; import { NgbModule, NgbPopover } from '@ng-bootstrap/ng-bootstrap'; import { PlacementArray } from '@ng-bootstrap/ng-bootstrap/util/positioning'; import { Theme, ThemeService } from 'app/core/theme/theme.service'; import { fromEvent } from 'rxjs'; import { faSync } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; +import { TranslateDirective } from 'app/shared/language/translate.directive'; /** * Displays a sun or a moon in the navbar, depending on the current theme. @@ -16,7 +16,7 @@ import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; selector: 'jhi-theme-switch', templateUrl: './theme-switch.component.html', styleUrls: ['theme-switch.component.scss'], - imports: [TranslateModule, NgbModule, FontAwesomeModule], + imports: [TranslateDirective, NgbModule, FontAwesomeModule], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, }) From e697bb5650225c577cd1ba98b3a46b3048ee51e1 Mon Sep 17 00:00:00 2001 From: "Felix T.J. Dietrich" Date: Fri, 8 Nov 2024 15:05:54 +0100 Subject: [PATCH 23/24] fix modules? --- src/main/webapp/app/core/theme/theme-switch.component.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/webapp/app/core/theme/theme-switch.component.ts b/src/main/webapp/app/core/theme/theme-switch.component.ts index 7911e3237e32..2108bd3a0fb6 100644 --- a/src/main/webapp/app/core/theme/theme-switch.component.ts +++ b/src/main/webapp/app/core/theme/theme-switch.component.ts @@ -1,11 +1,13 @@ import { ChangeDetectionStrategy, Component, OnInit, computed, inject, input, viewChild } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ArtemisSharedModule } from 'app/shared/shared.module'; +import { TranslateModule } from '@ngx-translate/core'; import { NgbModule, NgbPopover } from '@ng-bootstrap/ng-bootstrap'; import { PlacementArray } from '@ng-bootstrap/ng-bootstrap/util/positioning'; import { Theme, ThemeService } from 'app/core/theme/theme.service'; import { fromEvent } from 'rxjs'; import { faSync } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; -import { TranslateDirective } from 'app/shared/language/translate.directive'; /** * Displays a sun or a moon in the navbar, depending on the current theme. @@ -16,7 +18,7 @@ import { TranslateDirective } from 'app/shared/language/translate.directive'; selector: 'jhi-theme-switch', templateUrl: './theme-switch.component.html', styleUrls: ['theme-switch.component.scss'], - imports: [TranslateDirective, NgbModule, FontAwesomeModule], + imports: [TranslateModule, CommonModule, ArtemisSharedModule, NgbModule, FontAwesomeModule], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, }) From 2d4f970570d69d8428c5b166ed9b8da394ae76d4 Mon Sep 17 00:00:00 2001 From: "Felix T.J. Dietrich" Date: Wed, 13 Nov 2024 12:01:18 +0100 Subject: [PATCH 24/24] fix failing tests --- .../spec/component/shared/navbar.component.spec.ts | 4 ++++ .../javascript/spec/helpers/mocks/mock-instance.helper.ts | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/src/test/javascript/spec/component/shared/navbar.component.spec.ts b/src/test/javascript/spec/component/shared/navbar.component.spec.ts index 342ccd3ef2a5..bc2e7cb37b92 100644 --- a/src/test/javascript/spec/component/shared/navbar.component.spec.ts +++ b/src/test/javascript/spec/component/shared/navbar.component.spec.ts @@ -40,6 +40,7 @@ import { provideHttpClientTesting } from '@angular/common/http/testing'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { GuidedTourService } from 'app/guided-tour/guided-tour.service'; import { ThemeSwitchComponent } from 'app/core/theme/theme-switch.component'; +import { mockThemeSwitcherComponentViewChildren } from '../../helpers/mocks/mock-instance.helper'; class MockBreadcrumb { label: string; @@ -92,6 +93,9 @@ describe('NavbarComponent', () => { activeProfiles: ['test'], } as ProfileInfo; + // Workaround for an error with MockComponent(). You can remove this once https://github.com/help-me-mom/ng-mocks/issues/8634 is resolved. + mockThemeSwitcherComponentViewChildren(); + beforeEach(() => { return TestBed.configureTestingModule({ imports: [NgbTooltipMocksModule, NgbCollapseMocksModule, NgbDropdownMocksModule], diff --git a/src/test/javascript/spec/helpers/mocks/mock-instance.helper.ts b/src/test/javascript/spec/helpers/mocks/mock-instance.helper.ts index 8f7c213e5f6b..b84969f15363 100644 --- a/src/test/javascript/spec/helpers/mocks/mock-instance.helper.ts +++ b/src/test/javascript/spec/helpers/mocks/mock-instance.helper.ts @@ -1,6 +1,7 @@ import { MockInstance } from 'ng-mocks'; import { CodeEditorMonacoComponent } from 'app/exercises/programming/shared/code-editor/monaco/code-editor-monaco.component'; import { signal } from '@angular/core'; +import { ThemeSwitchComponent } from 'app/core/theme/theme-switch.component'; /* * This file contains mock instances for the tests where they would otherwise fail due to the use of a signal-based viewChild or contentChild with MockComponent(SomeComponent). @@ -19,3 +20,10 @@ export function mockCodeEditorMonacoViewChildren() { inlineFeedbackSuggestionComponents: signal([]), })); } + +export function mockThemeSwitcherComponentViewChildren() { + MockInstance.scope('case'); + MockInstance(ThemeSwitchComponent, () => ({ + popover: signal({}), + })); +}