From dfb25b249bee85e23971c7fce295eafd891c1645 Mon Sep 17 00:00:00 2001 From: Nessar Date: Fri, 28 Aug 2026 14:56:45 +0200 Subject: [PATCH] refactor(theme): remove obsolete compatibility --- .../dark-light-mode-button.spec.ts | 134 +++++++----------- src/app/services/theme-service.spec.ts | 42 ------ src/app/services/theme-service.ts | 16 +-- 3 files changed, 55 insertions(+), 137 deletions(-) delete mode 100644 src/app/services/theme-service.spec.ts diff --git a/src/app/components/dark-light-mode-button/dark-light-mode-button.spec.ts b/src/app/components/dark-light-mode-button/dark-light-mode-button.spec.ts index c0b70bf..9bd228c 100644 --- a/src/app/components/dark-light-mode-button/dark-light-mode-button.spec.ts +++ b/src/app/components/dark-light-mode-button/dark-light-mode-button.spec.ts @@ -6,47 +6,36 @@ import { vi } from 'vitest'; class ControllableMatchMedia { matches: boolean; readonly media = '(prefers-color-scheme: light)'; - onchange: ((this: MediaQueryList, event: MediaQueryListEvent) => unknown) | null = null; - private readonly listeners = new Set<(event: MediaQueryListEvent) => void>(); + private listener?: (event: MediaQueryListEvent) => void; constructor(matches: boolean) { this.matches = matches; } - addEventListener(_type: string, listener: EventListenerOrEventListenerObject | null): void { - if (typeof listener === 'function') { - this.listeners.add(listener as (event: MediaQueryListEvent) => void); - } - } - - removeEventListener(_type: string, listener: EventListenerOrEventListenerObject | null): void { - if (typeof listener === 'function') { - this.listeners.delete(listener as (event: MediaQueryListEvent) => void); - } - } - - addListener(listener: (this: MediaQueryList, event: MediaQueryListEvent) => unknown): void { - this.listeners.add(listener); - } - - removeListener(listener: (this: MediaQueryList, event: MediaQueryListEvent) => unknown): void { - this.listeners.delete(listener); - } - - dispatchEvent(_event: Event): boolean { - return true; + addEventListener(_type: string, listener: (event: MediaQueryListEvent) => void): void { + this.listener = listener; } change(matches: boolean): void { this.matches = matches; - const event = { matches, media: this.media } as MediaQueryListEvent; - this.listeners.forEach((listener) => listener(event)); - this.onchange?.call(this as unknown as MediaQueryList, event); + this.listener?.({ matches, media: this.media } as MediaQueryListEvent); } } +function expectTheme( + button: HTMLButtonElement, + icon: string, + description: string, + isLight: boolean, +): void { + expect(button.querySelector('mat-icon')?.getAttribute('data-mat-icon-name')).toBe(icon); + expect(button.getAttribute('aria-label')).toBe(description); + expect(button.getAttribute('title')).toBe(description); + expect(document.body.classList.contains('light-mode')).toBe(isLight); + expect(document.body.classList.contains('dark-mode')).toBe(!isLight); +} + describe('DarkLightModeButton', () => { - let component: DarkLightModeButton; let fixture: ComponentFixture; let mediaQuery: ControllableMatchMedia; @@ -60,7 +49,6 @@ describe('DarkLightModeButton', () => { }).compileComponents(); fixture = TestBed.createComponent(DarkLightModeButton); - component = fixture.componentInstance; fixture.detectChanges(); }); @@ -69,23 +57,14 @@ describe('DarkLightModeButton', () => { document.body.classList.remove('light-mode', 'dark-mode'); }); - it('should create', () => { - expect(component).toBeTruthy(); - }); - it('starts in System and resolves its initial appearance from the browser preference', () => { const button = fixture.nativeElement.querySelector('button') as HTMLButtonElement; - const icon = button.querySelector('mat-icon'); - - expect(icon?.getAttribute('data-mat-icon-name')).toBe('brightness_auto'); - expect(button.getAttribute('aria-label')).toBe( + expectTheme( + button, + 'brightness_auto', 'Theme: System (currently Light). Switch to Light theme', + true, ); - expect(button.getAttribute('title')).toBe( - 'Theme: System (currently Light). Switch to Light theme', - ); - expect(document.body.classList.contains('light-mode')).toBe(true); - expect(document.body.classList.contains('dark-mode')).toBe(false); }); it('cycles System to Light to Dark and wraps back to System', () => { @@ -93,45 +72,30 @@ describe('DarkLightModeButton', () => { button.click(); fixture.detectChanges(); - expect(button.querySelector('mat-icon')?.getAttribute('data-mat-icon-name')).toBe( + expectTheme( + button, 'light_mode', - ); - expect(button.getAttribute('aria-label')).toBe( 'Theme: Light (currently Light). Switch to Dark theme', + true, ); - expect(button.getAttribute('title')).toBe( - 'Theme: Light (currently Light). Switch to Dark theme', - ); - expect(document.body.classList.contains('light-mode')).toBe(true); - expect(document.body.classList.contains('dark-mode')).toBe(false); button.click(); fixture.detectChanges(); - expect(button.querySelector('mat-icon')?.getAttribute('data-mat-icon-name')).toBe( + expectTheme( + button, 'dark_mode', - ); - expect(button.getAttribute('aria-label')).toBe( 'Theme: Dark (currently Dark). Switch to System theme', + false, ); - expect(button.getAttribute('title')).toBe( - 'Theme: Dark (currently Dark). Switch to System theme', - ); - expect(document.body.classList.contains('light-mode')).toBe(false); - expect(document.body.classList.contains('dark-mode')).toBe(true); button.click(); fixture.detectChanges(); - expect(button.querySelector('mat-icon')?.getAttribute('data-mat-icon-name')).toBe( + expectTheme( + button, 'brightness_auto', - ); - expect(button.getAttribute('aria-label')).toBe( 'Theme: System (currently Light). Switch to Light theme', + true, ); - expect(button.getAttribute('title')).toBe( - 'Theme: System (currently Light). Switch to Light theme', - ); - expect(document.body.classList.contains('light-mode')).toBe(true); - expect(document.body.classList.contains('dark-mode')).toBe(false); }); it('follows live browser changes while System is selected', () => { @@ -139,22 +103,21 @@ describe('DarkLightModeButton', () => { mediaQuery.change(false); fixture.detectChanges(); - expect(button.getAttribute('aria-label')).toBe( + expectTheme( + button, + 'brightness_auto', 'Theme: System (currently Dark). Switch to Light theme', - ); - expect(document.body.classList.contains('light-mode')).toBe(false); - expect(document.body.classList.contains('dark-mode')).toBe(true); - expect(document.body.classList.contains('light-mode')).not.toBe( - document.body.classList.contains('dark-mode'), + false, ); mediaQuery.change(true); fixture.detectChanges(); - expect(button.getAttribute('aria-label')).toBe( + expectTheme( + button, + 'brightness_auto', 'Theme: System (currently Light). Switch to Light theme', + true, ); - expect(document.body.classList.contains('light-mode')).toBe(true); - expect(document.body.classList.contains('dark-mode')).toBe(false); }); it('keeps explicit modes fixed and uses the latest browser value when returning to System', () => { @@ -164,29 +127,32 @@ describe('DarkLightModeButton', () => { fixture.detectChanges(); mediaQuery.change(false); fixture.detectChanges(); - expect(button.getAttribute('aria-label')).toBe( + expectTheme( + button, + 'light_mode', 'Theme: Light (currently Light). Switch to Dark theme', + true, ); - expect(document.body.classList.contains('light-mode')).toBe(true); - expect(document.body.classList.contains('dark-mode')).toBe(false); button.click(); fixture.detectChanges(); mediaQuery.change(true); fixture.detectChanges(); - expect(button.getAttribute('aria-label')).toBe( + expectTheme( + button, + 'dark_mode', 'Theme: Dark (currently Dark). Switch to System theme', + false, ); - expect(document.body.classList.contains('light-mode')).toBe(false); - expect(document.body.classList.contains('dark-mode')).toBe(true); mediaQuery.change(false); button.click(); fixture.detectChanges(); - expect(button.getAttribute('aria-label')).toBe( + expectTheme( + button, + 'brightness_auto', 'Theme: System (currently Dark). Switch to Light theme', + false, ); - expect(document.body.classList.contains('light-mode')).toBe(false); - expect(document.body.classList.contains('dark-mode')).toBe(true); }); }); diff --git a/src/app/services/theme-service.spec.ts b/src/app/services/theme-service.spec.ts deleted file mode 100644 index f21cfb1..0000000 --- a/src/app/services/theme-service.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { provideZonelessChangeDetection } from '@angular/core'; -import { ThemeService } from './theme-service'; - -describe('ThemeService', () => { - let service: ThemeService; - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [provideZonelessChangeDetection()], - }); - service = TestBed.inject(ThemeService); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); - - it('should initialize with system preference', () => { - expect(service.isLightMode()).toBeDefined(); - }); - - it('should toggle theme', () => { - const initialMode = service.isLightMode(); - service.toggleTheme(); - expect(service.isLightMode()).toBe(!initialMode); - }); - - it('should force light mode when specified', () => { - service.toggleTheme(true); - expect(service.isLightMode()).toBe(true); - expect(document.body.classList.contains('light-mode')).toBe(true); - expect(document.body.classList.contains('dark-mode')).toBe(false); - }); - - it('should force dark mode when specified', () => { - service.toggleTheme(false); - expect(service.isLightMode()).toBe(false); - expect(document.body.classList.contains('light-mode')).toBe(false); - expect(document.body.classList.contains('dark-mode')).toBe(true); - }); -}); diff --git a/src/app/services/theme-service.ts b/src/app/services/theme-service.ts index 9b5a6e5..65291c6 100644 --- a/src/app/services/theme-service.ts +++ b/src/app/services/theme-service.ts @@ -13,7 +13,6 @@ export class ThemeService { const preference = this.selectedPreference(); return preference === 'system' ? (this.systemIsLight() ? 'light' : 'dark') : preference; }); - readonly isLightMode = computed(() => this.effectiveTheme() === 'light'); readonly nextThemePreference = computed(() => this.nextPreference(this.selectedPreference())); constructor() { @@ -26,18 +25,13 @@ export class ThemeService { }); } - setThemePreference(preference: ThemePreference): void { - this.selectedPreference.set(preference); - this.applyEffectiveTheme(); + toggleTheme(): void { + this.setThemePreference(this.nextThemePreference()); } - toggleTheme(forceLight?: boolean): void { - if (forceLight !== undefined) { - this.setThemePreference(forceLight ? 'light' : 'dark'); - return; - } - - this.setThemePreference(this.nextThemePreference()); + private setThemePreference(preference: ThemePreference): void { + this.selectedPreference.set(preference); + this.applyEffectiveTheme(); } private nextPreference(preference: ThemePreference): ThemePreference {