Plain node activation no longer navigates, case cards stay quiet evidence rows, and the terminal keeps one top-to-bottom scrollport. Co-authored-by: Cursor <cursoragent@cursor.com>
169 lines
6.1 KiB
TypeScript
169 lines
6.1 KiB
TypeScript
import { ApplicationRef, PLATFORM_ID } from '@angular/core';
|
|
import { TestBed, type ComponentFixture } from '@angular/core/testing';
|
|
import { provideRouter, Router, TitleStrategy } from '@angular/router';
|
|
import { App, WIDE_NAV_QUERY } from './app';
|
|
import { routes } from './app.routes';
|
|
import { SITE_CONTENT } from './core/content/content.token';
|
|
import { SITE_CONTENT_DATA } from './core/content/site-content';
|
|
import { SeoTitleStrategy } from './core/seo/seo-title.strategy';
|
|
|
|
function mockViewport(wide: boolean): void {
|
|
Object.defineProperty(window, 'matchMedia', {
|
|
configurable: true,
|
|
writable: true,
|
|
value: (query: string): MediaQueryList =>
|
|
({
|
|
matches: wide && query === WIDE_NAV_QUERY,
|
|
media: query,
|
|
onchange: null,
|
|
addListener: vi.fn(),
|
|
removeListener: vi.fn(),
|
|
addEventListener: vi.fn(),
|
|
removeEventListener: vi.fn(),
|
|
dispatchEvent: vi.fn(),
|
|
}) as MediaQueryList,
|
|
});
|
|
}
|
|
|
|
async function configureApp(
|
|
extraProviders: { provide: unknown; useValue: unknown }[] = [],
|
|
): Promise<void> {
|
|
await TestBed.configureTestingModule({
|
|
imports: [App],
|
|
providers: [
|
|
provideRouter(routes),
|
|
{ provide: SITE_CONTENT, useValue: SITE_CONTENT_DATA },
|
|
{ provide: TitleStrategy, useClass: SeoTitleStrategy },
|
|
...extraProviders,
|
|
],
|
|
}).compileComponents();
|
|
}
|
|
|
|
async function flush(fixture: ComponentFixture<App>): Promise<void> {
|
|
fixture.detectChanges();
|
|
await fixture.whenStable();
|
|
TestBed.inject(ApplicationRef).tick();
|
|
fixture.detectChanges();
|
|
await fixture.whenStable();
|
|
}
|
|
|
|
function toggle(root: HTMLElement): HTMLButtonElement {
|
|
return root.querySelector('.nav-toggle') as HTMLButtonElement;
|
|
}
|
|
|
|
describe('App', () => {
|
|
beforeEach(async () => {
|
|
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(null);
|
|
mockViewport(true);
|
|
|
|
await configureApp();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
Reflect.deleteProperty(window, 'matchMedia');
|
|
});
|
|
|
|
it('should create the shell', async () => {
|
|
const fixture = TestBed.createComponent(App);
|
|
await fixture.whenStable();
|
|
expect(fixture.componentInstance).toBeTruthy();
|
|
});
|
|
|
|
it('should render an accessible application shell', async () => {
|
|
const fixture = TestBed.createComponent(App);
|
|
await fixture.whenStable();
|
|
const compiled = fixture.nativeElement as HTMLElement;
|
|
|
|
const focusable = compiled.querySelectorAll(
|
|
'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])',
|
|
);
|
|
const firstFocusable = focusable.item(0);
|
|
|
|
expect(firstFocusable).toBeTruthy();
|
|
expect(firstFocusable.getAttribute('href')).toBe('#main-content');
|
|
expect(firstFocusable.classList.contains('skip-link')).toBe(true);
|
|
expect(compiled.querySelector('main#main-content')).toBeTruthy();
|
|
expect(compiled.querySelector('nav[aria-label]')).toBeTruthy();
|
|
expect(compiled.querySelector('a.language-switch[hreflang]')).toBeTruthy();
|
|
expect(compiled.querySelector('footer')).toBeTruthy();
|
|
});
|
|
|
|
it('renders the terminal dock outside .site and keeps the header free of a CV link', async () => {
|
|
const fixture = TestBed.createComponent(App);
|
|
await fixture.whenStable();
|
|
const compiled = fixture.nativeElement as HTMLElement;
|
|
const site = compiled.querySelector('.site');
|
|
const dock = compiled.querySelector('app-terminal-dock');
|
|
const trigger = compiled.querySelector('.terminal-dock-trigger');
|
|
|
|
expect(site).toBeTruthy();
|
|
expect(dock).toBeTruthy();
|
|
expect(site?.contains(dock)).toBe(false);
|
|
expect(compiled.querySelector('header a[href="/cv/CV.pdf"]')).toBeNull();
|
|
expect(compiled.querySelector('header .command-palette-trigger')).toBeNull();
|
|
|
|
const panel = compiled.querySelector('.terminal-dock-panel');
|
|
expect(panel).toBeTruthy();
|
|
expect((trigger as HTMLButtonElement).hasAttribute('hidden')).toBe(true);
|
|
expect(site?.contains(panel)).toBe(false);
|
|
expect(site?.hasAttribute('inert')).toBe(false);
|
|
});
|
|
|
|
it('keeps the server-rendered nav expanded and collapses after hydration on a narrow viewport', async () => {
|
|
TestBed.resetTestingModule();
|
|
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(null);
|
|
mockViewport(false);
|
|
await configureApp();
|
|
|
|
const fixture = TestBed.createComponent(App);
|
|
const shell = fixture.componentInstance as unknown as { navOpen: () => boolean };
|
|
expect(shell.navOpen()).toBe(true);
|
|
|
|
await flush(fixture);
|
|
const button = toggle(fixture.nativeElement);
|
|
expect(button.getAttribute('aria-expanded')).toBe('false');
|
|
expect(button.getAttribute('aria-controls')).toBe('primary-nav');
|
|
expect(button.getAttribute('aria-label')).toBeTruthy();
|
|
expect(fixture.nativeElement.querySelector('.site')?.classList.contains('nav-collapsed')).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it('does not collapse the nav on the server even when the viewport helper would be narrow', async () => {
|
|
TestBed.resetTestingModule();
|
|
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(null);
|
|
mockViewport(false);
|
|
await configureApp([{ provide: PLATFORM_ID, useValue: 'server' }]);
|
|
|
|
const fixture = TestBed.createComponent(App);
|
|
await flush(fixture);
|
|
|
|
expect(toggle(fixture.nativeElement).getAttribute('aria-expanded')).toBe('true');
|
|
expect(fixture.nativeElement.querySelector('.site')?.classList.contains('nav-collapsed')).toBe(
|
|
false,
|
|
);
|
|
});
|
|
|
|
it('closes the nav on navigation when the viewport is narrow', async () => {
|
|
TestBed.resetTestingModule();
|
|
vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(null);
|
|
mockViewport(false);
|
|
await configureApp();
|
|
|
|
const fixture = TestBed.createComponent(App);
|
|
await flush(fixture);
|
|
|
|
const button = toggle(fixture.nativeElement);
|
|
expect(button.getAttribute('aria-expanded')).toBe('false');
|
|
button.click();
|
|
fixture.detectChanges();
|
|
expect(button.getAttribute('aria-expanded')).toBe('true');
|
|
|
|
await TestBed.inject(Router).navigateByUrl('/projekte');
|
|
await flush(fixture);
|
|
|
|
expect(toggle(fixture.nativeElement).getAttribute('aria-expanded')).toBe('false');
|
|
});
|
|
});
|