import { test, expect } from '@playwright/test'; import { MOCK_FS_INIT_SCRIPT } from './fixtures/mock-fs-api.js'; import * as path from 'path'; const HTML_PATH = path.resolve('archive/dist/archive.html'); test.describe('Archive Browser', () => { test.beforeEach(async ({ page }) => { await page.addInitScript(MOCK_FS_INIT_SCRIPT); }); test('loads without errors', async ({ page }) => { await page.goto(`file://${HTML_PATH}`, { waitUntil: 'domcontentloaded' }); await page.waitForSelector('#appContainer', { timeout: 15000 }); // Page title contains "Archive" await expect(page).toHaveTitle(/Archive/i); // No-directory message is shown before any directory is opened await expect(page.locator('#noDirectoryMessage')).toBeVisible(); // The open-directory button is present await expect(page.locator('#addDirectoryBtn')).toBeVisible(); }); test('scans a mock directory tree and displays files', async ({ page }) => { await page.goto(`file://${HTML_PATH}`, { waitUntil: 'domcontentloaded' }); await page.waitForSelector('#addDirectoryBtn', { timeout: 15000 }); // Log to see if the scan is working page.on('console', msg => { console.log('Browser console:', msg.text()); }); page.on('pageerror', err => { console.log('Page error:', err.message); }); // The archive expects a two-level structure: root → transmittal-folder → files. // Flat files at the root are not counted — they must be inside subdirectories. await page.evaluate(() => { window.__setMockDirectoryTree('test-project', { '2025-01-15_123456-EM-TRN-0001 (IFC) - First Transmittal': { '123456-EL-SPC-2623_A (IFC) - Specification.pdf': '%PDF', '123456-EL-DRW-0001_B (IFR) - Drawing.dwg': 'DWG', }, '2025-02-10_123456-EM-TRN-0002 (IFC) - Second Transmittal': { '789012-ME-CAL-0001_A (IFA) - Calculation.pdf': '%PDF', }, }); console.log('Mock directory set up'); }); await page.locator('#addDirectoryBtn').click(); // Wait for scanning to complete await page.waitForTimeout(2000); // Log state for debugging await page.evaluate(() => { console.log('After scanning:'); console.log(' window.app exists:', typeof window.app !== 'undefined'); if (typeof window.app !== 'undefined') { console.log(' window.app type:', typeof window.app); console.log(' window.app constructor:', window.app.constructor.name); console.log(' window.app.directories:', Array.isArray(window.app.directories) ? window.app.directories.length : window.app.directories); console.log(' window.app.files:', Array.isArray(window.app.files) ? window.app.files.length : window.app.files); console.log(' window.app.filteredFiles:', Array.isArray(window.app.filteredFiles) ? window.app.filteredFiles.length : window.app.filteredFiles); console.log(' window.app modules:', Object.keys(window.app || {}).filter(k => k.startsWith('modules'))); } else { console.log(' window.app is undefined!'); } }); // Select all grouping folders so files are included in the file list await page.evaluate(() => { const cb = document.getElementById('selectAllGroupingCheckbox'); if (cb && !cb.checked) cb.click(); }); await page.waitForTimeout(500); // The files table should have at least one data row const rowCount = await page.locator('#filesTableBody tr').count(); expect(rowCount).toBeGreaterThanOrEqual(1); // Status bar should show files were found (any non-empty text is fine) const fileCountText = await page.locator('#fileCount').textContent(); expect(fileCountText).toBeTruthy(); }); test('Mode 1: ?projects=A,B enters each project\'s Archive subfolder', async ({ page }) => { // Multi-project layout: server root holds project folders, each containing an // Archive/ subfolder with third-party folders. ?projects=A,B (set as // window.app.projectFilter; url-state.restore() handles parsing in real flow) // selects the projects to scan. Each is descended into /Archive/. await page.goto(`file://${HTML_PATH}`, { waitUntil: 'domcontentloaded' }); await page.waitForSelector('#addDirectoryBtn', { timeout: 15000 }); await page.evaluate(() => { window.__setMockDirectoryTree('combined-root', { 'Project-A': { 'Archive': { 'ACME': { '2025-01-15_123456-EM-TRN-0001 (IFC) - First': { '123456-EL-SPC-0001_A (IFC) - SpecA.pdf': '%PDF', }, }, }, }, 'Project-B': { 'Archive': { 'Beta': { '2025-02-10_789012-EM-TRN-0001 (IFC) - Second': { '789012-EL-SPC-0002_A (IFC) - SpecB.pdf': '%PDF', }, }, }, }, 'Project-C': { 'Archive': { 'Gamma': { '2025-03-01_345678-EM-TRN-0001 (IFC) - Third': { '345678-EL-SPC-0003_A (IFC) - SpecC.pdf': '%PDF', }, }, }, }, }); }); await page.evaluate(() => { window.app.projectFilter = new Set(['Project-A', 'Project-B']); }); await page.locator('#addDirectoryBtn').click(); await page.waitForTimeout(2000); // Third-party (grouping) folders should be ACME and Beta — never project IDs or "Archive" const groupingFolders = await page.evaluate(() => window.app.groupingFolders.map(f => f.name) ); expect(groupingFolders).toContain('ACME'); expect(groupingFolders).toContain('Beta'); expect(groupingFolders).not.toContain('Project-A'); expect(groupingFolders).not.toContain('Project-B'); expect(groupingFolders).not.toContain('Archive'); expect(groupingFolders).not.toContain('Gamma'); await page.evaluate(() => { const cb = document.getElementById('selectAllGroupingCheckbox'); if (cb && !cb.checked) cb.click(); }); await page.waitForTimeout(500); const tableText = await page.locator('#filesTableBody').textContent(); expect(tableText).toContain('SpecA'); expect(tableText).toContain('SpecB'); expect(tableText).not.toContain('SpecC'); }); test('same-name third-parties across projects merge into one party row', async ({ page }) => { // Both projects have a "BM" third-party folder. The parties pane must // show "BM" once, and selecting it must surface files from both projects. await page.goto(`file://${HTML_PATH}`, { waitUntil: 'domcontentloaded' }); await page.waitForSelector('#addDirectoryBtn', { timeout: 15000 }); await page.evaluate(() => { window.__setMockDirectoryTree('combined-root', { '176109': { 'Archive': { 'BM': { 'Issued': { '2025-01-15_123456-EM-TRN-0001 (IFC) - First': { '123456-EL-SPC-0001_A (IFC) - SpecFromA.pdf': '%PDF', }, }, }, }, }, '197072': { 'Archive': { 'BM': { 'Issued': { '2025-02-10_789012-EM-TRN-0001 (IFC) - Second': { '789012-EL-SPC-0002_A (IFC) - SpecFromB.pdf': '%PDF', }, }, }, }, }, }); }); await page.evaluate(() => { window.app.projectFilter = new Set(['176109', '197072']); }); await page.locator('#addDirectoryBtn').click(); await page.waitForTimeout(2000); // The DOM should render exactly one BM row, not two. const renderedNames = await page.locator('#groupingFoldersList .folder-item-name').allTextContents(); const bmRows = renderedNames.filter(n => n.trim() === 'BM'); expect(bmRows.length).toBe(1); // Both projects' files should be visible under the single BM party. const tableText = await page.locator('#filesTableBody').textContent(); expect(tableText).toContain('SpecFromA'); expect(tableText).toContain('SpecFromB'); }); test('toggling visibleProjects hides files without re-scanning', async ({ page }) => { // Two projects scanned. Toggling 197072 out of visibleProjects should // hide its files; window.app.files (the in-memory scan result) must NOT // shrink — only the visible/filtered set changes. await page.goto(`file://${HTML_PATH}`, { waitUntil: 'domcontentloaded' }); await page.waitForSelector('#addDirectoryBtn', { timeout: 15000 }); await page.evaluate(() => { window.__setMockDirectoryTree('combined-root', { '176109': { 'Archive': { 'BM': { 'Issued': { '2025-01-15_123456-EM-TRN-0001 (IFC) - First': { '123456-EL-SPC-0001_A (IFC) - SpecFromA.pdf': '%PDF', }, }, }, }, }, '197072': { 'Archive': { 'BM': { 'Issued': { '2025-02-10_789012-EM-TRN-0001 (IFC) - Second': { '789012-EL-SPC-0002_A (IFC) - SpecFromB.pdf': '%PDF', }, }, }, }, }, }); }); await page.evaluate(() => { window.app.projectFilter = new Set(['176109', '197072']); window.app.visibleProjects = new Set(['176109', '197072']); window.app.isMultiProject = true; }); await page.locator('#addDirectoryBtn').click(); await page.waitForTimeout(2000); const beforeFileCount = await page.evaluate(() => window.app.files.length); expect(beforeFileCount).toBeGreaterThanOrEqual(2); // Toggle 197072 out of the visible set — must NOT shrink window.app.files. await page.evaluate(() => { window.app.visibleProjects = new Set(['176109']); window.app.modules.app.updateUI(); window.app.modules.filtering.applyFilters(); }); const afterFileCount = await page.evaluate(() => window.app.files.length); expect(afterFileCount).toBe(beforeFileCount); const tableText = await page.locator('#filesTableBody').textContent(); expect(tableText).toContain('SpecFromA'); expect(tableText).not.toContain('SpecFromB'); // Toggle 197072 back — file reappears immediately, no rescan needed. await page.evaluate(() => { window.app.visibleProjects = new Set(['176109', '197072']); window.app.modules.app.updateUI(); window.app.modules.filtering.applyFilters(); }); const tableTextAfter = await page.locator('#filesTableBody').textContent(); expect(tableTextAfter).toContain('SpecFromA'); expect(tableTextAfter).toContain('SpecFromB'); }); test('?show= URL param round-trips: serialize emits it, restore reads it', async ({ page }) => { await page.goto(`file://${HTML_PATH}`, { waitUntil: 'domcontentloaded' }); await page.waitForSelector('#addDirectoryBtn', { timeout: 15000 }); // Serialize: visibleProjects ⊊ projectFilter → ?show= present const qsWithSubset = await page.evaluate(() => { window.app.projectFilter = new Set(['A', 'B', 'C']); window.app.visibleProjects = new Set(['A']); return window.app.modules.urlState.serialize(); }); expect(qsWithSubset).toContain('projects=A%2CB%2CC'); expect(qsWithSubset).toContain('show=A'); // Serialize: visibleProjects equals projectFilter → ?show= omitted const qsAllVisible = await page.evaluate(() => { window.app.projectFilter = new Set(['A', 'B']); window.app.visibleProjects = new Set(['A', 'B']); return window.app.modules.urlState.serialize(); }); expect(qsAllVisible).not.toContain('show='); // Restore: ?show= populates visibleProjects from URL. await page.goto(`file://${HTML_PATH}?projects=A,B,C&show=A,B`, { waitUntil: 'domcontentloaded' }); await page.waitForSelector('#addDirectoryBtn', { timeout: 15000 }); const restored = await page.evaluate(() => { // Re-run restore in case the early init() raced with DOM availability — // matches how the existing projectFilter test bypasses this path too. window.app.modules.urlState.restore(); return { search: location.search, projectFilter: Array.from(window.app.projectFilter).sort(), visibleProjects: Array.from(window.app.visibleProjects).sort(), }; }); expect(restored.search).toBe('?projects=A,B,C&show=A,B'); expect(restored.projectFilter).toEqual(['A', 'B', 'C']); expect(restored.visibleProjects).toEqual(['A', 'B']); }); test('Mode 2: project-root with Archive child auto-descends into Archive only', async ({ page }) => { // Picked directory is a project root containing Archive/ plus Reviewing/. // The app should enter Archive/ automatically and ignore Reviewing/. await page.goto(`file://${HTML_PATH}`, { waitUntil: 'domcontentloaded' }); await page.waitForSelector('#addDirectoryBtn', { timeout: 15000 }); await page.evaluate(() => { window.__setMockDirectoryTree('project-176109', { 'Archive': { 'ACME': { '2025-01-15_123456-EM-TRN-0001 (IFC) - First': { '123456-EL-SPC-0001_A (IFC) - InArchive.pdf': '%PDF', }, }, }, 'Reviewing': { 'ShouldNotShow': { '2025-02-10_789012-EM-TRN-0001 (IFC) - HiddenStage': { '789012-EL-SPC-0002_A (IFC) - InReviewing.pdf': '%PDF', }, }, }, }); }); await page.locator('#addDirectoryBtn').click(); await page.waitForTimeout(2000); const groupingFolders = await page.evaluate(() => window.app.groupingFolders.map(f => f.name) ); expect(groupingFolders).toContain('ACME'); expect(groupingFolders).not.toContain('Archive'); expect(groupingFolders).not.toContain('Reviewing'); expect(groupingFolders).not.toContain('ShouldNotShow'); const fileNames = await page.evaluate(() => window.app.files.map(f => f.name)); expect(fileNames.some(n => n.includes('InArchive'))).toBe(true); expect(fileNames.some(n => n.includes('InReviewing'))).toBe(false); }); test('Mode 3: in-archive layout (no projects=, no Archive child) uses today\'s scan', async ({ page }) => { // Picked directory IS the Archive folder — children are third-parties directly. // Behavior must match pre-change baseline. await page.goto(`file://${HTML_PATH}`, { waitUntil: 'domcontentloaded' }); await page.waitForSelector('#addDirectoryBtn', { timeout: 15000 }); await page.evaluate(() => { window.__setMockDirectoryTree('Archive', { 'ACME': { 'Issued': { '2025-01-15_123456-EM-TRN-0001 (IFC) - First': { '123456-EL-SPC-0001_A (IFC) - SpecA.pdf': '%PDF', }, }, }, 'Beta': { 'Received': { '2025-02-10_789012-EM-TRN-0001 (IFC) - Second': { '789012-EL-SPC-0002_A (IFC) - SpecB.pdf': '%PDF', }, }, }, }); }); await page.locator('#addDirectoryBtn').click(); await page.waitForTimeout(2000); const groupingFolders = await page.evaluate(() => window.app.groupingFolders.map(f => f.name) ); expect(groupingFolders).toContain('ACME'); expect(groupingFolders).toContain('Beta'); await page.evaluate(() => { const cb = document.getElementById('selectAllGroupingCheckbox'); if (cb && !cb.checked) cb.click(); }); await page.waitForTimeout(500); const tableText = await page.locator('#filesTableBody').textContent(); expect(tableText).toContain('SpecA'); expect(tableText).toContain('SpecB'); }); test('disabled folder types are skipped at scan time (not just hidden in UI)', async ({ page }) => { // Default enabledFolderTypes is {issued, received}. The Incoming folder should // never be listed — its files must not appear in window.app.files at all. await page.goto(`file://${HTML_PATH}`, { waitUntil: 'domcontentloaded' }); await page.waitForSelector('#addDirectoryBtn', { timeout: 15000 }); await page.evaluate(() => { window.__setMockDirectoryTree('Archive', { 'ACME': { 'Issued': { '2025-01-15_123456-EM-TRN-0001 (IFC) - First': { '123456-EL-SPC-0001_A (IFC) - SpecIssued.pdf': '%PDF', }, }, 'Incoming': { '2025-02-10_789012-EM-TRN-0001 (IFC) - Second': { '789012-EL-SPC-0002_A (IFC) - SpecIncoming.pdf': '%PDF', }, }, }, }); }); await page.locator('#addDirectoryBtn').click(); await page.waitForTimeout(2000); const fileNames = await page.evaluate(() => window.app.files.map(f => f.name)); expect(fileNames.some(n => n.includes('SpecIssued'))).toBe(true); // Incoming is in FOLDER_TYPE_NAMES but not in default enabledFolderTypes — must be skipped expect(fileNames.some(n => n.includes('SpecIncoming'))).toBe(false); }); test('Preview toggle is checked by default', async ({ page }) => { await page.goto(`file://${HTML_PATH}`, { waitUntil: 'domcontentloaded' }); // The checkbox lives inside the (hidden) preview-controls area // before any directory is loaded — waitForSelector defaults to // 'visible' which would time out. Wait for it to be ATTACHED // and verify the underlying state instead. await page.waitForSelector('#filePreviewToggle', { state: 'attached', timeout: 15000 }); await expect(page.locator('#filePreviewToggle')).toBeChecked(); }); test('default modifier filter selects only base and +C revisions', async ({ page }) => { // Files with mixed revision modifiers — by default only base + +C should // surface in the visible file table; +B (and any other non-+C modifier) // should be hidden until the user explicitly opts in. await page.goto(`file://${HTML_PATH}`, { waitUntil: 'domcontentloaded' }); await page.waitForSelector('#addDirectoryBtn', { timeout: 15000 }); await page.evaluate(() => { window.__setMockDirectoryTree('test-project', { '2025-01-15_123456-EM-TRN-0001 (IFC) - Mixed Mods': { '123456-EL-SPC-0001_A (IFC) - SpecBase.pdf': '%PDF', '123456-EL-SPC-0001_A+C1 (IFC) - SpecComment.pdf': '%PDF', '123456-EL-SPC-0001_A+B1 (IFC) - SpecScratch.pdf': '%PDF', '123456-EL-SPC-0001_A+D1 (IFC) - SpecDraft.pdf': '%PDF', }, }); }); await page.locator('#addDirectoryBtn').click(); await page.waitForTimeout(2000); await page.evaluate(() => { const cb = document.getElementById('selectAllGroupingCheckbox'); if (cb && !cb.checked) cb.click(); }); await page.waitForTimeout(300); const state = await page.evaluate(() => ({ available: Array.from(window.app.availableModifiers).sort(), selected: Array.from(window.app.selectedModifiers).sort(), visibleNames: window.app.filteredFiles.map(f => f.name), })); // The dropdown still LISTS all modifiers found in the data… expect(state.available).toEqual(['+B', '+C', '+D', 'base']); // …but only base + +C are pre-selected by default. expect(state.selected).toEqual(['+C', 'base']); // Files with hidden modifier types must be absent from the table. expect(state.visibleNames.some(n => n.includes('SpecBase'))).toBe(true); expect(state.visibleNames.some(n => n.includes('SpecComment'))).toBe(true); expect(state.visibleNames.some(n => n.includes('SpecScratch'))).toBe(false); expect(state.visibleNames.some(n => n.includes('SpecDraft'))).toBe(false); }); test('parser module uses shared zddc helpers (not its own wrappers)', async ({ page }) => { await page.goto(`file://${HTML_PATH}`, { waitUntil: 'domcontentloaded' }); await page.waitForSelector('#appContainer', { timeout: 15000 }); const probe = await page.evaluate(() => ({ // archive's parser module should NOT re-export wrappers we removed hasParseFileNameWrapper: typeof window.app.modules.parser?.parseFileName === 'function', hasParseRevisionWrapper: typeof window.app.modules.parser?.parseRevision === 'function', // but should still expose archive-specific helpers hasIsTransmittalFolder: typeof window.app.modules.parser?.isTransmittalFolder === 'function', hasGroupFiles: typeof window.app.modules.parser?.groupFilesByTrackingNumber === 'function', // and isTransmittalFolder should agree with shared zddc.parseFolder validFolder: window.app.modules.parser.isTransmittalFolder('2025-10-31_123456-EM-SUB-0001 (IFR) - General Arrangement'), invalidFolder: window.app.modules.parser.isTransmittalFolder('not-a-zddc-folder'), })); expect(probe.hasParseFileNameWrapper).toBe(false); expect(probe.hasParseRevisionWrapper).toBe(false); expect(probe.hasIsTransmittalFolder).toBe(true); expect(probe.hasGroupFiles).toBe(true); expect(probe.validFolder).toBe(true); expect(probe.invalidFolder).toBe(false); }); });