// @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
| Object |
' + data.map(([name]) => `| ${name} |
`).join('') + '
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'], ['']]);
});