// @vitest-environment jsdom import { describe, expect, it } from 'vitest'; import * as XLSX from 'xlsx'; import { parseCsvDocument } from '../../src/importers/csv-importer'; import { parseExcelDocument } from '../../src/importers/excel-importer'; import { parseHtmlDocument } from '../../src/importers/html-importer'; import { parseJsonDocument } from '../../src/importers/json-importer'; import { createImportTable } from '../../src/importers/intermediate'; import { importXml, parseXmlDocument } from '../../src/importers/xml-importer'; import { browserMarkupDocumentAdapter } from '../../src/platform/browser/import-runtime-adapters'; import { ImportError } from '../../src/shared/errors'; import { binarySource, sourceMetadata } from './import-source-fixtures'; describe('universal import intermediate model', () => { it('represents CSV, HTML and Excel tables with the same downstream structure', async () => { const csvText = 'Name;Severity;Weight\nSynthetic finding;High;7'; const csv = parseCsvDocument(sourceMetadata('findings.csv', csvText.length, 'text/csv'), csvText); const htmlText = '
Findings
NameSeverityWeight
Synthetic findingHigh7
'; const html = parseHtmlDocument( sourceMetadata('findings.html', htmlText.length, 'text/html'), htmlText, browserMarkupDocumentAdapter ); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet([ ['Name', 'Severity', 'Weight'], ['Synthetic finding', 'High', 7] ]), 'Findings'); const excelBytes = XLSX.write(workbook, { type: 'array', bookType: 'xlsx' }); const excel = await parseExcelDocument(binarySource('findings.xlsx', new Uint8Array(excelBytes))); for (const document of [csv, html, excel]) { expect(document.version).toBe(1); expect(document.status).toBe('complete'); expect(document.tables).toHaveLength(1); expect(document.tables[0].columns).toHaveLength(3); expect(document.tables[0].headerCandidates[0]).toMatchObject({ rowIndex: 0, headers: ['Name', 'Severity', 'Weight'] }); expect(document.tables[0].rows[1].cells.map(cell => cell.decodedValue)).toEqual([ 'Synthetic finding', 'High', '7' ]); expect(document.tables[0].rows[1].cells[2].detectedType).toBe('number'); expect(document.tables[0].rows[1].cells[2].source).toMatchObject({ fileName: document.file.name, tableIndex: document.tables[0].index, rowIndex: 1, columnIndex: 2 }); } expect(csv.tables[0].rows[1].cells[2].rawValue).toBe('7'); expect(excel.tables[0].rows[1].cells[2].rawValue).toBe(7); expect(excel.tables[0].rows[1].cells[2].source.cellReference).toBe('C2'); expect(html.tables[0].rows[1].cells[2].source.elementSelector).toContain('table:nth-of-type(1)'); for (const document of [csv, html, excel]) { expect(document.views[0]).toMatchObject({ kind: 'table', tableId: document.tables[0].id, source: { fileName: document.file.name, tableIndex: document.tables[0].index }, provenance: { version: 1, processor: { kind: 'format-adapter', version: '1.0.0' } } }); expect(document.processing.map(item => item.stage)).toEqual(['format-decoding', 'structure-decoding']); expect(document.processing[0]).toMatchObject({ stage: 'format-decoding', status: 'complete', diagnosticCodes: [] }); expect(document.processing[1]).toMatchObject({ stage: 'structure-decoding', status: document.status }); } }); it('preserves native object, hierarchy and HTML text views beside compatibility tables', () => { const jsonText = JSON.stringify({ metadata: { tenant: 'synthetic' }, findings: [{ id: 'SYN-1', enabled: true }] }); const json = parseJsonDocument(sourceMetadata('nested.json', jsonText.length, 'application/json'), jsonText); const objectView = json.views.find(view => view.kind === 'object'); expect(objectView).toMatchObject({ kind: 'object', rootKind: 'object', value: { metadata: { tenant: 'synthetic' }, findings: [{ id: 'SYN-1', enabled: true }] }, source: { fileName: 'nested.json' } }); expect(json.views.find(view => view.kind === 'table')?.provenance.transformations).toContainEqual( expect.objectContaining({ kind: 'flatten-for-compatibility', lossless: false }) ); const xmlText = 'Synthetic'; const xml = parseXmlDocument(sourceMetadata('nested.xml', xmlText.length, 'application/xml'), xmlText, browserMarkupDocumentAdapter); const hierarchyView = xml.views.find(view => view.kind === 'hierarchy'); expect(hierarchyView).toMatchObject({ kind: 'hierarchy', name: 'report', source: { fileName: 'nested.xml' } }); if (hierarchyView?.kind !== 'hierarchy') throw new Error('Expected hierarchy view.'); expect(hierarchyView.nodes).toContainEqual(expect.objectContaining({ path: 'report/finding[1]/title[1]', name: 'title', value: 'Synthetic', source: { fileName: 'nested.xml', objectPath: 'report/finding[1]/title[1]' } })); const htmlText = '

Synthetic introduction

Name
Finding
'; const html = parseHtmlDocument(sourceMetadata('mixed.html', htmlText.length, 'text/html'), htmlText, browserMarkupDocumentAdapter); expect(html.views).toContainEqual(expect.objectContaining({ kind: 'table' })); expect(html.views).toContainEqual(expect.objectContaining({ kind: 'text', text: expect.stringContaining('Synthetic introduction') })); }); it('returns recoverable parser warnings and discarded content structurally', async () => { const csvText = 'Name;Evidence\nFinding;"not closed'; const csv = parseCsvDocument(sourceMetadata('partial.csv', csvText.length), csvText); expect(csv.status).toBe('partial'); expect(csv.diagnostics).toContainEqual(expect.objectContaining({ code: 'csv-unclosed-quote', severity: 'warning' })); const htmlText = '
Name
Safe
'; const html = parseHtmlDocument( sourceMetadata('untrusted.html', htmlText.length), htmlText, browserMarkupDocumentAdapter ); expect(html.status).toBe('partial'); expect(html.discardedContent).toContainEqual(expect.objectContaining({ kind: 'active-html-content', count: 1 })); const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet([]), 'Empty'); const excelBytes = XLSX.write(workbook, { type: 'array', bookType: 'xlsx' }); const excel = await parseExcelDocument(binarySource('empty.xlsx', new Uint8Array(excelBytes))); expect(excel.status).toBe('partial'); expect(excel.discardedContent).toContainEqual(expect.objectContaining({ kind: 'empty-sheet' })); }); it('attaches structured diagnostics to controlled parse errors', () => { const text = ''; const source = sourceMetadata('broken.xml', text.length, 'application/xml'); try { importXml(source, text, browserMarkupDocumentAdapter); throw new Error('Expected XML import to fail.'); } catch (error) { expect(error).toBeInstanceOf(ImportError); expect((error as ImportError).diagnostics).toContainEqual(expect.objectContaining({ code: 'xml-parse-error', severity: 'error' })); } }); it('builds a supported six-figure row model without spreading rows onto the call stack', () => { const rows = Array.from({ length: 140_000 }, () => [] as string[]); const table = createImportTable({ sourceFile: 'large.html', name: 'Large table', index: 0, decodedRows: rows }); expect(table.rows).toHaveLength(140_000); expect(table.columns).toHaveLength(0); }); });