1import { describe, expect, it } from 'vitest'2import { ErdSyntaxError, erdToDot, parseErd } from '../src/languages/erd'34describe('ERD source lowering', () => {5 it('parses the repository schema and emits entities, fields, and cardinalities', () => {6 const doc = parseErd(`[Person]\n*name\nheight\nweight\n+birth_location_id\n\n[Location]\n*id\ncity\nstate\ncountry\n\nPerson *--1 Location`)7 expect(doc.entities).toHaveLength(2)8 expect(doc.entities[0]!.attributes[0]).toMatchObject({ field: 'name', pk: true, fk: false })9 expect(doc.entities[0]!.attributes[3]).toMatchObject({ field: 'birth_location_id', pk: false, fk: true })10 expect(doc.relations[0]).toMatchObject({ entity1: 'Person', entity2: 'Location', card1: 'zero-plus', card2: 'one' })11 const dot = erdToDot(doc)12 expect(dot).toContain('"Person" [label=<')13 expect(dot).toContain('taillabel=<0..N>')14 expect(dot).toContain('headlabel=<1>')15 })1617 it('supports quoted identifiers, comments, directives, labels, and all cardinalities', () => {18 const doc = parseErd(`# comment\ntitle { label: "People & places", size: "24" }\nheader { label: "header", color: "navy", font: "Georgia", size: "18" }\nentity { bgcolor: "#fff", border: "2", border-color: "red", cellborder: "1", cellspacing: "2", cellpadding: "3", color: "black", font: "Arial", size: "11" }\nrelationship { label: "lives at", color: "gray", font: "Arial", size: "10" }\n\n["People & Co"]\n*"display name" { label: "Name", color: "blue", font: "Verdana", size: "9", text-alignment: "center" }\n[Place]\n?code\n\n"People & Co" ?--+ Place # trailing comment`)19 expect(doc.titleOptions).toContainEqual({ name: 'label', value: 'People & places' })20 expect(doc.relations[0]).toMatchObject({ card1: 'zero-one', card2: 'one-plus' })21 const dot = erdToDot(doc)22 expect(dot).toContain('People & places')23 expect(dot).toContain('People & Co')24 expect(dot).toContain('{0,1}')25 expect(dot).toContain('headlabel=<')26 expect(dot).toContain('BGCOLOR="#fff"')27 expect(dot).toContain('COLOR="red"')28 expect(dot).toContain('POINT-SIZE="10"')29 expect(dot).toContain(' [Name]')30 expect(dot).toContain('graph [label=<<FONT POINT-SIZE="24">People & places</FONT>>, labelloc=t, labeljust=l];')31 })3233 it('retains upstream defaults when a global directive overrides one property', () => {34 const dot = erdToDot(parseErd('header { color: "navy" }\nentity { bgcolor: "#fff" }\n[A]\nid'))35 expect(dot).toContain('POINT-SIZE="16"')36 expect(dot).toContain('FACE="Helvetica"')37 expect(dot).toContain('CELLBORDER="1"')38 expect(dot).toContain('CELLPADDING="4"')39 })4041 it('supports upstream multiline options, comments, and trailing commas', () => {42 const doc = parseErd(`entity {43 bgcolor: "#ececfc", # preserve the upstream comment form44 size: "20",45 }46 [Person]47 name {48 label: "string",49 color: "#3366ff",50 }`)51 const dot = erdToDot(doc)52 expect(dot).toContain('BGCOLOR="#ececfc"')53 expect(dot).toContain('POINT-SIZE="20"')54 expect(dot).toContain('COLOR="#3366ff"')55 expect(dot).toContain(' [string]')56 })5758 it('treats directive names after the preamble as ordinary attributes', () => {59 const doc = parseErd('[A]\nid\nheader { color: "red" }\n[B]\nid')60 expect(doc.headerOptions).toEqual([])61 expect(doc.entities[0]!.attributes[1]).toMatchObject({ field: 'header' })62 expect(erdToDot(doc)).toContain('<FONT COLOR="red">header</FONT>')63 })6465 it('escapes DOT and HTML metacharacters in labels', () => {66 const dot = erdToDot(parseErd('["A]\\B"]\n"x < y & z"'))67 expect(dot).toContain('"A]\\\\B"')68 expect(dot).toContain('x < y & z')69 })7071 it('applies supported HTML table styling to attributes', () => {72 const dot = erdToDot(parseErd('[A]\nfield { bgcolor: "#ececfc", border: "2", border-color: "red", cellborder: "3", cellspacing: "4", cellpadding: "5" }'))73 expect(dot).toContain('BGCOLOR="#ececfc"')74 expect(dot).toContain('BORDER="2"')75 expect(dot).toContain('COLOR="red"')76 expect(dot).toContain('CELLBORDER="3"')77 expect(dot).toContain('CELLSPACING="4"')78 expect(dot).toContain('CELLPADDING="5"')79 })8081 it('rejects malformed input and filesystem-style configuration', () => {82 expect(() => parseErd('id')).toThrow(/attribute comes before first entity/)83 expect(() => parseErd('[A]\nfield { config: "/tmp/erd.yaml" }')).toThrow(/option 'config' does not exist/)84 expect(() => parseErd('[A]\nfoo\nA *--1 Missing')).toThrow(/unknown entity 'Missing'/)85 expect(() => parseErd('[A]\nfoo { label: "unterminated }')).toThrow(ErdSyntaxError)86 expect(() => parseErd('[A]\nfoo { border: "256" }')).toThrow(/0 to 255/)87 expect(() => parseErd('[A]\nfoo { size: "0" }')).toThrow(/positive finite/)88 expect(() => parseErd('[A]\nfoo { text-alignment: "diagonal" }')).toThrow(/text-alignment/)89 expect(() => parseErd('[A]\nfoo { color: "red"')).toThrow(/unterminated options/)90 })91})