import { describe, expect, it } from "vitest"; import { buildChartXml, buildCellXml, buildHyperlinksXml, buildMergeCellsXml, createCellStyleRegistry, buildWorksheetRelationshipsXml, buildWorksheetXml, exportXlsxBlob, importXlsxFile, chartDataPoints, chartXmlToSheetChart, workbookRelationshipTargets, workbookSheetRefs, worksheetXmlToCells, worksheetXmlToCellFormats, worksheetXmlToHyperlinks, worksheetXmlToMergedCells, xlsxStylesXmlToCellFormatTable, xlsxCellToText } from "./excellOffice"; describe("excellOffice OOXML helpers", () => { it("сохраняет формулу при импорте как строку с '='", () => { expect(xlsxCellToText({ r: "C1", f: "SUM(A1:B1)", v: "3" }, [])).toBe("=SUM(A1:B1)"); expect(xlsxCellToText({ r: "C2", f: { "#text": "A2*2" }, v: "8" }, [])).toBe("=A2*2"); }); it("читает shared string, inline string, число и boolean", () => { expect(xlsxCellToText({ r: "A1", t: "s", v: "1" }, ["Первый", "Второй"])).toBe("Второй"); expect(xlsxCellToText({ r: "A2", t: "inlineStr", is: { t: "Текст" } }, [])).toBe("Текст"); expect(xlsxCellToText({ r: "A3", v: "12500" }, [])).toBe("12500"); expect(xlsxCellToText({ r: "A4", t: "b", v: "1" }, [])).toBe("true"); }); it("записывает формулу в тег ", () => { expect(buildCellXml("c1", "=SUM(A1:B1)")).toBe('SUM(A1:B1)'); }); it("собирает XML листа с отсортированными ячейками", () => { const xml = buildWorksheetXml({ B2: "200", A1: "Статья", C2: "=B2*2" }); expect(xml).toContain('Статья'); expect(xml).toContain('200B2*2'); }); it("сохраняет merged cells в XML листа", () => { const xml = buildWorksheetXml({ A1: "Заголовок" }, [{ start: "A1", end: "C2" }]); expect(buildMergeCellsXml([{ start: "C2", end: "A1" }, { start: "A1", end: "C2" }])).toBe( '' ); expect(xml).toContain(''); }); it("сохраняет внешние гиперссылки XLSX в XML листа и relationships", () => { const hyperlinks = { A1: "https://example.com/report", B2: "javascript:alert(1)" }; const xml = buildWorksheetXml({ A1: "Отчет", B2: "Скрипт" }, [], hyperlinks); const relationshipsXml = buildWorksheetRelationshipsXml(hyperlinks); expect(buildHyperlinksXml(hyperlinks)).toBe(''); expect(xml).toContain(''); expect(relationshipsXml).toContain('Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"'); expect(relationshipsXml).toContain('Target="https://example.com/report"'); expect(relationshipsXml).toContain('TargetMode="External"'); expect(relationshipsXml).not.toContain("javascript:alert"); }); it("сохраняет базовое форматирование ячеек XLSX в XML листа", () => { const cellFormats = { A1: { bold: true, fillColor: "fff2cc" }, B2: { italics: true, underline: true }, C3: { fillColor: "bad-color" } }; const registry = createCellStyleRegistry([cellFormats]); const xml = buildWorksheetXml({ A1: "Итог", B2: "Комментарий", C3: "Без стиля" }, [], {}, cellFormats, registry); const emptyCellXml = buildWorksheetXml({}, [], {}, { C3: { underline: true } }); expect(xml).toContain('Итог'); expect(xml).toContain('Комментарий'); expect(xml).toContain('Без стиля'); expect(emptyCellXml).toContain(''); }); it("читает базовое форматирование ячеек XLSX из styles.xml и XML листа", () => { const styleTable = xlsxStylesXmlToCellFormatTable({ styleSheet: { numFmts: { numFmt: { numFmtId: "164", formatCode: "dd.mm.yyyy" } }, fonts: { font: [{}, { b: {}, i: {}, u: {} }] }, fills: { fill: [{}, {}, { patternFill: { fgColor: { rgb: "FFFFF2CC" } } }] }, cellXfs: { xf: [{ fontId: "0", fillId: "0" }, { fontId: "1", fillId: "2", numFmtId: "164" }, { fontId: "0", fillId: "0", numFmtId: "14" }] } } }); expect(styleTable[1]).toEqual({ bold: true, italics: true, underline: true, fillColor: "FFF2CC", numberFormat: "dd.mm.yyyy" }); expect(styleTable[2]).toEqual({ numberFormat: "m/d/yy" }); expect( worksheetXmlToCellFormats( { worksheet: { sheetData: { row: { r: "1", c: [{ r: "A1", s: "1", t: "inlineStr", is: { t: "Итог" } }] } } } }, styleTable ) ).toEqual({ A1: { bold: true, italics: true, underline: true, fillColor: "FFF2CC", numberFormat: "dd.mm.yyyy" } }); }); it("преобразует XML листа в модель QExcell", () => { const cells = worksheetXmlToCells( { worksheet: { sheetData: { row: { r: "1", c: [ { r: "A1", t: "inlineStr", is: { t: "Статья" } }, { r: "B1", v: "1000" }, { r: "C1", f: "B1*2" } ] } } } }, [] ); expect(cells).toEqual({ A1: "Статья", B1: "1000", C1: "=B1*2" }); }); it("читает merged cells из XML листа", () => { expect( worksheetXmlToMergedCells({ worksheet: { mergeCells: { mergeCell: [{ ref: "C2:A1" }, { ref: "D4:D4" }, { ref: "bad" }] } } }) ).toEqual([{ start: "A1", end: "C2" }]); }); it("читает внешние гиперссылки XLSX из XML листа и relationships", () => { expect( worksheetXmlToHyperlinks( { worksheet: { hyperlinks: { hyperlink: [ { ref: "A1", "r:id": "rIdLink" }, { ref: "B2", "r:id": "rIdUnsafe" }, { ref: "bad", "r:id": "rIdLink" } ] } } }, { Relationships: { Relationship: [ { Id: "rIdLink", Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink", Target: "https://example.com/report", TargetMode: "External" }, { Id: "rIdUnsafe", Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink", Target: "javascript:alert(1)", TargetMode: "External" } ] } } ) ).toEqual({ A1: "https://example.com/report" }); }); it("читает ссылки листов и relationships из workbook XML", () => { expect( workbookSheetRefs({ workbook: { sheets: { sheet: { name: "Бюджет", sheetId: "7", "r:id": "rId3" } } } }) ).toEqual([{ id: "sheet-7", name: "Бюджет", relationshipId: "rId3" }]); expect( workbookRelationshipTargets({ Relationships: { Relationship: [{ Id: "rId3", Target: "worksheets/sheet7.xml" }] } }) ).toEqual({ rId3: "worksheets/sheet7.xml" }); }); it("создает OOXML chart part для диаграммы XLSX", () => { const xml = buildChartXml( { id: "chart-1", type: "bar", title: "Итого", labelRange: "A2:A4", valueRange: "E2:E4" }, "Бюджет", { A2: "Базовый", A3: "Команды", A4: "Корпоративный", E2: "43600", E3: "27200", E4: "64800" } ); expect(xml).toContain(""); expect(xml).toContain("'Бюджет'!$A$2:$A$4"); expect(xml).toContain("'Бюджет'!$E$2:$E$4"); expect(xml).toContain("Базовый"); expect(xml).toContain("43600"); }); it("читает базовую диаграмму из chart XML", () => { expect( chartXmlToSheetChart( { "c:chartSpace": { "c:chart": { "c:title": { "c:tx": { "c:rich": { "a:p": { "a:r": { "a:t": "Продажи" } } } } }, "c:plotArea": { "c:lineChart": { "c:ser": { "c:cat": { "c:strRef": { "c:f": "'Бюджет'!$A$2:$A$4" } }, "c:val": { "c:numRef": { "c:f": "'Бюджет'!$E$2:$E$4" } } } } } } } }, "Бюджет", 0 ) ).toEqual({ id: "xlsx-chart-1", type: "line", title: "Продажи", labelRange: "A2:A4", valueRange: "E2:E4" }); }); it("готовит точки диаграммы из диапазонов листа", () => { expect( chartDataPoints( { id: "chart-1", type: "bar", title: "Итого", labelRange: "A2:A3", valueRange: "E2:E3" }, { A2: "Базовый", A3: "Команды", E2: "=SUM(B2:D2)", B2: "10", C2: "20", D2: "30", E3: "25" } ) ).toEqual([ { label: "Базовый", value: 60 }, { label: "Команды", value: 25 } ]); }); it("сохраняет и импортирует несколько листов с дальними ячейками", async () => { const blob = await exportXlsxBlob({ id: "workbook", title: "Бюджет.xlsx", updatedAt: "2026-05-31T00:00:00.000Z", activeSheet: "sheet-1", sheets: [ { id: "sheet-1", name: "Бюджет", cells: { A1: "Статья", AA20: "7", AB20: "8", AC20: "=SUM(AA20:AB20)", D20: "44927", E20: "0.25" }, mergedCells: [{ start: "A1", end: "C1" }], hyperlinks: { A1: "https://example.com/budget" }, cellFormats: { A1: { bold: true, fillColor: "FFF2CC" }, AC20: { underline: true }, D20: { numberFormat: "dd.mm.yyyy" }, E20: { numberFormat: "0%" } }, charts: [ { id: "chart-budget", type: "bar", title: "Итого", labelRange: "A2:A3", valueRange: "AA20:AB20" } ] }, { id: "sheet-2", name: "Справочник", cells: { B2: "Проект" } } ] }); const imported = await importXlsxFile(new File([blob], "Бюджет.xlsx", { type: blob.type })); expect(imported.title).toBe("Бюджет.xlsx"); expect(imported.sheets).toHaveLength(2); expect(imported.sheets[0].name).toBe("Бюджет"); expect(imported.sheets[0].cells.AC20).toBe("=SUM(AA20:AB20)"); expect(imported.sheets[0].mergedCells).toEqual([{ start: "A1", end: "C1" }]); expect(imported.sheets[0].hyperlinks).toEqual({ A1: "https://example.com/budget" }); expect(imported.sheets[0].cellFormats).toEqual({ A1: { bold: true, fillColor: "FFF2CC" }, AC20: { underline: true }, D20: { numberFormat: "dd.mm.yyyy" }, E20: { numberFormat: "0%" } }); expect(imported.sheets[0].charts).toEqual([ { id: "xlsx-chart-1", type: "bar", title: "Итого", labelRange: "A2:A3", valueRange: "AA20:AB20" } ]); expect(imported.sheets[1].name).toBe("Справочник"); expect(imported.sheets[1].cells.B2).toBe("Проект"); }); });