From 921713d0a4fa29667b8536f3d39a138537428070 Mon Sep 17 00:00:00 2001 From: ZDDC Date: Sat, 13 Jun 2026 12:02:51 -0500 Subject: [PATCH] =?UTF-8?q?chore:=20clear=20tech=20debt=20=E2=80=94=20gree?= =?UTF-8?q?n=20the=20suite=20+=20delete=20dead=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The full Playwright suite had 5 pre-existing failures (stale assertions for since-reworked behavior) and the classifier carried dead code from removed flows. Stale tests refreshed to current behavior: - toast.spec (×3): toast.js now STACKS (sticky/dismissible) rather than showing one at a time — assert stacking + the "Clear all" control, read the message from .zddc-toast__msg (the toast also holds a × button), and dismiss via the × (clicking the body no longer dismisses, by design). - browse.spec: "New folder/New file" moved from the toolbar into the context menu — drop the #newFolderBtn/#newFileBtn assertions (Sort + Show-hidden stay). - tokens.spec XSS guard: rewritten to the current apiActions modal flow (#api-create-btn → .api-modal → #table-root) instead of the long-gone inline #desc form. The escaping assertion now actually runs and confirms it holds. Dead code removed: - classifier .mdl-overlay* CSS (orphaned when the "MDL from archive" instantiate flow moved to the tables tool). - classify.js filesInNode() — defined + exported but called nowhere. - "From a list" naming: refreshed a stale "catalog" comment and renamed the 3 remaining "By existing:" test titles. Full suite now 340 passed / 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) --- classifier/css/layout.css | 11 ----------- classifier/js/classify.js | 11 +---------- classifier/js/target-tree.js | 10 +++++----- tests/browse.spec.js | 7 +++---- tests/classify.spec.js | 6 +++--- tests/toast.spec.js | 29 ++++++++++++++++++----------- tests/tokens.spec.js | 28 ++++++++++++++++------------ 7 files changed, 46 insertions(+), 56 deletions(-) diff --git a/classifier/css/layout.css b/classifier/css/layout.css index 0d44531..da979e6 100644 --- a/classifier/css/layout.css +++ b/classifier/css/layout.css @@ -628,17 +628,6 @@ input.tfile__name:focus { border-color: var(--primary); background: var(--bg); o .scratch-match__tn { font-family: var(--mono, monospace); } .scratch-match__conf { color: var(--text-muted); font-size: 0.72rem; width: 3rem; text-align: right; } -/* ── MDL-from-archive overlay ───────────────────────────────────────────── */ -.mdl-overlay { position: fixed; inset: 0; z-index: 1100; background: rgba(0,0,0,0.45); display: flex; align-items: center; justify-content: center; padding: 2rem 1rem; } -.mdl-overlay__box { background: var(--bg); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: 0 10px 40px rgba(0,0,0,0.3); width: 100%; max-width: 1000px; height: 80vh; display: flex; flex-direction: column; } -.mdl-overlay__head { display: flex; align-items: center; justify-content: space-between; padding: 0.75rem 1rem; border-bottom: 1px solid var(--border); } -.mdl-overlay__head h2 { margin: 0; font-size: 1.1rem; } -.mdl-overlay__close { background: none; border: none; font-size: 1.6rem; line-height: 1; color: var(--text-muted); cursor: pointer; padding: 0 0.4rem; } -.mdl-overlay__close:hover { color: var(--text); } -.mdl-overlay__status { padding: 0.4rem 1rem; color: var(--text-muted); font-size: 0.82rem; border-bottom: 1px solid var(--border); } -.mdl-overlay__table { flex: 1; min-height: 0; } -.mdl-overlay__foot { display: flex; justify-content: flex-end; gap: 0.5rem; padding: 0.75rem 1rem; border-top: 1px solid var(--border); } - /* ── Shared selectable + autofilter table (seltable) ────────────────────── */ .seltable { display: flex; flex-direction: column; min-height: 0; height: 100%; } .seltable__bar { display: flex; align-items: center; gap: 0.5rem; padding: 0.4rem 0.5rem; border-bottom: 1px solid var(--border); flex: 0 0 auto; } diff --git a/classifier/js/classify.js b/classifier/js/classify.js index feb0add..de7acb9 100644 --- a/classifier/js/classify.js +++ b/classifier/js/classify.js @@ -378,15 +378,6 @@ return out; } - // Files currently placed in a node (reverse lookup over all source files). - function filesInNode(nodeId, axis, allFiles) { - var field = axis === 'transmittal' ? 'transmittalNodeId' : 'trackingNodeId'; - return (allFiles || []).filter(function (f) { - var a = state.assignments[srcKeyForFile(f)]; - return a && a[field] === nodeId; - }); - } - // Per-file classification state for the left-tree markers. // 'excluded' | 'done' | 'tracking' | 'transmittal' | 'partial' | 'none' function fileState(file) { @@ -858,7 +849,7 @@ getNode: getNode, getTrackingTree: function () { return state.trackingTree; }, getTransmittalTree: function () { return state.transmittalTree; }, // derive + reverse - deriveTarget: deriveTarget, filesInNode: filesInNode, + deriveTarget: deriveTarget, fileState: fileState, stats: stats, // persistence serialize: serialize, load: load, reset: reset, diff --git a/classifier/js/target-tree.js b/classifier/js/target-tree.js index 2a7db34..4195f1c 100644 --- a/classifier/js/target-tree.js +++ b/classifier/js/target-tree.js @@ -601,11 +601,11 @@ }); } - // Load the catalog: "Load…" opens a multi-select directory tree (scoped to - // the served context); every ticked directory is walked recursively into the - // union of existing files + MDL deliverables, deduped by tracking number to - // one row at the latest revision. Writes/alters nothing — the revision cell - // is classifier-local and starts blank. + // "From a list" loader: "Load…" opens a multi-select directory tree (scoped + // to the served context); every ticked directory is walked recursively into + // the union of existing files + MDL deliverables, deduped by tracking number + // to one row at the latest revision. Writes/alters nothing — the revision + // cell is classifier-local and starts blank. function isRowYaml(nm) { return /\.yaml$/i.test(nm) && nm !== 'table.yaml' && nm !== 'form.yaml'; } // The newest combined " ()" string in a set, by revision token. diff --git a/tests/browse.spec.js b/tests/browse.spec.js index fd80da3..2ad91f4 100644 --- a/tests/browse.spec.js +++ b/tests/browse.spec.js @@ -271,11 +271,10 @@ test.describe('Browse menu — context & tiers', () => { expect(res.rwd).toContain('Delete…'); }); - test('toolbar Sort and Show-hidden drive state; New buttons present', async ({ page }) => { + // New folder / New file are not toolbar buttons — they live in the + // row/pane context menu (see the "keyboard menu key and kebab" test). + test('toolbar Sort and Show-hidden drive state', async ({ page }) => { await openWithTree(page); - await expect(page.locator('#newFolderBtn')).toBeVisible(); - await expect(page.locator('#newFileBtn')).toBeVisible(); - await page.locator('#sortSelect').selectOption('date:-1'); expect(await page.evaluate(() => window.app.state.sort)).toEqual({ key: 'date', dir: -1 }); diff --git a/tests/classify.spec.js b/tests/classify.spec.js index 3149322..d282090 100644 --- a/tests/classify.spec.js +++ b/tests/classify.spec.js @@ -1408,7 +1408,7 @@ test('proposeMatches finds a row whose tracking number is in the filename', asyn expect(r[1].conf).toBeCloseTo(0.8); }); -test('By existing: walkDirInto unions files + mdl deliverables, deduped to the latest revision', async ({ page }) => { +test('From a list: walkDirInto unions files + mdl deliverables, deduped to the latest revision', async ({ page }) => { await page.click('#modeClassifyBtn'); const r = await page.evaluate(async () => { const tt = window.app.modules.targetTree; @@ -1459,7 +1459,7 @@ test('By existing: walkDirInto unions files + mdl deliverables, deduped to the l expect(r.latestModifierWins).toBe('A+B1 (IFC)'); // A < A+B1 }); -test('By existing: _detectScope routes by URL/protocol', async ({ page }) => { +test('From a list: _detectScope routes by URL/protocol', async ({ page }) => { const r = await page.evaluate(() => { const tt = window.app.modules.targetTree; return { @@ -1475,7 +1475,7 @@ test('By existing: _detectScope routes by URL/protocol', async ({ page }) => { expect(r.offlineHttp).toBe('local'); }); -test('By existing: dir-picker resolves the topmost ticked directories only', async ({ page }) => { +test('From a list: dir-picker resolves the topmost ticked directories only', async ({ page }) => { const r = await page.evaluate(() => { const dp = window.app.modules.dirPicker; const childOfA = { handle: 'A/x', checked: true, children: [] }; diff --git a/tests/toast.spec.js b/tests/toast.spec.js index 752be7a..7b6b01e 100644 --- a/tests/toast.spec.js +++ b/tests/toast.spec.js @@ -22,12 +22,13 @@ test.describe('shared/toast.js', () => { expect(exposed).toBe(true); }); - test('renders a single toast with the level class and ARIA role', async ({ page }) => { + test('renders a toast with the level class and ARIA role', async ({ page }) => { const after = await page.evaluate(() => { window.zddc.toast('Saved.', 'success'); const el = document.querySelector('.zddc-toast'); return el && { - text: el.textContent, + // The message lives in its own span (the toast also holds a × button). + text: el.querySelector('.zddc-toast__msg').textContent, level: [...el.classList].find(c => c.startsWith('zddc-toast--')), role: el.getAttribute('role'), live: el.getAttribute('aria-live'), @@ -50,18 +51,24 @@ test.describe('shared/toast.js', () => { expect(probe).toEqual({ role: 'alert', live: 'assertive' }); }); - test('a second toast replaces the first (single-toast policy)', async ({ page }) => { - const count = await page.evaluate(() => { - window.zddc.toast('one', 'info'); - window.zddc.toast('two', 'info'); - return document.querySelectorAll('.zddc-toast').length; + test('toasts stack, and a "Clear all" control appears at 2+', async ({ page }) => { + const r = await page.evaluate(() => { + window.zddc.toast('one', 'error'); // sticky so it stays for the count + window.zddc.toast('two', 'error'); + return { + count: document.querySelectorAll('.zddc-toast').length, + clearAll: !!document.querySelector('.zddc-toasts__clear'), + }; }); - expect(count).toBe(1); + expect(r.count).toBe(2); // stack, not replace + expect(r.clearAll).toBe(true); // "Clear all" surfaces when 2+ are stacked }); - test('clicking dismisses immediately', async ({ page }) => { - await page.evaluate(() => window.zddc.toast('click me', 'info')); - await page.locator('.zddc-toast').click(); + test('the × button dismisses a toast; clicking the body does not', async ({ page }) => { + await page.evaluate(() => window.zddc.toast('keep me', 'error')); // sticky + await page.locator('.zddc-toast .zddc-toast__msg').click(); // selecting text ≠ dismiss + await expect(page.locator('.zddc-toast')).toHaveCount(1); + await page.locator('.zddc-toast .zddc-toast__close').click(); // × dismisses await expect(page.locator('.zddc-toast')).toHaveCount(0); }); }); diff --git a/tests/tokens.spec.js b/tests/tokens.spec.js index d6e19f5..cedfc00 100644 --- a/tests/tokens.spec.js +++ b/tests/tokens.spec.js @@ -162,19 +162,23 @@ test.describe('/.tokens self-service token UI', () => { }); test('XSS guard: description with HTML special chars is escaped on render', async ({ page }) => { + page.on('dialog', d => d.accept()); const xssDesc = ``; await page.goto(`${server.baseURL}/.tokens`); - await page.fill('#desc', xssDesc); - await page.click('button[type="submit"]'); - // Wait for the row to appear in the table. - await expect(page.locator('#tokens tbody')).toContainText(' tag should NOT have been parsed as HTML — - // window.__xss must remain undefined. - const xssFired = await page.evaluate(() => window.__xss === 1); - expect(xssFired).toBe(false); - // And the on-disk text content of the cell should contain the - // literal angle brackets, proving they were escaped. - const rowText = await page.locator('#tokens tbody tr', { hasText: 'img src' }).textContent(); - expect(rowText).toContain(' must NOT have been parsed (its onerror never fires)… + expect(await page.evaluate(() => window.__xss === 1)).toBe(false); + // …and the literal angle brackets survive in the cell text. + expect(await row.textContent()).toContain('