Files
QOffice/src/io/excellOffice.test.ts
T
Курнат Андрей 9bf4dd3672 Initial QOffice implementation
2026-06-01 05:31:05 +03:00

378 lines
12 KiB
TypeScript

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("записывает формулу в тег <f>", () => {
expect(buildCellXml("c1", "=SUM(A1:B1)")).toBe('<c r="C1"><f>SUM(A1:B1)</f></c>');
});
it("собирает XML листа с отсортированными ячейками", () => {
const xml = buildWorksheetXml({
B2: "200",
A1: "Статья",
C2: "=B2*2"
});
expect(xml).toContain('<row r="1"><c r="A1" t="inlineStr"><is><t>Статья</t></is></c></row>');
expect(xml).toContain('<row r="2"><c r="B2"><v>200</v></c><c r="C2"><f>B2*2</f></c></row>');
});
it("сохраняет merged cells в XML листа", () => {
const xml = buildWorksheetXml({ A1: "Заголовок" }, [{ start: "A1", end: "C2" }]);
expect(buildMergeCellsXml([{ start: "C2", end: "A1" }, { start: "A1", end: "C2" }])).toBe(
'<mergeCells count="1"><mergeCell ref="A1:C2"/></mergeCells>'
);
expect(xml).toContain('<mergeCells count="1"><mergeCell ref="A1:C2"/></mergeCells>');
});
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('<hyperlinks><hyperlink ref="A1" r:id="rId1"/></hyperlinks>');
expect(xml).toContain('<hyperlinks><hyperlink ref="A1" r:id="rId1"/></hyperlinks>');
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('<c r="A1" s="1" t="inlineStr"><is><t>Итог</t></is></c>');
expect(xml).toContain('<c r="B2" s="2" t="inlineStr"><is><t>Комментарий</t></is></c>');
expect(xml).toContain('<c r="C3" t="inlineStr"><is><t>Без стиля</t></is></c>');
expect(emptyCellXml).toContain('<c r="C3" s="1"/>');
});
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("<c:barChart>");
expect(xml).toContain("<c:f>'Бюджет'!$A$2:$A$4</c:f>");
expect(xml).toContain("<c:f>'Бюджет'!$E$2:$E$4</c:f>");
expect(xml).toContain("<c:v>Базовый</c:v>");
expect(xml).toContain("<c:v>43600</c:v>");
});
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("Проект");
});
});