import { afterEach, describe, expect, it } from 'vitest'; import { ProjectStore } from '@/app/store'; import { createEmptyProject } from '@/domain/model/factory'; import { LEERER_LAGEPLAN, type Lageplan } from '@/domain/geometrie/lageplan'; import type { Project } from '@/domain/model/project'; import { lageplanView } from '@/ui/views/lageplanView'; /** * Der Arbeitsbereich in der Lageplanansicht. * * Geprüft wird das Werkzeug, die Tabelle und die Messkarte - nicht die * Zeichnung selbst; die steht in tests/render/arbeitsbereichBlatt.test.ts und * in der Leinwand, die unter jsdom keinen Zeichenkontext hat. */ const JETZT = new Date('2026-09-11T00:00:00Z'); const KALIBRIERUNG = { von: { x: 0, y: 0 }, bis: { x: 100, y: 0 }, laengeMeter: 100, herkunft: 'gemessen' as const, }; const BAND = { id: 'ab1', name: 'Sperrung Nord', breiteMeter: 4, punkte: [ { x: 0, y: 0 }, { x: 60, y: 0 }, ], }; const HALTLINIE = { id: 'h1', name: 'Ost', art: 'zufahrt' as const, armId: null, von: { x: 70, y: -3 }, bis: { x: 70, y: 3 }, }; function projekt(lageplan: Partial = {}): Project { return { ...createEmptyProject('Baustelle', JETZT, 'einstreifig'), lageplan: { ...LEERER_LAGEPLAN, kalibrierung: KALIBRIERUNG, ...lageplan }, }; } let aufraeumen: () => void = () => {}; function zeichne(project: Project) { const wurzel = document.createElement('div'); document.body.append(wurzel); const store = new ProjectStore(project); const abmelden = lageplanView.render(wurzel, { store, refresh: () => {}, navigate: () => {}, target: null, }); aufraeumen = () => { abmelden(); wurzel.remove(); }; return { wurzel, store }; } function knopf(wurzel: ParentNode, beschriftung: string): HTMLButtonElement | null { return ( [...wurzel.querySelectorAll('button')].find((b) => b.textContent?.trim() === beschriftung) ?? null ); } afterEach(() => { aufraeumen(); document.body.replaceChildren(); }); describe('Das fünfte Werkzeug', () => { it('gibt es einen Knopf "Arbeitsbereich"', () => { const { wurzel } = zeichne(projekt()); expect(knopf(wurzel, 'Arbeitsbereich')).not.toBeNull(); }); it('ist er gesperrt, solange kein Maßstab festgelegt ist', () => { // Ohne Maßstab hat ein Band in Metern keine Breite. const { wurzel } = zeichne({ ...createEmptyProject('Ohne Maßstab', JETZT), lageplan: LEERER_LAGEPLAN, }); expect(knopf(wurzel, 'Arbeitsbereich')?.disabled).toBe(true); }); it('ist er frei, sobald der Maßstab steht', () => { const { wurzel } = zeichne(projekt()); expect(knopf(wurzel, 'Arbeitsbereich')?.disabled).toBe(false); }); it('sagt der Hinweis, dass er nicht mitrechnet', () => { const { wurzel } = zeichne(projekt()); knopf(wurzel, 'Arbeitsbereich')?.click(); const text = wurzel.textContent ?? ''; expect(text).toContain('geht in keine Zwischenzeit'); }); }); describe('Die Tabelle', () => { it('sagt sie, wenn noch nichts gezeichnet ist', () => { const { wurzel } = zeichne(projekt()); expect(wurzel.textContent).toContain('Arbeitsbereiche (0)'); expect(wurzel.textContent).toContain('Noch kein Arbeitsbereich'); }); it('führt sie ein gezeichnetes Band mit Name und Länge', () => { const { wurzel } = zeichne(projekt({ arbeitsbereiche: [BAND] })); expect(wurzel.textContent).toContain('Arbeitsbereiche (1)'); // Der Name steht in einem Eingabefeld und nicht im Text der Seite. const namen = [...wurzel.querySelectorAll('input')].map((i) => i.value); expect(namen).toContain('Sperrung Nord'); // 60 m bei einem Meter je Bildpunkt. expect(wurzel.textContent).toContain('60,0 m'); }); it('löscht ein Band auf Knopfdruck', () => { const { wurzel, store } = zeichne(projekt({ arbeitsbereiche: [BAND] })); const loeschen = [...wurzel.querySelectorAll('button')].find( (b) => b.getAttribute('aria-label') === 'Arbeitsbereich "Sperrung Nord" löschen', ); expect(loeschen, 'Kein Löschknopf').not.toBeUndefined(); loeschen?.click(); expect(store.getProject().lageplan.arbeitsbereiche).toEqual([]); }); }); describe('Die Messung unter der Tabelle', () => { it('nennt sie den Abstand jeder Haltlinie', () => { // Haltlinie bei x = 70, Band endet bei x = 60: 10 m. const { wurzel } = zeichne(projekt({ arbeitsbereiche: [BAND], haltlinien: [HALTLINIE] })); const text = wurzel.textContent ?? ''; expect(text).toContain('Abstand der Haltlinien zum nächsten Ende'); expect(text).toContain('10,0 m'); }); it('sagt sie, wenn eine Haltlinie im Arbeitsbereich liegt', () => { const drin = { ...HALTLINIE, von: { x: 30, y: -3 }, bis: { x: 30, y: 3 } }; const { wurzel } = zeichne(projekt({ arbeitsbereiche: [BAND], haltlinien: [drin] })); expect(wurzel.textContent).toContain('liegt im Arbeitsbereich'); }); it('schweigt sie ohne Haltlinien', () => { const { wurzel } = zeichne(projekt({ arbeitsbereiche: [BAND] })); expect(wurzel.textContent).not.toContain('Abstand der Haltlinien'); }); }); describe('Zeichnen mit der Tastatur', () => { /** Ein Bild, damit die Zeichenfläche eine Größe hat. */ const BILD = { datenUrl: 'data:image/png;base64,AAAA', breite: 320, hoehe: 240, herkunft: 'datei' as const, geladenAm: '2026-09-11T00:00:00.000Z', }; function taste(leinwand: Element, key: string, male = 1): void { for (let i = 0; i < male; i += 1) { leinwand.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true })); } } function baue() { const { wurzel, store } = zeichne(projekt({ bild: BILD })); const leinwand = wurzel.querySelector('canvas'); if (leinwand === null) throw new Error('Keine Zeichenfläche'); return { wurzel, store, leinwand }; } it('legt ein Band aus zwei Punkten an', () => { const { wurzel, store, leinwand } = baue(); knopf(wurzel, 'Arbeitsbereich')?.click(); taste(leinwand, 'ArrowRight', 4); taste(leinwand, ' '); taste(leinwand, 'ArrowRight', 8); taste(leinwand, ' '); taste(leinwand, 'Enter'); const bereiche = store.getProject().lageplan.arbeitsbereiche; expect(bereiche).toHaveLength(1); expect(bereiche[0]?.punkte).toHaveLength(2); expect(bereiche[0]?.name).toBe('Arbeitsbereich 1'); }); it('rastet der erste Punkt nicht auf eine Haltlinie ein', () => { // Die Arbeitsstelle beginnt an der Absperrung, nicht an einem Querschnitt // der Fahrbahn. Rastete sie ein, richtete sich das Band an der Haltlinie // aus - und der Bearbeiter danach die Haltlinie am Band. const { wurzel } = zeichne(projekt({ bild: BILD, haltlinien: [HALTLINIE] })); const leinwand = wurzel.querySelector('canvas')!; knopf(wurzel, 'Arbeitsbereich')?.click(); taste(leinwand, ' '); const ansage = wurzel.textContent ?? ''; expect(ansage).toContain('Erster Punkt des Arbeitsbereichs gesetzt'); expect(ansage).not.toContain('eingerastet'); }); it('nimmt die Rückschritttaste den letzten Punkt zurück', () => { const { wurzel, store, leinwand } = baue(); knopf(wurzel, 'Arbeitsbereich')?.click(); taste(leinwand, ' '); taste(leinwand, 'ArrowRight', 4); taste(leinwand, ' '); taste(leinwand, 'Backspace'); taste(leinwand, 'Enter'); // Ein Punkt reicht nicht - es entsteht nichts. expect(store.getProject().lageplan.arbeitsbereiche).toEqual([]); }); it('verwirft der Werkzeugwechsel den halben Zug', () => { const { wurzel, store, leinwand } = baue(); knopf(wurzel, 'Arbeitsbereich')?.click(); taste(leinwand, ' '); knopf(wurzel, 'Ansehen')?.click(); knopf(wurzel, 'Arbeitsbereich')?.click(); taste(leinwand, 'ArrowRight', 4); taste(leinwand, ' '); taste(leinwand, 'Enter'); // Der Punkt von vorhin ist fort, also fehlt der zweite. expect(store.getProject().lageplan.arbeitsbereiche).toEqual([]); }); it('beendet der Doppelklick das Band ebenfalls', () => { const { wurzel, store, leinwand } = baue(); knopf(wurzel, 'Arbeitsbereich')?.click(); taste(leinwand, ' '); taste(leinwand, 'ArrowRight', 4); taste(leinwand, ' '); leinwand.dispatchEvent(new MouseEvent('dblclick', { bubbles: true })); expect(store.getProject().lageplan.arbeitsbereiche).toHaveLength(1); }); it('bricht Escape ab und schaltet auf Ansehen', () => { const { wurzel, store, leinwand } = baue(); knopf(wurzel, 'Arbeitsbereich')?.click(); taste(leinwand, ' '); taste(leinwand, 'Escape'); taste(leinwand, 'Enter'); expect(store.getProject().lageplan.arbeitsbereiche).toEqual([]); }); }); describe('Die übrigen Tabellen bleiben stehen', () => { it('stehen Haltlinien und Fahrlinien weiterhin daneben', () => { // Die drei Karten hängen an EINEM render-Aufruf; ein zweiter löschte die // beiden ersten. const { wurzel } = zeichne(projekt({ arbeitsbereiche: [BAND], haltlinien: [HALTLINIE] })); const text = wurzel.textContent ?? ''; expect(text).toContain('Haltlinien (1)'); expect(text).toContain('Fahrlinien (0)'); expect(text).toContain('Arbeitsbereiche (1)'); }); });