// @vitest-environment jsdom import { act } from 'react'; import { createRoot } from 'react-dom/client'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { read, utils } from 'xlsx'; import { FloatingResultsWindow } from '../../src/features/linked-results/FloatingResultsWindow'; import { makeSheet } from '../../src/importers/sheet-factory'; import { buildResultsWindowExcel } from '../../src/exports/results-window-excel'; import { resultsExcelContent } from '../../src/features/linked-results/results-excel-content'; import { downloadBrowserBlob } from '../../src/platform/browser/browser-download-adapter'; vi.mock('../../src/platform/browser/browser-download-adapter', () => ({ downloadBrowserBlob: vi.fn() })); vi.mock('../../src/app/AppProvider', () => ({ useOptionalApp: () => undefined })); afterEach(() => { vi.clearAllMocks(); vi.unstubAllGlobals(); }); describe.each(['dark', 'light'])('complete Results Excel (%s)', theme => { it('keeps every linked, Markdown and HTML row; exports all content or exactly the checked tables', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); vi.stubGlobal('ResizeObserver', class { observe() {} disconnect() {} }); document.documentElement.dataset.theme = theme; const data = Array.from({ length: 1001 }, (_, index) => [`OBJECT-${index + 1}`, index === 1000 ? '=literal-evidence' : String(index)]); const sheet = makeSheet('Full evidence', [['Object', 'Value'], ...data], '', 'synthetic.xlsx'); const markdown = '| Object | Status |\n| --- | --- |\n' + data.map(([name]) => `| ${name} | Failed |`).join('\n'); const html = '

Original source note

' + data.map(([name]) => ``).join('') + '
Object
${name}
Hidden technical source
'; const sections = [{ title: 'Markdown evidence', value: markdown }, { title: 'HTML evidence', value: html }, { title: 'Plain evidence', value: 'Complete result prose' }]; const before = JSON.stringify({ sheet, sections }); const host = document.createElement('div'); document.body.append(host); const root = createRoot(host); try { await act(async () => root.render( {}} onClose={() => {}} onToggleBookmark={() => {}}/>)); const tables = [...host.querySelectorAll('table')]; expect(tables).toHaveLength(3); expect(tables.map(table => table.tBodies[0].rows.length)).toEqual([1001, 1001, 1001]); const all = host.querySelector('[aria-label="Export entire Results to Excel"]')!; const selected = host.querySelector('[aria-label="Export selected tables to Excel"]')!; expect(all.disabled).toBe(false); expect(selected.disabled).toBe(true); expect(all.previousElementSibling?.querySelector('svg')).not.toBeNull(); await act(async () => all.click()); expect(downloadBrowserBlob).toHaveBeenCalledOnce(); expect(vi.mocked(downloadBrowserBlob).mock.calls[0][1]).toMatch(/-all\.xlsx$/); const snapshot = resultsExcelContent(host.querySelector('.results-window-body')!, 'Full synthetic result', sections); const full = read(buildResultsWindowExcel(snapshot), { type: 'array' }); const tableNames = full.SheetNames.filter(name => /^Results(?: \d+)?$/.test(name)); expect(tableNames).toHaveLength(3); for (const name of tableNames) { const rows = utils.sheet_to_json(full.Sheets[name], { header: 1 }); expect(rows).toHaveLength(name === 'Results' ? 1006 : 1003); expect(rows[1002][0]).toBe('OBJECT-1001'); expect(rows[0][0]).toContain('Full synthetic result'); } expect(full.Sheets[tableNames[2]].B1003.t).toBe('s'); expect(full.Sheets[tableNames[2]].B1003.f).toBeUndefined(); expect(full.Sheets.Results.A1006.v).toContain('Complete result prose'); expect(full.SheetNames).not.toContain('Results text'); const overflow = utils.sheet_to_json(full.Sheets['Long text'], { header: 1 }).slice(2); const sourceValue = full.Sheets['Source text'].B4.v; expect(sourceValue === html ? sourceValue : overflow.filter(row => row[0] === 'Source text' && row[1] === '4' && row[2] === '2').map(row => row[4]).join('')).toBe(html); expect(full.Sheets.Results.A1006.v).not.toContain('Select table for Excel export'); const checkboxes = [...host.querySelectorAll('input[type="checkbox"]')]; await act(async () => checkboxes[1].click()); expect(selected.disabled).toBe(false); await act(async () => selected.click()); expect(vi.mocked(downloadBrowserBlob).mock.calls.at(-1)![1]).toMatch(/-selected\.xlsx$/); const subset = read(buildResultsWindowExcel(snapshot, new Set([tables[1].dataset.resultTableId!])), { type: 'array' }); expect(subset.SheetNames).toHaveLength(2); expect(subset.SheetNames).not.toContain('Source text'); expect(utils.sheet_to_json(subset.Sheets[subset.SheetNames[0]], { header: 1 })).toHaveLength(1003); expect(subset.SheetNames[1]).toBe('Export info'); await act(async () => checkboxes[1].click()); expect(selected.disabled).toBe(true); expect(JSON.stringify({ sheet, sections })).toBe(before); } finally { await act(async () => root.unmount()); host.remove(); } }, 30000); }); it('preserves long Unicode cells, literal formulas, leading zeros and duplicate invalid sheet names without loss', () => { const long = 'x'.repeat(32766) + '๐Ÿงช' + 'tail'.repeat(1000); const content = { title: 'Synthetic', text: long, sourceSections: [], tables: [ { id: 'a', title: 'Duplicate/[name]'.repeat(4), rows: [['Value'], ['0000123'], ['=1+1'], [long]] }, { id: 'b', title: 'Duplicate/[name]'.repeat(4), rows: [['Value'], ['Second']] } ] }; const workbook = read(buildResultsWindowExcel(content), { type: 'array' }); expect(new Set(workbook.SheetNames).size).toBe(workbook.SheetNames.length); expect(workbook.SheetNames.every(name => name.length <= 31 && !/[\\/?:*\[\]]/.test(name))).toBe(true); const table = workbook.Sheets[workbook.SheetNames[0]]; expect(table.A3.v).toBe('0000123'); expect(table.A4.v).toBe('=1+1'); expect(table.A4.f).toBeUndefined(); const parts = utils.sheet_to_json(workbook.Sheets['Long text'], { header: 1 }).slice(2); expect(parts.filter(row => row[0] === 'Results' && row[1] === '5').map(row => row[4]).join('')).toBe(long); expect(parts.filter(row => row[0] === 'Results' && row[1] === '8').map(row => row[4]).join('')).toBe(long); expect(() => buildResultsWindowExcel(content, new Set())).toThrow('Select at least one table'); }); it('keeps the complete export active for a text-only result', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); vi.stubGlobal('ResizeObserver', class { observe() {} disconnect() {} }); const host = document.createElement('div'); document.body.append(host); const root = createRoot(host); try { await act(async () => root.render( {}} onClose={() => {}}/>)); const all = host.querySelector('[aria-label="Export entire Results to Excel"]')!; expect(all.disabled).toBe(false); await act(async () => all.click()); expect(downloadBrowserBlob).toHaveBeenCalledOnce(); expect(host.querySelector('[aria-label="Export selected tables to Excel"]')!.disabled).toBe(true); } finally { await act(async () => root.unmount()); host.remove(); } }); it('isolates table selection per Results window and disables selective export when a selected table disappears', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); vi.stubGlobal('ResizeObserver', class { observe() {} disconnect() {} }); const sheet = makeSheet('Synthetic', [['Value'], ['Evidence']], '', 'synthetic.csv'); const host = document.createElement('div'); document.body.append(host); const root = createRoot(host); const window = (id: string, sheets = [sheet]) => {}} onClose={() => {}}/>; try { await act(async () => root.render(<>{window('first')}{window('second')})); const dialogs = host.querySelectorAll('[role="dialog"]'); await act(async () => dialogs[0].querySelector('input')!.click()); const selected = (dialog: Element) => dialog.querySelector('[aria-label="Export selected tables to Excel"]')!; expect(selected(dialogs[0]).disabled).toBe(false); expect(selected(dialogs[1]).disabled).toBe(true); await act(async () => root.render(<>{window('first', [])}{window('second')})); expect(selected(dialogs[0]).disabled).toBe(true); } finally { await act(async () => root.unmount()); host.remove(); } }); it('retains completely empty evidence rows in the spreadsheet extent', () => { const bytes = buildResultsWindowExcel({ title: 'Empty rows', text: '', sourceSections: [], tables: [ { id: 'blank', title: 'Evidence', rows: [['Value'], [''], ['Evidence'], ['']] } ] }); const workbook = read(bytes, { type: 'array' }); expect(utils.sheet_to_json(workbook.Sheets.Results, { header: 1, defval: '' })).toEqual([['Empty rows\nEvidence'], ['Value'], [''], ['Evidence'], ['']]); });