/** * Task G3 – Titelblock: Blockdefinition mit attributierten Texten * (Projekt/Datum/Maßstab/Autor) + auto-insert als Layout-Frame. * * Die Text-Elemente tragen tag/attdef-Metadaten (F5/F6-Konvention), * sodass der Titelblock als DXF ATTDEF/ATTRIB roundtripped und das * spätere PropertiesPanel (D12) das Formular daraus bauen kann. */ import { describe, it, expect } from 'vitest'; import { CADDocument } from '../src/kernel/document/CADDocument'; import { createInMemoryDoc } from './helpers/inMemoryDoc'; import { createTitleBlockElements, insertTitleBlockForLayout, TITLE_BLOCK_NAME, type TitleBlockMeta, } from '../src/services/titleBlockService'; import type { LayoutDefinition } from '../src/types/cad.types'; const META: TitleBlockMeta = { project: 'Musterhalle A4', date: '2026-08-29', scale: '1:100', author: 'L. Admin', }; function mkDoc(): CADDocument { const { ydoc } = createInMemoryDoc(); return new CADDocument(ydoc); } const LAYOUT: LayoutDefinition = { id: 'layout-1', name: 'A4 Plan', viewport: { center: { x: 0, y: 0 }, scale: 1 }, }; describe('G3: createTitleBlockElements', () => { it('Rahmen + Titel + 4 attributierte Texte mit Standard-Tags', () => { const els = createTitleBlockElements(META); const rects = els.filter((e) => e.type === 'rect'); const texts = els.filter((e) => e.type === 'text'); expect(rects.length).toBeGreaterThanOrEqual(1); expect(texts.length).toBe(5); // Titel + PROJEKT + DATUM + MASSSTAB + AUTOR const tags = texts.map((t) => t.properties.tag).filter(Boolean); expect(tags).toEqual(['PROJEKT', 'DATUM', 'MASSSTAB', 'AUTOR']); // Nur die attributierten Texte tragen attdef (Titel-Label bewusst nicht): const attrTexts = texts.filter((t) => t.properties.tag); expect(attrTexts.every((t) => t.properties.attdef === true)).toBe(true); // Werte stehen im Text const proj = texts.find((t) => t.properties.tag === 'PROJEKT'); expect(String(proj!.properties.text)).toContain('Musterhalle A4'); const autor = texts.find((t) => t.properties.tag === 'AUTOR'); expect(String(autor!.properties.text)).toContain('L. Admin'); }); it('Texte folgen der tag=Wert-Konvention des F5-ATTDEF-Parsers', () => { const els = createTitleBlockElements(META); const scale = els.find((e) => e.properties.tag === 'MASSSTAB')!; expect(String(scale.properties.text)).toBe('MASSSTAB=1:100'); expect(scale.properties.prompt).toBeTruthy(); }); it('leere Meta erzeugt Füllzeichen statt undefined', () => { const els = createTitleBlockElements({}); const proj = els.find((e) => e.properties.tag === 'PROJEKT')!; expect(String(proj.properties.text)).toBe('PROJEKT=—'); }); }); describe('G3: insertTitleBlockForLayout (auto-insert als Frame)', () => { it('erzeugt Blockdefinition im Doc und setzt frameBlockRef im Layout', () => { const doc = mkDoc(); doc.addLayout(LAYOUT); const blockId = insertTitleBlockForLayout(doc, 'layout-1', META); expect(typeof blockId).toBe('string'); const layout = doc.getLayout('layout-1'); expect(layout!.frameBlockRef).toBe(blockId); const block = doc.getBlock(blockId); expect(block).toBeDefined(); expect(block!.name).toBe(TITLE_BLOCK_NAME); expect(block!.category).toBe('frame'); expect(block!.elements.length).toBe(6); // 1 rect + 5 text }); it('vorhandener frameBlockRef wird ersetzt (kein Duplikat-Block je Re-Insert)', () => { const doc = mkDoc(); doc.addLayout(LAYOUT); const first = insertTitleBlockForLayout(doc, 'layout-1', META); const second = insertTitleBlockForLayout(doc, 'layout-1', { ...META, project: 'Neu' }); expect(second).not.toBe(first); expect(doc.getLayout('layout-1')!.frameBlockRef).toBe(second); }); it('Layout ohne frameBlockRef bleibt unverändert bei fehlendem Layout (throws)', () => { const doc = mkDoc(); expect(() => insertTitleBlockForLayout(doc, 'gibts-nicht', META)).toThrow(); }); });