refactor(theme): remove obsolete compatibility

This commit is contained in:
nessar committed 2026-08-28 14:56:45 +02:00
1 parent c19b62ff85
commit dfb25b249b
3 files changed
+55 -137

No files matched your search

@@ -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<DarkLightModeButton>;
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);
});
});
-42
View File
@@ -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);
});
});
+5 -11
View File
@@ -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,20 +25,15 @@ export class ThemeService {
});
}
setThemePreference(preference: ThemePreference): void {
toggleTheme(): void {
this.setThemePreference(this.nextThemePreference());
}
private setThemePreference(preference: ThemePreference): void {
this.selectedPreference.set(preference);
this.applyEffectiveTheme();
}
toggleTheme(forceLight?: boolean): void {
if (forceLight !== undefined) {
this.setThemePreference(forceLight ? 'light' : 'dark');
return;
}
this.setThemePreference(this.nextThemePreference());
}
private nextPreference(preference: ThemePreference): ThemePreference {
return preference === 'system' ? 'light' : preference === 'light' ? 'dark' : 'system';
}