import { describe, expect, test } from "bun:test"; import { fileURLToPath } from "node:url"; import ExcelJS from "exceljs"; import JSZip from "jszip"; import { XlsxWriter } from "../src/xlsx/XlsxWriter.ts"; import { makeMinimalProject, fixtureExists } from "./helpers.ts"; const FIXTURE_MPP_PATH = resolveFixturePath("./sample-schedule.mpp"); const HAS_MPP_FIXTURE = fixtureExists(FIXTURE_MPP_PATH); async function getMppReader() { return import("../src/mpp/MppReader.ts"); } describe("XlsxWriter", () => { test.skipIf(!HAS_MPP_FIXTURE)( "produces a valid XLSX buffer from the MPP fixture", async () => { const { MppReader } = await getMppReader(); const data = await Bun.file(FIXTURE_MPP_PATH).arrayBuffer(); const project = new MppReader().read(data); const writer = new XlsxWriter(); const buffer = await writer.write(project); expect(buffer).toBeInstanceOf(Uint8Array); expect(buffer.byteLength).toBeGreaterThan(0); // Parse the buffer back and verify structure const workbook = new ExcelJS.Workbook(); await workbook.xlsx.load(buffer); const sheet = workbook.getWorksheet("Schedule"); expect(sheet).toBeDefined(); // Header row + task rows (tasks with non-null id) const tasksWithId = project.tasks.filter((t) => t.id !== null); expect(sheet!.rowCount).toBe(tasksWithId.length + 1); // +1 for header // Verify header const headerRow = sheet!.getRow(1); expect(headerRow.getCell(1).value).toBe("ID"); expect(headerRow.getCell(2).value).toBe("Task Name"); expect(headerRow.getCell(3).value).toBe("WBS"); expect(headerRow.getCell(4).value).toBe("Start"); expect(headerRow.getCell(5).value).toBe("Finish"); expect(headerRow.getCell(6).value).toBe("Duration"); // Verify first data row matches first task const firstTask = tasksWithId[0]!; const firstRow = sheet!.getRow(2); expect(firstRow.getCell(1).value).toBe(firstTask.id); expect(firstRow.getCell(2).value).toBe(firstTask.name); }, ); test("applies bold formatting to summary tasks", async () => { const project = makeMinimalProject(); project.tasks[0]!.summary = true; const buffer = await new XlsxWriter().write(project); const workbook = new ExcelJS.Workbook(); await workbook.xlsx.load(buffer); const sheet = workbook.getWorksheet("Schedule")!; const row = sheet.getRow(2); expect(row.font?.bold).toBe(true); }); test("sets row outline levels from task outline levels", async () => { const project = makeMinimalProject(); project.tasks[0]!.outlineLevel = 3; const buffer = await new XlsxWriter().write(project); const workbook = new ExcelJS.Workbook(); await workbook.xlsx.load(buffer); const sheet = workbook.getWorksheet("Schedule")!; const row = sheet.getRow(2); expect(row.outlineLevel).toBe(3); }); test("uses custom sheet name from options", async () => { const project = makeMinimalProject(); const buffer = await new XlsxWriter().write(project, { sheetName: "Tasks", }); const workbook = new ExcelJS.Workbook(); await workbook.xlsx.load(buffer); expect(workbook.getWorksheet("Tasks")).toBeDefined(); expect(workbook.getWorksheet("Schedule")).toBeUndefined(); }); test("formats dates as Excel serial numbers with date numFmt", async () => { const project = makeMinimalProject(); const buffer = await new XlsxWriter().write(project); const workbook = new ExcelJS.Workbook(); await workbook.xlsx.load(buffer); const sheet = workbook.getWorksheet("Schedule")!; const row = sheet.getRow(2); const startCell = row.getCell(4); // column 4 = Start // ExcelJS round-trips serial numbers as Date objects expect(startCell.value).not.toBeNull(); expect(startCell.numFmt).toBe("M/D/YYYY H:MM AM/PM"); }); test("writeWithGantt adds hidden data sheet, Gantt sheet, and chart XML", async () => { const project = makeMinimalProject(); const buffer = await new XlsxWriter().writeWithGantt(project, { title: "Installation Plan", phases: [ { label: "Phase 1", items: [ { name: "MB204", start: "2027-09-01", finish: "2027-11-15" }, { name: "MB205", start: "2027-10-01", finish: "2027-12-15" }, ], }, { label: "Phase 2", items: [{ name: "MB210", start: "2028-01-10", finish: "2028-02-28" }], }, ], }); const workbook = new ExcelJS.Workbook(); await workbook.xlsx.load(buffer.buffer.slice(0) as ArrayBuffer); const dataSheet = workbook.getWorksheet("_Gantt Data"); expect(dataSheet).toBeDefined(); expect(dataSheet!.state).toBe("hidden"); // header + 3 items expect(dataSheet!.rowCount).toBe(4); // First data row should be the earliest start (2027-09-01) -> MB204 const firstRow = dataSheet!.getRow(2); expect(firstRow.getCell(1).value).toBe("Phase 1: MB204"); expect(workbook.getWorksheet("Gantt")).toBeDefined(); const zip = await JSZip.loadAsync(buffer.buffer.slice(0) as ArrayBuffer); const chartFile = zip.file("xl/charts/chart1.xml"); expect(chartFile).not.toBeNull(); const chartXml = await chartFile!.async("string"); expect(chartXml).toContain(''); expect(chartXml).toContain(''); // Phase 1 color on the duration data points expect(chartXml).toContain('val="4472C4"'); // Phase 2 color on the duration data points expect(chartXml).toContain('val="ED7D31"'); // Plot area row shading: gradient with alternating white / light-gray bands. expect(chartXml).toContain(" { const project = makeMinimalProject(); const buffer = await new XlsxWriter().writeWithGantt(project, { phases: [], }); const workbook = new ExcelJS.Workbook(); await workbook.xlsx.load(buffer.buffer.slice(0) as ArrayBuffer); expect(workbook.getWorksheet("Schedule")).toBeDefined(); expect(workbook.getWorksheet("Gantt")).toBeUndefined(); expect(workbook.getWorksheet("_Gantt Data")).toBeUndefined(); }); test("styles the header row with blue background and white bold text", async () => { const project = makeMinimalProject(); const buffer = await new XlsxWriter().write(project); const workbook = new ExcelJS.Workbook(); await workbook.xlsx.load(buffer); const sheet = workbook.getWorksheet("Schedule")!; const headerRow = sheet.getRow(1); expect(headerRow.font?.bold).toBe(true); expect(headerRow.font?.color?.argb).toBe("FFFFFFFF"); }); }); function resolveFixturePath(relativePath: string): string { return fileURLToPath(new URL(relativePath, import.meta.url)); }