chore: clear tech debt — green the suite + delete dead code
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) <noreply@anthropic.com>
This commit is contained in:
parent
7c0b66590c
commit
921713d0a4
7 changed files with 46 additions and 56 deletions
|
|
@ -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__tn { font-family: var(--mono, monospace); }
|
||||||
.scratch-match__conf { color: var(--text-muted); font-size: 0.72rem; width: 3rem; text-align: right; }
|
.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) ────────────────────── */
|
/* ── Shared selectable + autofilter table (seltable) ────────────────────── */
|
||||||
.seltable { display: flex; flex-direction: column; min-height: 0; height: 100%; }
|
.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; }
|
.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; }
|
||||||
|
|
|
||||||
|
|
@ -378,15 +378,6 @@
|
||||||
return out;
|
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.
|
// Per-file classification state for the left-tree markers.
|
||||||
// 'excluded' | 'done' | 'tracking' | 'transmittal' | 'partial' | 'none'
|
// 'excluded' | 'done' | 'tracking' | 'transmittal' | 'partial' | 'none'
|
||||||
function fileState(file) {
|
function fileState(file) {
|
||||||
|
|
@ -858,7 +849,7 @@
|
||||||
getNode: getNode, getTrackingTree: function () { return state.trackingTree; },
|
getNode: getNode, getTrackingTree: function () { return state.trackingTree; },
|
||||||
getTransmittalTree: function () { return state.transmittalTree; },
|
getTransmittalTree: function () { return state.transmittalTree; },
|
||||||
// derive + reverse
|
// derive + reverse
|
||||||
deriveTarget: deriveTarget, filesInNode: filesInNode,
|
deriveTarget: deriveTarget,
|
||||||
fileState: fileState, stats: stats,
|
fileState: fileState, stats: stats,
|
||||||
// persistence
|
// persistence
|
||||||
serialize: serialize, load: load, reset: reset,
|
serialize: serialize, load: load, reset: reset,
|
||||||
|
|
|
||||||
|
|
@ -601,11 +601,11 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the catalog: "Load…" opens a multi-select directory tree (scoped to
|
// "From a list" loader: "Load…" opens a multi-select directory tree (scoped
|
||||||
// the served context); every ticked directory is walked recursively into the
|
// to the served context); every ticked directory is walked recursively into
|
||||||
// union of existing files + MDL deliverables, deduped by tracking number to
|
// the union of existing files + MDL deliverables, deduped by tracking number
|
||||||
// one row at the latest revision. Writes/alters nothing — the revision cell
|
// to one row at the latest revision. Writes/alters nothing — the revision
|
||||||
// is classifier-local and starts blank.
|
// cell is classifier-local and starts blank.
|
||||||
function isRowYaml(nm) { return /\.yaml$/i.test(nm) && nm !== 'table.yaml' && nm !== 'form.yaml'; }
|
function isRowYaml(nm) { return /\.yaml$/i.test(nm) && nm !== 'table.yaml' && nm !== 'form.yaml'; }
|
||||||
|
|
||||||
// The newest combined "<rev> (<status>)" string in a set, by revision token.
|
// The newest combined "<rev> (<status>)" string in a set, by revision token.
|
||||||
|
|
|
||||||
|
|
@ -271,11 +271,10 @@ test.describe('Browse menu — context & tiers', () => {
|
||||||
expect(res.rwd).toContain('Delete…');
|
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 openWithTree(page);
|
||||||
await expect(page.locator('#newFolderBtn')).toBeVisible();
|
|
||||||
await expect(page.locator('#newFileBtn')).toBeVisible();
|
|
||||||
|
|
||||||
await page.locator('#sortSelect').selectOption('date:-1');
|
await page.locator('#sortSelect').selectOption('date:-1');
|
||||||
expect(await page.evaluate(() => window.app.state.sort)).toEqual({ key: 'date', dir: -1 });
|
expect(await page.evaluate(() => window.app.state.sort)).toEqual({ key: 'date', dir: -1 });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1408,7 +1408,7 @@ test('proposeMatches finds a row whose tracking number is in the filename', asyn
|
||||||
expect(r[1].conf).toBeCloseTo(0.8);
|
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');
|
await page.click('#modeClassifyBtn');
|
||||||
const r = await page.evaluate(async () => {
|
const r = await page.evaluate(async () => {
|
||||||
const tt = window.app.modules.targetTree;
|
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
|
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 r = await page.evaluate(() => {
|
||||||
const tt = window.app.modules.targetTree;
|
const tt = window.app.modules.targetTree;
|
||||||
return {
|
return {
|
||||||
|
|
@ -1475,7 +1475,7 @@ test('By existing: _detectScope routes by URL/protocol', async ({ page }) => {
|
||||||
expect(r.offlineHttp).toBe('local');
|
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 r = await page.evaluate(() => {
|
||||||
const dp = window.app.modules.dirPicker;
|
const dp = window.app.modules.dirPicker;
|
||||||
const childOfA = { handle: 'A/x', checked: true, children: [] };
|
const childOfA = { handle: 'A/x', checked: true, children: [] };
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,13 @@ test.describe('shared/toast.js', () => {
|
||||||
expect(exposed).toBe(true);
|
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(() => {
|
const after = await page.evaluate(() => {
|
||||||
window.zddc.toast('Saved.', 'success');
|
window.zddc.toast('Saved.', 'success');
|
||||||
const el = document.querySelector('.zddc-toast');
|
const el = document.querySelector('.zddc-toast');
|
||||||
return el && {
|
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--')),
|
level: [...el.classList].find(c => c.startsWith('zddc-toast--')),
|
||||||
role: el.getAttribute('role'),
|
role: el.getAttribute('role'),
|
||||||
live: el.getAttribute('aria-live'),
|
live: el.getAttribute('aria-live'),
|
||||||
|
|
@ -50,18 +51,24 @@ test.describe('shared/toast.js', () => {
|
||||||
expect(probe).toEqual({ role: 'alert', live: 'assertive' });
|
expect(probe).toEqual({ role: 'alert', live: 'assertive' });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('a second toast replaces the first (single-toast policy)', async ({ page }) => {
|
test('toasts stack, and a "Clear all" control appears at 2+', async ({ page }) => {
|
||||||
const count = await page.evaluate(() => {
|
const r = await page.evaluate(() => {
|
||||||
window.zddc.toast('one', 'info');
|
window.zddc.toast('one', 'error'); // sticky so it stays for the count
|
||||||
window.zddc.toast('two', 'info');
|
window.zddc.toast('two', 'error');
|
||||||
return document.querySelectorAll('.zddc-toast').length;
|
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 }) => {
|
test('the × button dismisses a toast; clicking the body does not', async ({ page }) => {
|
||||||
await page.evaluate(() => window.zddc.toast('click me', 'info'));
|
await page.evaluate(() => window.zddc.toast('keep me', 'error')); // sticky
|
||||||
await page.locator('.zddc-toast').click();
|
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);
|
await expect(page.locator('.zddc-toast')).toHaveCount(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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 }) => {
|
test('XSS guard: description with HTML special chars is escaped on render', async ({ page }) => {
|
||||||
|
page.on('dialog', d => d.accept());
|
||||||
const xssDesc = `<img src=x onerror="window.__xss=1">`;
|
const xssDesc = `<img src=x onerror="window.__xss=1">`;
|
||||||
await page.goto(`${server.baseURL}/.tokens`);
|
await page.goto(`${server.baseURL}/.tokens`);
|
||||||
await page.fill('#desc', xssDesc);
|
// Create via the apiActions modal (the inline #desc form is long gone).
|
||||||
await page.click('button[type="submit"]');
|
await page.locator('#api-create-btn').click();
|
||||||
// Wait for the row to appear in the table.
|
await expect(page.locator('.api-modal')).toBeVisible();
|
||||||
await expect(page.locator('#tokens tbody')).toContainText('<img');
|
await page.locator('.api-modal input').first().fill(xssDesc);
|
||||||
// The literal <img> tag should NOT have been parsed as HTML —
|
await page.locator('.api-modal button[type="submit"]').click();
|
||||||
// window.__xss must remain undefined.
|
await expect(page.locator('.api-modal__secret')).toBeVisible();
|
||||||
const xssFired = await page.evaluate(() => window.__xss === 1);
|
await page.locator('.api-modal button:has-text("Done")').click();
|
||||||
expect(xssFired).toBe(false);
|
await page.waitForLoadState('networkidle');
|
||||||
// And the on-disk text content of the cell should contain the
|
// The description renders as a row — as TEXT, not parsed HTML.
|
||||||
// literal angle brackets, proving they were escaped.
|
const row = page.locator('#table-root tbody tr', { hasText: 'img src' });
|
||||||
const rowText = await page.locator('#tokens tbody tr', { hasText: 'img src' }).textContent();
|
await expect(row).toBeVisible();
|
||||||
expect(rowText).toContain('<img');
|
// The <img> 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('<img');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue