import { describe, expect, it } from "vitest"; import { collectOdfTransferables, parseOdp, parseOds, parseOdt, parseOpenDocument, } from "../../../src/office/odf"; import { makeOdf, odpContent, odsContent, odtContent, XMLNS } from "./fixtures"; const PIXEL = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]); describe("OpenDocument normalized models", () => { it("parses ODT metadata, styles, rich text, lists, tables, images, and notes", () => { const content = odtContent(` A heading Helloworld! 1 Footnote body First Second Cell PixelA tiny pixel `); const meta = ` Test documentAda localoffice `; const styles = ` `; const result = parseOdt( makeOdf("odt", content, { extra: { "Pictures/pixel.png": PIXEL }, meta, styles, }), { fileName: "example.odt" }, ); expect(result.format).toBe("odt"); expect(result.metadata).toMatchObject({ title: "Test document", creator: "Ada", keywords: ["local", "office"], statistics: { wordCount: 8, pageCount: 1 }, }); expect(result.styles).toEqual( expect.arrayContaining([ expect.objectContaining({ name: "Body", family: "paragraph", properties: { "paragraph-properties.fo:text-align": "start" }, }), expect.objectContaining({ name: "InlineBold", family: "text" }), ]), ); expect(result.blocks[0]).toMatchObject({ kind: "heading", level: 2, text: "A heading", }); expect(result.blocks[1]).toMatchObject({ kind: "paragraph", text: expect.stringContaining("Hello world\t!"), runs: expect.arrayContaining([ expect.objectContaining({ text: "world", styleName: "InlineBold" }), ]), }); expect(result.blocks[2]).toMatchObject({ kind: "list", items: [{}, {}] }); expect(result.blocks[3]).toMatchObject({ kind: "table", name: "Data", rows: [{ rowRepeat: 2, cells: [{ columnSpan: 2 }, { covered: true }] }], }); expect(result.blocks[4]).toMatchObject({ kind: "image", assetId: "asset-1", alt: "A tiny pixel", title: "Pixel", width: { value: 3, unit: "cm" }, }); expect(result.notes).toHaveLength(1); expect(result.notes[0]).toMatchObject({ id: "note-1", citation: "1" }); expect(result.assets[0]).toMatchObject({ id: "asset-1", path: "Pictures/pixel.png", mediaType: "image/png", byteLength: PIXEL.byteLength, }); expect(new Uint8Array(result.assets[0]!.data)).toEqual(PIXEL); expect(collectOdfTransferables(result)).toEqual([result.assets[0]!.data]); }); it("parses ODS sheets, typed values, formulas, repeats, merges, and annotations", () => { const content = odsContent(` Name 42.50 Cached value only TRUE `); const result = parseOds(makeOdf("ods", content)); const sheet = result.sheets[0]!; expect(sheet).toMatchObject({ name: "Budget", styleName: "SheetStyle", protected: true, expandedRowCount: 2, expandedCellCount: 10, }); expect(sheet.rows[0]).toMatchObject({ index: 0, rowRepeat: 2, styleName: "RowStyle", }); expect(sheet.rows[0]!.cells).toEqual( expect.arrayContaining([ expect.objectContaining({ column: 0, value: "Name", display: "Name" }), expect.objectContaining({ column: 1, columnRepeat: 2, value: 42.5, formula: "of:=SUM([.B1:.C1])", display: "42.50", annotation: [ expect.objectContaining({ kind: "paragraph", text: "Cached value only", }), ], }), expect.objectContaining({ column: 3, columnSpan: 2, rowSpan: 2, value: true, }), expect.objectContaining({ column: 4, kind: "covered" }), ]), ); }); it("trims office-suite full-grid tail repeats without expanding them", () => { const content = odsContent(` Only value `); const sheet = parseOds(makeOdf("ods", content)).sheets[0]!; expect(sheet.expandedRowCount).toBe(1); expect(sheet.expandedCellCount).toBe(1); expect(sheet.rows).toHaveLength(1); expect(sheet.rows[0]!.cells).toHaveLength(1); }); it("parses ODP slides, coordinates, text, shapes, images, tables, groups, and notes", () => { const content = odpContent(` Local Office Box A1 Speaker note `); const result = parseOdp( makeOdf("odp", content, { extra: { "Pictures/pixel.png": PIXEL } }), ); const slide = result.slides[0]!; expect(slide).toMatchObject({ name: "Opening", styleName: "SlideStyle", masterPageName: "Master", layoutName: "TitleLayout", notes: [ expect.objectContaining({ kind: "paragraph", text: "Speaker note" }), ], }); expect(slide.shapes).toEqual( expect.arrayContaining([ expect.objectContaining({ id: "title", type: "text-box", x: { value: 1, unit: "cm", raw: "1cm" }, blocks: [ expect.objectContaining({ kind: "paragraph", text: "Local Office", }), ], }), expect.objectContaining({ type: "rectangle", name: "Accent" }), expect.objectContaining({ id: "picture", type: "image", image: expect.objectContaining({ assetId: "asset-1" }), }), expect.objectContaining({ id: "grid", type: "table", table: expect.objectContaining({ name: "Slide table" }), }), expect.objectContaining({ id: "group", type: "group", children: [ expect.objectContaining({ id: "circle", type: "ellipse" }), ], }), ]), ); }); it("auto-detects formats without parsing the package twice and warns on a missing manifest", () => { const document = parseOpenDocument( makeOdf("odt", odtContent("Minimal"), { manifest: false, }), ); expect(document.format).toBe("odt"); expect(document.warnings).toContain("Package has no META-INF/manifest.xml"); }); it("retains page geometry, default styles, comments and spreadsheet column metadata", () => { const styles = ` `; const odt = parseOdt( makeOdf( "odt", odtContent( `ReviewedAda2026-09-01Looks good`, ), { styles }, ), ); expect(odt.pageWidth).toMatchObject({ value: 21, unit: "cm" }); expect(odt.styles).toEqual( expect.arrayContaining([ expect.objectContaining({ isDefault: true, family: "paragraph" }), ]), ); expect(odt.annotations[0]).toMatchObject({ id: "c1", creator: "Ada", createdAt: "2026-09-01", blocks: [expect.objectContaining({ text: "Looks good" })], }); const ods = parseOds( makeOdf( "ods", odsContent(` x `), ), ); expect(ods.sheets[0]).toMatchObject({ visibility: "collapse", columns: [ { index: 0, repeat: 3, styleName: "Wide", defaultCellStyleName: "Money", }, ], rows: [expect.objectContaining({ defaultCellStyleName: "RowDefault" })], }); }); });