378 lines
12 KiB
TypeScript
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("Проект");
|
|
});
|
|
});
|