import { describe, expect, it } from "vitest"; import JSZip from "jszip"; import { buildChartXml, buildCellXml, buildAutoFilterXml, buildCommentsXml, buildColumnWidthsXml, buildHyperlinksXml, buildMergeCellsXml, buildSheetPrXml, buildSheetViewsXml, createCellStyleRegistry, buildWorksheetRelationshipsXml, buildWorksheetXml, exportXlsxBlob, importXlsxFile, chartDataPoints, chartXmlToSheetChart, workbookActiveSheetId, workbookPrintAreas, workbookRelationshipTargets, workbookSheetRefs, workbookUsesDate1904, worksheetXmlToAutoFilter, worksheetXmlToCells, worksheetXmlToCellFormats, worksheetXmlToColumnWidths, worksheetXmlToFrozenPane, worksheetXmlToHiddenColumns, worksheetXmlToHiddenRows, worksheetXmlToHyperlinks, worksheetCommentsXmlToComments, worksheetXmlToMergedCells, worksheetXmlToRowHeights, worksheetXmlToShowGridLines, worksheetXmlToTabColor, worksheetXmlToZoomScale, xlsxStylesXmlToCellFormatTable, xlsxThemeXmlToColors, xlsxSafeSheetNames, 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("сдвигает относительные ссылки XLSX shared formulas при импорте", () => { const cells = worksheetXmlToCells( { worksheet: { sheetData: { row: [ { r: "1", c: [{ r: "C1", f: { t: "shared", si: "0", ref: "C1:D2", "#text": "A1+B$1+$A1+$B$2+'A1 Sheet'!A1&\"A1\"" }, v: "0" }] }, { r: "2", c: [ { r: "C2", f: { t: "shared", si: "0" }, v: "0" }, { r: "D2", f: { t: "shared", si: "0" }, v: "0" } ] } ] } } }, [] ); expect(cells).toEqual({ C1: "=A1+B$1+$A1+$B$2+'A1 Sheet'!A1&\"A1\"", C2: "=A2+B$1+$A2+$B$2+'A1 Sheet'!A2&\"A1\"", D2: "=B2+C$1+$A2+$B$2+'A1 Sheet'!B2&\"A1\"" }); }); 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 как location без external relationship", () => { const hyperlinks = { A1: "#Справочник!B2", B2: "https://example.com/report" }; const xml = buildWorksheetXml({ A1: "Справочник", B2: "Отчет" }, [], hyperlinks); const relationshipsXml = buildWorksheetRelationshipsXml(hyperlinks); expect(buildHyperlinksXml(hyperlinks)).toBe( '' ); expect(xml).toContain(''); expect(relationshipsXml).toContain('Target="https://example.com/report"'); expect(relationshipsXml).not.toContain("Справочник!B2"); }); it("назначает drawing relationship после внешних ссылок, игнорируя внутренние ссылки XLSX", () => { const relationshipsXml = buildWorksheetRelationshipsXml({ A1: "#Справочник!B2" }, "../drawings/drawing1.xml"); expect(relationshipsXml).toContain('Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing"'); expect(relationshipsXml).not.toContain('TargetMode="External"'); }); it("собирает и читает legacy comments XLSX для заметок ячеек", () => { const commentsXml = buildCommentsXml({ B2: "Проверить сумму", A1: "Комментарий\nв две строки" }); const parsedComments = worksheetCommentsXmlToComments({ comments: { commentList: { comment: [ { ref: "B2", text: { t: "Проверить сумму" } }, { ref: "A1", text: { r: [{ t: "Комментарий" }, { t: "\nв две строки" }] } } ] } } }); expect(commentsXml).toContain('Комментарий\nв две строки'); expect(commentsXml).toContain('Проверить сумму'); expect(parsedComments).toEqual({ A1: "Комментарий\nв две строки", B2: "Проверить сумму" }); }); it("сохраняет заметки ячеек XLSX в comments, VML и relationships", async () => { const blob = await exportXlsxBlob({ id: "workbook", title: "Comments.xlsx", updatedAt: "2026-06-01T00:00:00.000Z", activeSheet: "sheet-1", sheets: [ { id: "sheet-1", name: "Sheet 1", cells: { A1: "Итого" }, comments: { A1: "Проверено финансовым отделом" } } ] }); const zip = await JSZip.loadAsync(await blob.arrayBuffer()); const contentTypesXml = await zip.file("[Content_Types].xml")?.async("string"); const worksheetXml = await zip.file("xl/worksheets/sheet1.xml")?.async("string"); const relationshipsXml = await zip.file("xl/worksheets/_rels/sheet1.xml.rels")?.async("string"); const commentsXml = await zip.file("xl/comments1.xml")?.async("string"); const vmlXml = await zip.file("xl/drawings/vmlDrawing1.vml")?.async("string"); const imported = await importXlsxFile(new File([blob], "Comments.xlsx", { type: blob.type })); expect(contentTypesXml).toContain("/xl/comments1.xml"); expect(contentTypesXml).toContain('Extension="vml"'); expect(worksheetXml).toContain(''); expect(relationshipsXml).toContain('Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"'); expect(relationshipsXml).toContain('Target="../comments1.xml"'); expect(relationshipsXml).toContain('Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing"'); expect(commentsXml).toContain(''); expect(vmlXml).toContain("00"); expect(imported.sheets[0].comments).toEqual({ A1: "Проверено финансовым отделом" }); }); it("сохраняет базовое форматирование ячеек XLSX в XML листа", () => { const cellFormats = { A1: { bold: true, fontSize: 14, fontFamily: "Arial", textColor: "0f5fae", fillColor: "fff2cc", horizontalAlign: "center" as const }, B2: { italics: true, underline: true, strike: 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", async () => { const blob = await exportXlsxBlob({ id: "workbook", title: "Strike.xlsx", updatedAt: "2026-06-01T00:00:00.000Z", activeSheet: "sheet-1", sheets: [ { id: "sheet-1", name: "Sheet 1", cells: { A1: "Deleted" }, cellFormats: { A1: { strike: true } } } ] }); const zip = await JSZip.loadAsync(await blob.arrayBuffer()); const stylesXml = await zip.file("xl/styles.xml")?.async("string"); expect(stylesXml).toContain(""); }); it("записывает семейство шрифта ячейки XLSX в styles.xml", async () => { const blob = await exportXlsxBlob({ id: "workbook", title: "Font.xlsx", updatedAt: "2026-06-01T00:00:00.000Z", activeSheet: "sheet-1", sheets: [ { id: "sheet-1", name: "Sheet 1", cells: { A1: "Arial" }, cellFormats: { A1: { fontFamily: "Arial" } } } ] }); const zip = await JSZip.loadAsync(await blob.arrayBuffer()); const stylesXml = await zip.file("xl/styles.xml")?.async("string"); expect(stylesXml).toContain(''); }); it("записывает перенос текста ячейки XLSX в styles.xml", async () => { const blob = await exportXlsxBlob({ id: "workbook", title: "Wrap.xlsx", updatedAt: "2026-06-01T00:00:00.000Z", activeSheet: "sheet-1", sheets: [ { id: "sheet-1", name: "Sheet 1", cells: { A1: "Long text" }, cellFormats: { A1: { wrapText: true } } } ] }); const zip = await JSZip.loadAsync(await blob.arrayBuffer()); const stylesXml = await zip.file("xl/styles.xml")?.async("string"); expect(stylesXml).toContain('applyAlignment="1"'); expect(stylesXml).toContain('wrapText="1"'); }); it("записывает вертикальное выравнивание ячейки XLSX в styles.xml", async () => { const blob = await exportXlsxBlob({ id: "workbook", title: "VerticalAlign.xlsx", updatedAt: "2026-06-01T00:00:00.000Z", activeSheet: "sheet-1", sheets: [ { id: "sheet-1", name: "Sheet 1", cells: { A1: "Centered" }, cellFormats: { A1: { verticalAlign: "middle" } } } ] }); const zip = await JSZip.loadAsync(await blob.arrayBuffer()); const stylesXml = await zip.file("xl/styles.xml")?.async("string"); expect(stylesXml).toContain('applyAlignment="1"'); expect(stylesXml).toContain('vertical="center"'); }); it("записывает границы ячейки XLSX в styles.xml", async () => { const blob = await exportXlsxBlob({ id: "workbook", title: "Borders.xlsx", updatedAt: "2026-06-01T00:00:00.000Z", activeSheet: "sheet-1", sheets: [ { id: "sheet-1", name: "Sheet 1", cells: { A1: "Bordered" }, cellFormats: { A1: { border: true, borderColor: "C2410C" } } } ] }); const zip = await JSZip.loadAsync(await blob.arrayBuffer()); const stylesXml = await zip.file("xl/styles.xml")?.async("string"); expect(stylesXml).toContain(''); expect(stylesXml).toContain('applyBorder="1"'); expect(stylesXml).toContain(''); expect(stylesXml).toContain(''); expect(stylesXml).toContain(''); expect(stylesXml).toContain(''); expect(stylesXml).toContain(''); }); it("читает базовое форматирование ячеек XLSX из styles.xml и XML листа", () => { const styleTable = xlsxStylesXmlToCellFormatTable({ styleSheet: { numFmts: { numFmt: { numFmtId: "164", formatCode: "dd.mm.yyyy" } }, fonts: { font: [{}, { b: {}, i: {}, u: {}, strike: {}, sz: { val: "14" }, name: { val: "Arial" }, color: { rgb: "FF0F5FAE" } }] }, fills: { fill: [{}, {}, { patternFill: { fgColor: { rgb: "FFFFF2CC" } } }] }, borders: { border: [ {}, { left: { style: "thin", color: { rgb: "FFC2410C" } }, right: { style: "thin", color: { rgb: "FFC2410C" } }, top: { style: "thin", color: { rgb: "FFC2410C" } }, bottom: { style: "thin", color: { rgb: "FFC2410C" } } } ] }, cellXfs: { xf: [ { fontId: "0", fillId: "0" }, { fontId: "1", fillId: "2", borderId: "1", numFmtId: "164", alignment: { horizontal: "center", vertical: "center", wrapText: "1" } }, { fontId: "0", fillId: "0", numFmtId: "14" } ] } } }); expect(styleTable[1]).toEqual({ bold: true, italics: true, underline: true, strike: true, fontSize: 14, fontFamily: "Arial", textColor: "0F5FAE", fillColor: "FFF2CC", horizontalAlign: "center", verticalAlign: "middle", wrapText: true, border: true, borderColor: "C2410C", 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, strike: true, fontSize: 14, fontFamily: "Arial", textColor: "0F5FAE", fillColor: "FFF2CC", horizontalAlign: "center", verticalAlign: "middle", wrapText: true, border: true, borderColor: "C2410C", numberFormat: "dd.mm.yyyy" } }); }); it("читает theme, tint и indexed цвета XLSX из styles.xml", () => { const styleTable = xlsxStylesXmlToCellFormatTable({ styleSheet: { fonts: { font: [{ color: { theme: "1" } }, { color: { theme: "4", tint: "0.5" } }, { color: { indexed: "22" } }] }, fills: { fill: [{}, {}, { patternFill: { fgColor: { theme: "6" } } }] }, borders: { border: [{}, { left: { style: "thin", color: { indexed: "4" } } }] }, cellXfs: { xf: [ { fontId: "0", fillId: "0", borderId: "0" }, { fontId: "1", fillId: "2", borderId: "1" }, { fontId: "2", fillId: "0", borderId: "0" } ] } } }); expect(styleTable[0]).toBeUndefined(); expect(styleTable[1]).toEqual({ textColor: "A7C0DE", fillColor: "9BBB59", border: true, borderColor: "0000FF" }); expect(styleTable[2]).toEqual({ textColor: "C0C0C0" }); }); it("использует цвета из xl/theme/theme1.xml при импорте XLSX", async () => { const themeXml = ``; const zip = new JSZip(); zip.file("xl/workbook.xml", ``); zip.file( "xl/_rels/workbook.xml.rels", `` ); zip.file("xl/theme/theme1.xml", themeXml); zip.file( "xl/styles.xml", `` ); zip.file( "xl/worksheets/sheet1.xml", `Themed` ); expect(xlsxThemeXmlToColors({ "a:theme": { "a:themeElements": { "a:clrScheme": { "a:accent1": { "a:srgbClr": { val: "123456" } } } } } })[4]).toBe( "123456" ); const blob = await zip.generateAsync({ type: "blob", mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }); const imported = await importXlsxFile(new File([blob], "Theme.xlsx", { type: blob.type })); expect(imported.sheets[0].cellFormats).toEqual({ A1: { textColor: "123456" } }); }); 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("читает внутренние гиперссылки XLSX из location", () => { expect( worksheetXmlToHyperlinks( { worksheet: { hyperlinks: { hyperlink: [ { ref: "A1", location: "Справочник!B2" }, { ref: "B2", location: "" }, { ref: "bad", location: "Справочник!B2" } ] } } }, undefined ) ).toEqual({ A1: "#Справочник!B2" }); }); it("читает ссылки листов и relationships из workbook XML", () => { expect( workbookSheetRefs({ workbook: { sheets: { sheet: { name: "Бюджет", sheetId: "7", "r:id": "rId3" } } } }) ).toEqual([{ id: "sheet-7", name: "Бюджет", relationshipId: "rId3" }]); expect( workbookSheetRefs({ workbook: { sheets: { sheet: { name: "Архив", sheetId: "8", "r:id": "rId4", state: "hidden" } } } }) ).toEqual([{ id: "sheet-8", name: "Архив", visibility: "hidden", relationshipId: "rId4" }]); expect( workbookRelationshipTargets({ Relationships: { Relationship: [{ Id: "rId3", Target: "worksheets/sheet7.xml" }] } }) ).toEqual({ rId3: "worksheets/sheet7.xml" }); expect( workbookActiveSheetId( { workbook: { bookViews: { workbookView: { activeTab: "1" } } } }, [ { id: "sheet-1" }, { id: "sheet-7" } ] ) ).toBe("sheet-7"); }); it("detects the XLSX 1904 date system flag from workbook properties", () => { expect(workbookUsesDate1904({ workbook: { workbookPr: { date1904: "1" } } })).toBe(true); expect(workbookUsesDate1904({ workbook: { workbookPr: { date1904: "true" } } })).toBe(true); expect(workbookUsesDate1904({ workbook: { workbookPr: { date1904: "0" } } })).toBe(false); }); it("converts imported XLSX 1904 date serials to the QExcell 1900 serial system", async () => { const zip = new JSZip(); zip.file( "xl/workbook.xml", `` ); zip.file( "xl/_rels/workbook.xml.rels", `` ); zip.file("xl/styles.xml", ``); zip.file( "xl/worksheets/sheet1.xml", `4346550.5` ); const blob = await zip.generateAsync({ type: "blob", mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }); const imported = await importXlsxFile(new File([blob], "Date1904.xlsx", { type: blob.type })); expect(imported.sheets[0].cells).toEqual({ A1: "44927", B1: "5", C1: "0.5" }); expect(imported.sheets[0].cellFormats).toEqual({ A1: { numberFormat: "m/d/yy" }, C1: { numberFormat: "h:mm" } }); }); it("сохраняет и импортирует активный лист XLSX через workbookView activeTab", async () => { const blob = await exportXlsxBlob({ id: "book-1", title: "Активный лист.xlsx", updatedAt: "2026-06-01T00:00:00.000Z", activeSheet: "sheet-2", sheets: [ { id: "sheet-1", name: "Первый", cells: { A1: "Первый" } }, { id: "sheet-2", name: "Второй", cells: { A1: "Второй" } } ] }); const zip = await JSZip.loadAsync(await blob.arrayBuffer()); const workbookXml = await zip.file("xl/workbook.xml")?.async("string"); const imported = await importXlsxFile(new File([blob], "Активный лист.xlsx", { type: blob.type })); expect(workbookXml).toContain(''); expect(imported.activeSheet).toBe("sheet-2"); }); it("normalizes XLSX sheet names to Excel-safe unique names on export", async () => { expect( xlsxSafeSheetNames([ { name: "Sales/2026:Q1*?" }, { name: "'Very long worksheet name that exceeds thirty one characters'" }, { name: "Sales 2026 Q1" }, { name: "'[]:*?/\\'" }, { name: "History" } ]) ).toEqual(["Sales 2026 Q1", "Very long worksheet name that e", "Sales 2026 Q1 (2)", "Лист 4", "History 1"]); const blob = await exportXlsxBlob({ id: "book-1", title: "Safe sheet names.xlsx", updatedAt: "2026-06-01T00:00:00.000Z", activeSheet: "sheet-1", sheets: [ { id: "sheet-1", name: "Sales/2026:Q1*?", printArea: "A1:B2", cells: { A1: "Sales" } }, { id: "sheet-2", name: "Sales 2026 Q1", cells: { A1: "Duplicate" } } ] }); const zip = await JSZip.loadAsync(await blob.arrayBuffer()); const workbookXml = await zip.file("xl/workbook.xml")?.async("string"); const imported = await importXlsxFile(new File([blob], "Safe sheet names.xlsx", { type: blob.type })); expect(workbookXml).toContain(''); expect(workbookXml).toContain(''); expect(workbookXml).toContain('\'Sales 2026 Q1\'!$A$1:$B$2'); expect(imported.sheets.map((sheet) => sheet.name)).toEqual(["Sales 2026 Q1", "Sales 2026 Q1 (2)"]); expect(imported.sheets[0].printArea).toBe("A1:B2"); }); it("сохраняет и импортирует область печати XLSX через definedName _xlnm.Print_Area", async () => { expect( workbookPrintAreas( { workbook: { definedNames: { definedName: [ { name: "_xlnm.Print_Area", localSheetId: "0", "#text": "'Отчет'!$D$20:$A$1" }, { name: "_xlnm.Print_Area", localSheetId: "1", "#text": "'Архив'!$B$2:$C$4,'Архив'!$E$1:$E$3" } ] } } }, [ { id: "sheet-1", name: "Отчет" }, { id: "sheet-2", name: "Архив" } ] ) ).toEqual({ "sheet-1": "A1:D20", "sheet-2": "B2:C4,E1:E3" }); const blob = await exportXlsxBlob({ id: "book-1", title: "Область печати.xlsx", updatedAt: "2026-06-01T00:00:00.000Z", activeSheet: "sheet-1", sheets: [ { id: "sheet-1", name: "Отчет", printArea: "D20:A1", cells: { A1: "Начало", D20: "Итог" } }, { id: "sheet-2", name: "Архив", printArea: "B2:C4,E1:E3", cells: { B2: "Архив" } } ] }); const zip = await JSZip.loadAsync(await blob.arrayBuffer()); const workbookXml = await zip.file("xl/workbook.xml")?.async("string"); const imported = await importXlsxFile(new File([blob], "Область печати.xlsx", { type: blob.type })); expect(workbookXml).toContain('\'Отчет\'!$A$1:$D$20'); expect(workbookXml).toContain('\'Архив\'!$B$2:$C$4,\'Архив\'!$E$1:$E$3'); expect(imported.sheets[0].printArea).toBe("A1:D20"); expect(imported.sheets[1].printArea).toBe("B2:C4,E1:E3"); }); it("сохраняет и импортирует скрытые листы XLSX через sheet state", async () => { const blob = await exportXlsxBlob({ id: "book-1", title: "Скрытые листы.xlsx", updatedAt: "2026-06-01T00:00:00.000Z", activeSheet: "sheet-1", sheets: [ { id: "sheet-1", name: "Рабочий", cells: { A1: "Открыт" } }, { id: "sheet-2", name: "Архив", visibility: "hidden", cells: { A1: "Скрыт" } }, { id: "sheet-3", name: "Служебный", visibility: "veryHidden", cells: { A1: "Очень скрыт" } } ] }); const zip = await JSZip.loadAsync(await blob.arrayBuffer()); const workbookXml = await zip.file("xl/workbook.xml")?.async("string"); const imported = await importXlsxFile(new File([blob], "Скрытые листы.xlsx", { type: blob.type })); expect(workbookXml).toContain(''); expect(workbookXml).toContain(''); expect(imported.sheets[1]).toMatchObject({ id: "sheet-2", name: "Архив", visibility: "hidden" }); expect(imported.sheets[2]).toMatchObject({ id: "sheet-3", name: "Служебный", visibility: "veryHidden" }); }); it("читает и записывает закрепленные области XLSX через sheetViews pane", async () => { expect( worksheetXmlToFrozenPane({ worksheet: { sheetViews: { sheetView: { pane: { xSplit: "1", ySplit: "2", topLeftCell: "B3", activePane: "bottomRight", state: "frozen" } } } } }) ).toEqual({ columns: 1, rows: 2, topLeftCell: "B3", activePane: "bottomRight" }); expect( worksheetXmlToFrozenPane({ worksheet: { sheetViews: { sheetView: { pane: { xSplit: "1", state: "split" } } } } }) ).toBeUndefined(); expect(buildSheetViewsXml({ columns: 1, rows: 1 })).toBe( '' ); const blob = await exportXlsxBlob({ id: "book-1", title: "Frozen panes.xlsx", updatedAt: "2026-06-01T00:00:00.000Z", activeSheet: "sheet-1", sheets: [ { id: "sheet-1", name: "Freeze", frozenPane: { columns: 1, rows: 1 }, cells: { A1: "Header", B2: "Body" } } ] }); const zip = await JSZip.loadAsync(await blob.arrayBuffer()); const sheetXml = (await zip.file("xl/worksheets/sheet1.xml")?.async("string")) ?? ""; const imported = await importXlsxFile(new File([blob], "Frozen panes.xlsx", { type: blob.type })); expect(sheetXml).toContain( '' ); expect(sheetXml.indexOf("")).toBeLessThan(sheetXml.indexOf("")); expect(imported.sheets[0].frozenPane).toEqual({ columns: 1, rows: 1, topLeftCell: "B2", activePane: "bottomRight" }); }); it("читает и записывает скрытие сетки листа XLSX через sheetView showGridLines", async () => { expect( worksheetXmlToShowGridLines({ worksheet: { sheetViews: { sheetView: { showGridLines: "0" } } } }) ).toBe(false); expect(buildSheetViewsXml(undefined, false)).toBe(''); expect(buildSheetViewsXml({ rows: 1 }, false)).toBe( '' ); const blob = await exportXlsxBlob({ id: "book-1", title: "Grid lines.xlsx", updatedAt: "2026-06-01T00:00:00.000Z", activeSheet: "sheet-1", sheets: [ { id: "sheet-1", name: "Grid", showGridLines: false, cells: { A1: "No grid" } } ] }); const zip = await JSZip.loadAsync(await blob.arrayBuffer()); const sheetXml = (await zip.file("xl/worksheets/sheet1.xml")?.async("string")) ?? ""; const imported = await importXlsxFile(new File([blob], "Grid lines.xlsx", { type: blob.type })); expect(sheetXml).toContain(''); expect(imported.sheets[0].showGridLines).toBe(false); }); it("читает и записывает масштаб листа XLSX через sheetView zoomScale", async () => { expect( worksheetXmlToZoomScale({ worksheet: { sheetViews: { sheetView: { zoomScale: "125" } } } }) ).toBe(125); expect( worksheetXmlToZoomScale({ worksheet: { sheetViews: { sheetView: { zoomScale: "100" } } } }) ).toBe(0); expect(buildSheetViewsXml(undefined, undefined, 125)).toBe(''); expect(buildSheetViewsXml({ columns: 1 }, false, 90)).toBe( '' ); const blob = await exportXlsxBlob({ id: "book-1", title: "Zoom.xlsx", updatedAt: "2026-06-01T00:00:00.000Z", activeSheet: "sheet-1", sheets: [ { id: "sheet-1", name: "Zoom", zoomScale: 125, cells: { A1: "Zoomed" } } ] }); const zip = await JSZip.loadAsync(await blob.arrayBuffer()); const sheetXml = (await zip.file("xl/worksheets/sheet1.xml")?.async("string")) ?? ""; const imported = await importXlsxFile(new File([blob], "Zoom.xlsx", { type: blob.type })); expect(sheetXml).toContain(''); expect(imported.sheets[0].zoomScale).toBe(125); }); it("читает и записывает автофильтр XLSX через autoFilter ref", async () => { expect( worksheetXmlToAutoFilter({ worksheet: { autoFilter: { ref: "$D$20:$A$1" } } }) ).toBe("A1:D20"); expect(buildAutoFilterXml("B2:B2")).toBe(''); expect(buildAutoFilterXml("bad")).toBe(""); const blob = await exportXlsxBlob({ id: "book-1", title: "AutoFilter.xlsx", updatedAt: "2026-06-01T00:00:00.000Z", activeSheet: "sheet-1", sheets: [ { id: "sheet-1", name: "Filter", autoFilter: "D20:A1", mergedCells: [{ start: "A22", end: "D22" }], cells: { A1: "Product", D20: "Total" } } ] }); const zip = await JSZip.loadAsync(await blob.arrayBuffer()); const sheetXml = (await zip.file("xl/worksheets/sheet1.xml")?.async("string")) ?? ""; const imported = await importXlsxFile(new File([blob], "AutoFilter.xlsx", { type: blob.type })); expect(sheetXml).toContain(''); expect(sheetXml.indexOf("")).toBeLessThan(sheetXml.indexOf(" { expect( worksheetXmlToTabColor({ worksheet: { sheetPr: { tabColor: { rgb: "FF0F766E" } } } }) ).toBe("0F766E"); expect( worksheetXmlToTabColor( { worksheet: { sheetPr: { tabColor: { theme: "4" } } } }, ["000000", "FFFFFF", "1F497D", "EEECE1", "123456"] ) ).toBe("123456"); expect(buildSheetPrXml("#0f766e")).toBe(''); expect(buildSheetPrXml("bad")).toBe(""); const blob = await exportXlsxBlob({ id: "book-1", title: "Tab color.xlsx", updatedAt: "2026-06-01T00:00:00.000Z", activeSheet: "sheet-1", sheets: [ { id: "sheet-1", name: "Color", tabColor: "#0f766e", frozenPane: { rows: 1 }, cells: { A1: "Header" } } ] }); const zip = await JSZip.loadAsync(await blob.arrayBuffer()); const sheetXml = (await zip.file("xl/worksheets/sheet1.xml")?.async("string")) ?? ""; const imported = await importXlsxFile(new File([blob], "Tab color.xlsx", { type: blob.type })); expect(sheetXml).toContain(''); expect(sheetXml.indexOf("")).toBeLessThan(sheetXml.indexOf("")); expect(imported.sheets[0].tabColor).toBe("0F766E"); }); it("читает и записывает размеры колонок и строк XLSX", async () => { expect( worksheetXmlToColumnWidths({ worksheet: { cols: { col: [ { min: "1", max: "2", width: "18.5" }, { min: "4", max: "4", width: "9.25" } ] } } }) ).toEqual({ A: 18.5, B: 18.5, D: 9.25 }); expect( worksheetXmlToRowHeights({ worksheet: { sheetData: { row: [{ r: "1", ht: "24" }, { r: "4", ht: "30.5" }] } } }) ).toEqual({ "1": 24, "4": 30.5 }); expect(buildColumnWidthsXml({ C: 9.25, A: 18.5, "1bad": 1 })).toBe( '' ); const blob = await exportXlsxBlob({ id: "book-1", title: "Sheet sizes.xlsx", updatedAt: "2026-06-01T00:00:00.000Z", activeSheet: "sheet-1", sheets: [ { id: "sheet-1", name: "Sizes", columnWidths: { A: 18.5, C: 9.25 }, rowHeights: { "1": 24, "4": 30.5 }, cells: { A1: "Header", C2: "Value" } } ] }); const zip = await JSZip.loadAsync(await blob.arrayBuffer()); const sheetXml = (await zip.file("xl/worksheets/sheet1.xml")?.async("string")) ?? ""; const imported = await importXlsxFile(new File([blob], "Sheet sizes.xlsx", { type: blob.type })); expect(sheetXml).toContain( '' ); expect(sheetXml).toContain('Header'); expect(sheetXml).toContain(''); expect(sheetXml.indexOf("")).toBeLessThan(sheetXml.indexOf("")); expect(imported.sheets[0].columnWidths).toEqual({ A: 18.5, C: 9.25 }); expect(imported.sheets[0].rowHeights).toEqual({ "1": 24, "4": 30.5 }); }); it("читает и записывает скрытые колонки и строки XLSX", async () => { expect( worksheetXmlToHiddenColumns({ worksheet: { cols: { col: [ { min: "2", max: "3", hidden: "1" }, { min: "5", max: "5", width: "12", hidden: "true" } ] } } }) ).toEqual({ B: true, C: true, E: true }); expect( worksheetXmlToHiddenRows({ worksheet: { sheetData: { row: [{ r: "4", hidden: "1" }, { r: "6", ht: "21", hidden: "true" }] } } }) ).toEqual({ "4": true, "6": true }); expect(buildColumnWidthsXml({ E: 12 }, { B: true, C: true, E: true })).toBe( '' ); const blob = await exportXlsxBlob({ id: "book-1", title: "Hidden rows and columns.xlsx", updatedAt: "2026-06-01T00:00:00.000Z", activeSheet: "sheet-1", sheets: [ { id: "sheet-1", name: "Hidden", columnWidths: { E: 12 }, rowHeights: { "6": 21 }, hiddenColumns: { B: true, C: true, E: true }, hiddenRows: { "4": true, "6": true }, cells: { A1: "Visible" } } ] }); const zip = await JSZip.loadAsync(await blob.arrayBuffer()); const sheetXml = (await zip.file("xl/worksheets/sheet1.xml")?.async("string")) ?? ""; const imported = await importXlsxFile(new File([blob], "Hidden rows and columns.xlsx", { type: blob.type })); expect(sheetXml).toContain( '' ); expect(sheetXml).toContain('