136 lines
6.2 KiB
TypeScript
136 lines
6.2 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { docxToHtml } from "../src/optional/docx.ts";
|
|
import { readWorkbook } from "../src/optional/sheet.ts";
|
|
import { listZip } from "../src/optional/zip.ts";
|
|
import { zipSync, strToU8 } from "fflate";
|
|
|
|
/**
|
|
* These exercise the parsers themselves. The browser wiring — one runtime asset
|
|
* per installed package, fetched on first use — is covered in component.test.ts.
|
|
*/
|
|
describe("zip listing", () => {
|
|
const archive = zipSync({
|
|
"docs/readme.md": strToU8("hello world\n".repeat(20)),
|
|
"src/index.ts": strToU8("export const value = 1;\n"),
|
|
});
|
|
|
|
test("entries are listed with their uncompressed sizes", async () => {
|
|
const entries = await listZip(archive);
|
|
expect(entries.map((entry) => entry.name).sort()).toEqual(["docs/readme.md", "src/index.ts"]);
|
|
expect(entries.find((entry) => entry.name === "docs/readme.md")!.size).toBe(240);
|
|
});
|
|
|
|
test("a corrupt archive rejects rather than resolving empty", async () => {
|
|
await expect(listZip(new Uint8Array([0x50, 0x4b, 3, 4, 9, 9, 9]))).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
describe("workbook reading", () => {
|
|
// Built with the same shape as examples/basic-app/public/viewer/sample.xlsx.
|
|
const NS = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
|
|
const R = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
const sheet = (rows: (string | number)[][]) =>
|
|
`<?xml version="1.0"?><worksheet xmlns="${NS}"><sheetData>${rows
|
|
.map(
|
|
(row, rowIndex) =>
|
|
`<row r="${rowIndex + 1}">${row
|
|
.map((value, index) => {
|
|
const ref = `${String.fromCharCode(65 + index)}${rowIndex + 1}`;
|
|
return typeof value === "number"
|
|
? `<c r="${ref}"><v>${value}</v></c>`
|
|
: `<c r="${ref}" t="inlineStr"><is><t>${value}</t></is></c>`;
|
|
})
|
|
.join("")}</row>`,
|
|
)
|
|
.join("")}</sheetData></worksheet>`;
|
|
|
|
const workbook = zipSync({
|
|
"[Content_Types].xml": strToU8(
|
|
'<?xml version="1.0"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">' +
|
|
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>' +
|
|
'<Default Extension="xml" ContentType="application/xml"/>' +
|
|
'<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>' +
|
|
'<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>' +
|
|
'<Override PartName="/xl/worksheets/sheet2.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>' +
|
|
"</Types>",
|
|
),
|
|
"_rels/.rels": strToU8(
|
|
`<?xml version="1.0"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="${R}/officeDocument" Target="xl/workbook.xml"/></Relationships>`,
|
|
),
|
|
"xl/workbook.xml": strToU8(
|
|
`<?xml version="1.0"?><workbook xmlns="${NS}" xmlns:r="${R}"><sheets>` +
|
|
'<sheet name="Revenue" sheetId="1" r:id="rId1"/>' +
|
|
'<sheet name="Headcount" sheetId="2" r:id="rId2"/>' +
|
|
"</sheets></workbook>",
|
|
),
|
|
"xl/_rels/workbook.xml.rels": strToU8(
|
|
`<?xml version="1.0"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">` +
|
|
`<Relationship Id="rId1" Type="${R}/worksheet" Target="worksheets/sheet1.xml"/>` +
|
|
`<Relationship Id="rId2" Type="${R}/worksheet" Target="worksheets/sheet2.xml"/>` +
|
|
"</Relationships>",
|
|
),
|
|
"xl/worksheets/sheet1.xml": strToU8(
|
|
sheet([
|
|
["Region", "Q1"],
|
|
["North", 15400],
|
|
["South", 11200],
|
|
]),
|
|
),
|
|
"xl/worksheets/sheet2.xml": strToU8(
|
|
sheet([
|
|
["Team", "People"],
|
|
["Engineering", 42],
|
|
]),
|
|
),
|
|
});
|
|
|
|
test("every sheet comes back named, with a header and string cells", () => {
|
|
const sheets = readWorkbook(workbook);
|
|
expect(sheets.map((item) => item.name)).toEqual(["Revenue", "Headcount"]);
|
|
expect(sheets[0]!.header).toEqual(["Region", "Q1"]);
|
|
expect(sheets[0]!.rows).toEqual([
|
|
["North", "15400"],
|
|
["South", "11200"],
|
|
]);
|
|
expect(sheets[1]!.rows).toEqual([["Engineering", "42"]]);
|
|
});
|
|
});
|
|
|
|
describe("document conversion", () => {
|
|
const W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
const R = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
const paragraph = (text: string, style?: string) =>
|
|
`<w:p>${style ? `<w:pPr><w:pStyle w:val="${style}"/></w:pPr>` : ""}` +
|
|
`<w:r><w:t xml:space="preserve">${text}</w:t></w:r></w:p>`;
|
|
const document = zipSync({
|
|
"[Content_Types].xml": strToU8(
|
|
'<?xml version="1.0"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">' +
|
|
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>' +
|
|
'<Default Extension="xml" ContentType="application/xml"/>' +
|
|
'<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>' +
|
|
"</Types>",
|
|
),
|
|
"_rels/.rels": strToU8(
|
|
`<?xml version="1.0"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="${R}/officeDocument" Target="word/document.xml"/></Relationships>`,
|
|
),
|
|
"word/document.xml": strToU8(
|
|
`<?xml version="1.0"?><w:document xmlns:w="${W}"><w:body>` +
|
|
paragraph("Quarterly summary", "Heading1") +
|
|
paragraph("Bodies of text survive; page layout does not.") +
|
|
"</w:body></w:document>",
|
|
),
|
|
});
|
|
|
|
test("a docx becomes semantic HTML", async () => {
|
|
const html = await docxToHtml(document);
|
|
expect(html).toContain("<h1>Quarterly summary</h1>");
|
|
expect(html).toContain("Bodies of text survive");
|
|
// Semantic output only: no document chrome, and nothing executable.
|
|
expect(html).not.toContain("<script");
|
|
});
|
|
|
|
test("a file that is not a docx rejects", async () => {
|
|
await expect(docxToHtml(strToU8("not a document"))).rejects.toThrow();
|
|
});
|
|
});
|