ZDDC/tests/archive.spec.js
ZDDC 7904a99c21 feat(browse): markdown front-matter pane + TOC resizer; misc UX fixes
Markdown preview pane now surfaces YAML front-matter above the TOC as a
key/value list (definition list), so engineering documents with header
metadata (title, revision, status, etc.) show their identity at a glance
without opening the file in mdedit. Front-matter parsing handles both
scalar and array values; arrays render as comma-joined.

TOC pane is now resizable (4px col-resize handle on its left edge);
preserves the user's chosen width across re-renders inside a single
session.

mdedit welcome banner moved inside #welcome-screen so the "browse opens
md in this same editor" callout only shows when no file is open — it
was previously visible in every state which was noisy.

archive.spec.js: wait for #filePreviewToggle to be attached before
clicking, fixing a Playwright flake where the preview button hadn't
mounted yet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:30:26 -05:00

516 lines
24 KiB
JavaScript

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 <project>/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);
});
});