import { test, expect } from '@playwright/test'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; // "Add from archive" for the tables tool's project MDL rollup. The page is // loaded offline (file://) with an injected #table-context whose columns drive // how a tracking number splits into deliverable fields. The walk / dedupe / // instantiate logic is exercised against in-page mock FS-Access handles — no // server needed. const HTML_PATH = path.resolve('tables/dist/tables.html'); const HTML_RAW = fs.readFileSync(HTML_PATH, 'utf8'); // originator … identity fields … title (originator is folder-pinned → omitted // from the body; everything between originator and title is the tracking split). const MDL_COLUMNS = [ { field: 'originator', title: 'Orig' }, { field: 'phase', title: 'Phase' }, { field: 'project', title: 'Project' }, { field: 'area', title: 'Area' }, { field: 'discipline', title: 'Disc' }, { field: 'type', title: 'Type' }, { field: 'sequence', title: 'Seq' }, { field: 'suffix', title: 'Suffix' }, { field: 'title', title: 'Deliverable' }, ]; async function loadRollup(page) { const ctx = { title: 'MDL', columns: MDL_COLUMNS, rows: [], addable: false }; const ctxJson = JSON.stringify(ctx).replace(/<\//g, '<\\/'); const patched = HTML_RAW.replace( /`, ); const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tables-mdl-')); const tmpPath = path.join(tmpDir, 'tables.html'); fs.writeFileSync(tmpPath, patched); await page.goto(`file://${tmpPath}`, { waitUntil: 'load' }); await page.waitForFunction( () => window.tablesApp && window.tablesApp.modules && window.tablesApp.modules.mdlFromArchive, ); } test.describe('tables/ — Add deliverables from archive', () => { test('identityFields() = columns between originator and title', async ({ page }) => { await loadRollup(page); const fields = await page.evaluate(() => window.tablesApp.modules.mdlFromArchive.identityFields()); expect(fields).toEqual(['phase', 'project', 'area', 'discipline', 'type', 'sequence', 'suffix']); }); test('deliverableFromFile splits the tracking number, omits originator, keeps title', async ({ page }) => { await loadRollup(page); const d = await page.evaluate(() => { const m = window.tablesApp.modules.mdlFromArchive; return m.deliverableFromFile( { tracking: 'ACME-DD-PRJ-A1-CIV-DWG-001-X', title: 'Foundation Plan' }, m.identityFields(), ); }); expect(d.originator).toBe('ACME'); expect(d.tracking).toBe('ACME-DD-PRJ-A1-CIV-DWG-001-X'); expect(d.body).toEqual({ phase: 'DD', project: 'PRJ', area: 'A1', discipline: 'CIV', type: 'DWG', sequence: '001', suffix: 'X', title: 'Foundation Plan', }); // originator must NOT be in the body (server pins it from the folder). expect(d.body.originator).toBeUndefined(); }); test('a shorter tracking number leaves trailing identity fields unset', async ({ page }) => { await loadRollup(page); const d = await page.evaluate(() => { const m = window.tablesApp.modules.mdlFromArchive; // no suffix segment return m.deliverableFromFile({ tracking: 'ACME-DD-PRJ-A1-CIV-DWG-001', title: '' }, m.identityFields()); }); expect(d.body.sequence).toBe('001'); expect('suffix' in d.body).toBe(false); }); test('dedupe collapses duplicate tracking numbers, dropping unsplittable rows', async ({ page }) => { await loadRollup(page); const out = await page.evaluate(() => { const m = window.tablesApp.modules.mdlFromArchive; return m.dedupe([ { tracking: 'ACME-DD-PRJ-A1-CIV-DWG-001', title: 'a' }, { tracking: 'ACME-DD-PRJ-A1-CIV-DWG-001', title: 'a-dup' }, { tracking: 'ACME-DD-PRJ-A1-CIV-DWG-002', title: 'b' }, { tracking: 'NOPE', title: 'too short' }, ], m.identityFields()); }); expect(out.map(d => d.tracking)).toEqual([ 'ACME-DD-PRJ-A1-CIV-DWG-001', 'ACME-DD-PRJ-A1-CIV-DWG-002', ]); expect(out[0].body.title).toBe('a'); // first wins }); test('walkArchive collects valid document files, skipping mdl/rsk/dot/underscore dirs', async ({ page }) => { await loadRollup(page); const files = await page.evaluate(async () => { // Mock FS-Access directory handles. function dir(name, entries) { return { name, kind: 'directory', _entries: entries, async *values() { for (const e of entries) yield e; }, async getDirectoryHandle(n) { const e = entries.find(x => x.name === n && x.kind === 'directory'); if (!e) throw new DOMException('not found', 'NotFoundError'); return e; }, }; } const file = name => ({ name, kind: 'file' }); const root = dir('archive', [ dir('Acme', [ dir('issued', [ dir('2026-05-01_ACME-DD-PRJ-A1-CIV-DWG-001 (IFR) - Plan', [ file('ACME-DD-PRJ-A1-CIV-DWG-001_B (IFR) - Foundation Plan.pdf'), file('not-a-zddc-file.txt'), ]), ]), dir('mdl', [ file('ACME-DD-PRJ-A1-CIV-DWG-001.yaml') ]), // skipped dir('rsk', [ file('whatever_A (IFA) - x.pdf') ]), // skipped ]), dir('_system', [ file('ACME-DD-PRJ-A1-CIV-DWG-999_A (IFA) - hidden.pdf') ]), // skipped ]); const out = await window.tablesApp.modules.mdlFromArchive.walkArchive(root); return out.map(f => ({ tracking: f.tracking, party: f.party, slot: f.slot, rev: f.revision, title: f.title })); }); expect(files).toEqual([ { tracking: 'ACME-DD-PRJ-A1-CIV-DWG-001', party: 'Acme', slot: 'issued', rev: 'B', title: 'Foundation Plan' }, ]); }); test('instantiateOne writes a yaml on create, skips when it already exists', async ({ page }) => { await loadRollup(page); const result = await page.evaluate(async () => { const writes = []; function fileHandle(name, exists) { return { name, async createWritable() { return { async write(blob) { writes.push({ name, text: await blob.text() }); }, async close() {}, }; }, _exists: exists, }; } function mdlDir() { const present = {}; // tracking.yaml already there present['ACME-DD-PRJ-A1-CIV-DWG-002.yaml'] = true; return { async getFileHandle(n, opts) { if (opts && opts.create) return fileHandle(n, false); if (present[n]) return fileHandle(n, true); throw new DOMException('nf', 'NotFoundError'); }, }; } function originatorDir() { return { async getDirectoryHandle() { return mdlDir(); } }; } const archiveRoot = { async getDirectoryHandle() { return originatorDir(); } }; const m = window.tablesApp.modules.mdlFromArchive; const created = await m.instantiateOne(archiveRoot, { tracking: 'ACME-DD-PRJ-A1-CIV-DWG-001', originator: 'ACME', body: { phase: 'DD', project: 'PRJ', area: 'A1', discipline: 'CIV', type: 'DWG', sequence: '001', title: 'Plan' }, }); const skipped = await m.instantiateOne(archiveRoot, { tracking: 'ACME-DD-PRJ-A1-CIV-DWG-002', originator: 'ACME', body: { phase: 'DD', project: 'PRJ', area: 'A1', discipline: 'CIV', type: 'DWG', sequence: '002', title: 'Plan2' }, }); return { created, skipped, writes }; }); expect(result.created).toBe('created'); expect(result.skipped).toBe('skipped'); expect(result.writes.length).toBe(1); expect(result.writes[0].name).toBe('ACME-DD-PRJ-A1-CIV-DWG-001.yaml'); expect(result.writes[0].text).toContain('title: Plan'); expect(result.writes[0].text).toContain('discipline: CIV'); // originator must not be serialized into the body expect(result.writes[0].text).not.toContain('originator:'); }); test('the "From archive" button stays hidden when not on an /mdl/ rollup path', async ({ page }) => { await loadRollup(page); // file:// path is not /mdl/, so setup() must not reveal the button. const hidden = await page.evaluate(() => document.getElementById('table-add-from-archive').hidden); expect(hidden).toBe(true); }); });