build(test): run unit tests with vitest and jsdom

Karma needed headless Chrome. Vitest with jsdom runs with no browser.
Lighthouse CI now gets Chrome from @puppeteer/browsers.
Two CSS font tests are dropped: jsdom does not apply real CSS.
This commit is contained in:
lobo committed 2026-10-13 09:02:45 +02:00
1 parent dd7c9f6e3d
commit 4e59770e53
14 files changed
+751 -1865

No files matched your search

+1 -54
View File
@@ -1,6 +1,5 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, provideZonelessChangeDetection } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { provideZonelessChangeDetection } from '@angular/core';
import { By, DomSanitizer } from '@angular/platform-browser';
import { MatIconRegistry } from '@angular/material/icon';
import { provideRouter } from '@angular/router';
@@ -188,57 +187,5 @@ describe('AboutComponent', () => {
expect(emptyLinks.length).toBe(0);
expect(fixture.debugElement.query(By.css('[data-testid="contact-mail"]'))).toBeTruthy();
});
it('keeps the mail button in the same font as the links it sits next to', () => {
const mailButton = fixture.debugElement.query(By.css('[data-testid="contact-mail"]')).nativeElement;
const siblingLink = fixture.debugElement
.queryAll(By.css('a'))
.find(anchor => anchor.nativeElement.textContent.trim() === 'Gitea')!.nativeElement;
const buttonStyle = getComputedStyle(mailButton);
const linkStyle = getComputedStyle(siblingLink);
expect(buttonStyle.fontSize).toBe(linkStyle.fontSize);
expect(buttonStyle.fontFamily).toBe(linkStyle.fontFamily);
});
});
});
/**
* Material typography (`brand-family` in styles.scss) and the Tailwind body
* stack are declared in two separate places and are easy to change apart. When
* they disagree, only visitors who happen to have the extra font installed
* locally see it, so it will not show up in review.
*/
describe('global font stack', () => {
@Component({
imports: [MatCardModule],
template: `<mat-card><mat-card-title>probe</mat-card-title></mat-card>`,
})
class FontProbeComponent {}
function normalizeStack(value: string): string {
return value
.replace(/["']/g, '')
.split(',')
.map(family => family.trim())
.filter(family => family.length > 0)
.join(', ');
}
it('resolves Material card titles to the same font stack as body copy', async () => {
await TestBed.configureTestingModule({
imports: [FontProbeComponent],
providers: [provideZonelessChangeDetection()],
}).compileComponents();
const probe = TestBed.createComponent(FontProbeComponent);
probe.detectChanges();
const title = probe.nativeElement.querySelector('.mat-mdc-card-title') as HTMLElement;
const titleStack = normalizeStack(getComputedStyle(title).fontFamily);
const bodyStack = normalizeStack(getComputedStyle(document.body).fontFamily);
expect(titleStack).toBe(bodyStack);
});
});
@@ -44,7 +44,7 @@ describe('SortingService', () => {
const result = finalValues(snapshots);
expect(result).toEqual(expectedSorted);
expect(isAscending(result)).toBeTrue();
expect(isAscending(result)).toBe(true);
});
it(`${algorithm} should not mutate the input array`, () => {
@@ -6,6 +6,18 @@ import { register } from 'swiper/element/bundle';
import { ProjectDialogComponent } from './project-dialog.component';
describe('ProjectDialogComponent', () => {
// jsdom has no Element.part, which swiper-container uses while it renders.
beforeAll(() => {
Object.defineProperty(Element.prototype, 'part', {
value: { add: () => undefined },
configurable: true,
});
});
afterAll(() => {
delete (Element.prototype as { part?: unknown }).part;
});
it('registers the swiper custom elements', () => {
expect(customElements.get('swiper-container')).toBeDefined();
expect(customElements.get('swiper-slide')).toBeDefined();
@@ -13,6 +13,7 @@ describe('GpuCapabilityService', () => {
});
afterEach(() => {
vi.restoreAllMocks();
setGpu(originalGpu);
});
@@ -28,7 +29,7 @@ describe('GpuCapabilityService', () => {
it('falls back to "webgl" when WebGPU is missing but a WebGL2 context exists', async () => {
setGpu(undefined);
const fakeCanvas = { getContext: () => ({}) } as unknown as HTMLCanvasElement;
spyOn(document, 'createElement').and.returnValue(fakeCanvas);
vi.spyOn(document, 'createElement').mockReturnValue(fakeCanvas);
const tier = await service.detect();
@@ -38,7 +39,7 @@ describe('GpuCapabilityService', () => {
it('reports "none" when neither WebGPU nor WebGL is available', async () => {
setGpu(undefined);
const fakeCanvas = { getContext: () => null } as unknown as HTMLCanvasElement;
spyOn(document, 'createElement').and.returnValue(fakeCanvas);
vi.spyOn(document, 'createElement').mockReturnValue(fakeCanvas);
const tier = await service.detect();
+3 -2
View File
@@ -1,13 +1,14 @@
import { TestBed } from '@angular/core/testing';
import { TranslateService } from '@ngx-translate/core';
import type { Mock } from 'vitest';
import { LanguageService } from './language.service';
import { LocalStoreConstants } from '../constants/LocalStoreConstants';
describe('LanguageService', () => {
let translateSpy: jasmine.SpyObj<TranslateService>;
let translateSpy: { use: Mock };
function createService(): LanguageService {
translateSpy = jasmine.createSpyObj<TranslateService>('TranslateService', ['use']);
translateSpy = { use: vi.fn() };
TestBed.configureTestingModule({
providers: [{ provide: TranslateService, useValue: translateSpy }],
});