diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 495d06c5f..bc13dfdaa 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -526,6 +526,12 @@ jobs: CHROMIUM_REQUIRE=1 node test-a11y-1719-contrast-root-causes-e2e.js 2>&1 | tee -a e2e-output.txt CHROMIUM_REQUIRE=1 node test-nodes-favorite-rerender-e2e.js 2>&1 | tee -a e2e-output.txt CHROMIUM_REQUIRE=1 node test-packet-trace-alignment-e2e.js 2>&1 | tee -a e2e-output.txt + # Root E2Es repaired or ported to plain playwright in #189. + CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-live-pulse-ring-weight-e2e.js 2>&1 | tee -a e2e-output.txt + CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-channel-modal-e2e.js 2>&1 | tee -a e2e-output.txt + CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-node-reach-e2e.js 2>&1 | tee -a e2e-output.txt + CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-path-inspector-e2e.js 2>&1 | tee -a e2e-output.txt + CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1522-trace-url-sync-e2e.js 2>&1 | tee -a e2e-output.txt # #1616: slide-over focus-restore flake-gate. Runs the slide-over # E2E 20 consecutive times against the SAME backend instance so diff --git a/test-all.sh b/test-all.sh index 1eb9ecff4..edea7f19a 100755 --- a/test-all.sh +++ b/test-all.sh @@ -237,6 +237,18 @@ run test-map-clustering.js run test-panel-corner.js run test-customizer-v2.js +# Repaired orphans (#189) +run test-channel-colors.js +run test-channel-ux-followup.js +run test-channel-ux-round2.js +run test-drag-manager.js +run test-fluid-scaffolding.js +run test-hop-resolver-affinity.js +run test-issue-1470-card-bg-contrast.js +run test-issue-1646-compare-polish.js +run test-perf-disk-io-1120.js +run test-table-sort.js + # test-all.sh self-test (#174) run test-test-all.js diff --git a/test-channel-colors.js b/test-channel-colors.js index 06c0e249d..9fb35962b 100644 --- a/test-channel-colors.js +++ b/test-channel-colors.js @@ -146,20 +146,19 @@ test('getRowStyle returns empty string for null channel', function() { assert.strictEqual(ctx.window.ChannelColors.getRowStyle('GRP_TXT', null), ''); }); -test('getRowStyle returns border + background for assigned GRP_TXT channel', function() { +// #675 (#674): a 3px left border only, no background tint. +test('getRowStyle returns a 3px left border for assigned GRP_TXT channel', function() { const ctx = makeSandbox(); ctx.window.ChannelColors.set('#sf', '#ef4444'); const style = ctx.window.ChannelColors.getRowStyle('GRP_TXT', '#sf'); - assert.ok(style.includes('border-left:4px solid #ef4444'), 'should have left border'); - assert.ok(style.includes('background:#ef44441a'), 'should have 10% opacity background'); + assert.strictEqual(style, 'border-left:3px solid #ef4444;'); }); test('getRowStyle works with CHAN type (alias for GRP_TXT)', function() { const ctx = makeSandbox(); ctx.window.ChannelColors.set('#mesh', '#3b82f6'); const style = ctx.window.ChannelColors.getRowStyle('CHAN', '#mesh'); - assert.ok(style.includes('border-left:4px solid #3b82f6'), 'should have left border'); - assert.ok(style.includes('background:#3b82f61a'), 'should have background tint'); + assert.strictEqual(style, 'border-left:3px solid #3b82f6;'); }); test('getRowStyle returns empty when channel has no assigned color', function() { diff --git a/test-channel-modal-e2e.js b/test-channel-modal-e2e.js index 106776b73..60cc67218 100644 --- a/test-channel-modal-e2e.js +++ b/test-channel-modal-e2e.js @@ -6,7 +6,8 @@ * - sidebar [+ Add Channel] opens modal * - modal renders three labeled sections + privacy footer + QR placeholders * - close (✕) hides modal - * - sectioned sidebar renders My Channels / Network / Encrypted sections + * - sectioned sidebar renders Network / Encrypted sections, and My Channels + * only once this browser holds a key (#1111) * - PSK add flow: invalid hex → error; valid hex → modal closes * * Usage: BASE_URL=http://localhost:38201 node test-channel-modal-e2e.js @@ -42,8 +43,13 @@ function assert(c, m) { if (!c) throw new Error(m || 'assertion failed'); } }); await step('Add Channel button is visible', async () => { - const text = await page.textContent('#chAddChannelBtn'); - assert(/Add Channel/.test(text), 'button text: ' + text); + // #1227 (#1224) shortened the label to a "+ Add" chip; the accessible + // name is still "Add channel". + assert(await page.isVisible('#chAddChannelBtn'), '#chAddChannelBtn not visible'); + const text = (await page.textContent('#chAddChannelBtn')).trim(); + assert(/^\+\s*Add\b/.test(text), 'button text: ' + text); + const label = await page.getAttribute('#chAddChannelBtn', 'aria-label'); + assert(/^Add channel$/i.test(label || ''), 'aria-label: ' + label); }); await step('modal hidden on load', async () => { @@ -104,20 +110,20 @@ function assert(c, m) { if (!c) throw new Error(m || 'assertion failed'); } }, { timeout: 3000 }); }); - await step('sidebar renders three sections (My Channels / Network / Encrypted)', async () => { + await step('sidebar renders Network + Encrypted, and no My Channels without a key (#1111)', async () => { // Wait for channel list to populate from API (or render empty-state). await page.waitForFunction(() => { const el = document.getElementById('chList'); if (!el) return false; - return el.querySelector('.ch-section-mychannels') && - el.querySelector('.ch-section-network') && + return el.querySelector('.ch-section-network') && el.querySelector('.ch-section-encrypted'); }, { timeout: 8000 }); const headers = await page.$$eval('.ch-section-header', els => els.map(e => e.textContent.trim())); const joined = headers.join(' | '); - assert(/My Channels/.test(joined), 'My Channels header missing: ' + joined); - assert(/Network/.test(joined), 'Network header missing'); - assert(/Encrypted/.test(joined), 'Encrypted header missing'); + assert(/Network/.test(joined), 'Network header missing: ' + joined); + assert(/Encrypted/.test(joined), 'Encrypted header missing: ' + joined); + // 12d96a9d (#1111): the My Channels section is hidden while empty. + assert(!(await page.$('.ch-section-mychannels')), 'My Channels must not render without a stored key: ' + joined); }); await step('Encrypted section is collapsed by default', async () => { @@ -154,6 +160,13 @@ function assert(c, m) { if (!c) throw new Error(m || 'assertion failed'); } assert(/cafebabe/i.test(stored), 'expected stored key in localStorage corescope_channel_keys, got: ' + stored); }); + await step('sidebar now renders My Channels with the new channel', async () => { + await page.waitForFunction(() => { + const sec = document.querySelector('#chList .ch-section-mychannels'); + return sec && /My Channels/.test(sec.textContent) && /E2E Test Channel/.test(sec.textContent); + }, { timeout: 5000 }); + }); + await browser.close(); console.log(`\n=== Results: passed ${passed} failed ${failed} ===`); diff --git a/test-channel-ux-followup.js b/test-channel-ux-followup.js index 6e2f284a2..c049b5207 100644 --- a/test-channel-ux-followup.js +++ b/test-channel-ux-followup.js @@ -43,8 +43,12 @@ assert(!/\$\{ch\.messageCount\s*\|\|\s*0\}\s*packets/.test(chSrc), console.log('\n=== Fix 3: privacy footer wording ==='); assert(!/Clear browser data to remove stored keys/.test(chSrc), 'old "Clear browser data to remove stored keys" copy is gone'); -assert(/Use\s+✕\s+to remove individual channels/.test(chSrc), - 'new copy points at the ✕ button for individual key removal'); +// b812a98a (#1648 M3) swapped the ✕ glyph for the #ph-x sprite and reworded +// the hint to "close button"; the remove button must still be that X icon. +assert(/Use the close button to remove individual channels/.test(chSrc), + 'new copy points at the close (X) button for individual key removal'); +assert(/iconBtn\(\s*'ch-remove-btn'[^)]*'' + name + ''); + assert.ok(idx >= 0, name + ' row missing'); + return html.slice(idx, html.indexOf('', idx)); +} +const at = (sec) => new Date(Date.UTC(2026, 0, 1, 0, 0, sec)).toISOString(); + +await test('Write Sources: a source at >10× its rolling baseline gets the warning flag, a steady one does not', async () => { const sb = loadPerf(); - // Reset any previous cached snapshot - sb.ctx.window._perfWriteSourcesPrev = null; - const t0 = '2026-01-01T00:00:00Z'; - const t1 = '2026-01-01T00:00:01Z'; // 1s later - const phase1 = { sources: { tx_inserted: 100, backfill_path_json: 0 }, sampleAt: t0 }; - const phase2 = { sources: { tx_inserted: 105, backfill_path_json: 1000 }, sampleAt: t1 }; - // First render: no prev → no flags possible - stubFetch(sb, { ...basePerf, goRuntime }, goHealth, ioData, sqliteData, phase1); - await sb.pages.perf.init({ set innerHTML(v) {} }); - await new Promise(r => setTimeout(r, 50)); - // Second render: simulate tick with delta. Reuse the same fetch wiring. - stubFetch(sb, { ...basePerf, goRuntime }, goHealth, ioData, sqliteData, phase2); - await sb.pages.perf.init({ set innerHTML(v) {} }); - await new Promise(r => setTimeout(r, 50)); - const html = sb.getHtml(); - // After phase2: tx_rate = 5/s, backfill_rate = 1000/s → ratio = 200x → ⚠️ - const idx = html.indexOf('backfill_path_json'); - assert.ok(idx >= 0, 'backfill_path_json row missing'); - const row = html.slice(idx, idx + 400); - assert.ok(row.includes('⚠️'), 'expected ⚠️ on backfill row when rate ratio >10×, row=' + row); + // 40 s of baseline (backfill 1/s, tx 5/s), then 1 s where backfill jumps + // to 100/s while tx stays at 5/s. + const html = await renderWriteSources(sb, [ + { sources: { tx_inserted: 0, backfill_path_json: 0 }, sampleAt: at(0) }, + { sources: { tx_inserted: 200, backfill_path_json: 40 }, sampleAt: at(40) }, + { sources: { tx_inserted: 205, backfill_path_json: 140 }, sampleAt: at(41) }, + ]); + const backfill = sourceRow(html, 'backfill_path_json'); + assert.ok(backfill.includes('100.001.00'), 'backfill rate 100/s vs baseline 1/s rendered, row=' + backfill); + assert.ok(backfill.includes(WARN), 'expected the warning icon on the backfill row, row=' + backfill); + const tx = sourceRow(html, 'tx_inserted'); + assert.ok(tx.includes('5.005.00'), 'tx rate and baseline 5/s rendered, row=' + tx); + assert.ok(!tx.includes(WARN), 'expected NO warning icon on the steady tx_inserted row, row=' + tx); }); -await test('Backfill anomaly: tx_inserted <100 baseline guard SUPPRESSES ⚠️', async () => { - // Same shape but tx_inserted stays well below the 100 floor; even a huge - // backfill rate ratio must NOT fire while we lack a meaningful baseline. +await test('Write Sources: less than 30 s of history SUPPRESSES the warning flag', async () => { const sb = loadPerf(); - sb.ctx.window._perfWriteSourcesPrev = null; - const t0 = '2026-01-01T00:00:00Z'; - const t1 = '2026-01-01T00:00:01Z'; - const phase1 = { sources: { tx_inserted: 5, backfill_path_json: 0 }, sampleAt: t0 }; - const phase2 = { sources: { tx_inserted: 6, backfill_path_json: 1000 }, sampleAt: t1 }; - stubFetch(sb, { ...basePerf, goRuntime }, goHealth, ioData, sqliteData, phase1); - await sb.pages.perf.init({ set innerHTML(v) {} }); - await new Promise(r => setTimeout(r, 50)); - stubFetch(sb, { ...basePerf, goRuntime }, goHealth, ioData, sqliteData, phase2); - await sb.pages.perf.init({ set innerHTML(v) {} }); - await new Promise(r => setTimeout(r, 50)); - const html = sb.getHtml(); - const idx = html.indexOf('backfill_path_json'); - const row = html.slice(idx, idx + 400); - assert.ok(!row.includes('⚠️'), 'expected NO ⚠️ on backfill row when tx_inserted<100, row=' + row); + // Same spike, but the baseline only spans 10 s (< minHistorySec 30). + const html = await renderWriteSources(sb, [ + { sources: { tx_inserted: 0, backfill_path_json: 0 }, sampleAt: at(0) }, + { sources: { tx_inserted: 50, backfill_path_json: 10 }, sampleAt: at(10) }, + { sources: { tx_inserted: 55, backfill_path_json: 110 }, sampleAt: at(11) }, + ]); + const backfill = sourceRow(html, 'backfill_path_json'); + assert.ok(backfill.includes('100.00—'), 'rate shown, baseline not yet established, row=' + backfill); + assert.ok(!backfill.includes(WARN), 'expected NO warning icon with < 30 s of history, row=' + backfill); }); console.log(`\n${passed} passed, ${failed} failed\n`); diff --git a/test-table-sort.js b/test-table-sort.js index a6255b911..72da63a0b 100644 --- a/test-table-sort.js +++ b/test-table-sort.js @@ -1,7 +1,7 @@ /* test-table-sort.js — Unit tests for TableSort utility */ 'use strict'; -const { JSDOM } = require('jsdom'); +const vm = require('vm'); const fs = require('fs'); const path = require('path'); const assert = require('assert'); @@ -21,52 +21,132 @@ function test(name, fn) { } } -function createDOM(html) { - const dom = new JSDOM(`${html}`, { - url: 'http://localhost', - runScripts: 'dangerously' - }); - // Load TableSort into this DOM - const script = fs.readFileSync(path.join(__dirname, 'public', 'table-sort.js'), 'utf8'); - const el = dom.window.document.createElement('script'); - el.textContent = script; - dom.window.document.head.appendChild(el); - return dom; +// ── Minimal DOM (#189: jsdom is not a dependency) ───────────────────────── +// Just enough of the element API that table-sort.js touches: attributes, +// classList, style, child lists with real moves on appendChild, remove(), +// listeners + click(), cells, textContent and a small selector engine +// (tag, .class, tag[attr], tag[attr="v"]; descendants, document order). +class FakeEl { + constructor(tag) { + this.tagName = tag.toUpperCase(); + this.children = []; + this.parentNode = null; + this.attrs = {}; + this.style = {}; + this.listeners = {}; + this.text = ''; + const el = this; + this.classList = { + add(c) { const s = el._classes(); if (!s.includes(c)) el.className = s.concat(c).join(' '); }, + remove(c) { el.className = el._classes().filter((x) => x !== c).join(' '); }, + contains(c) { return el._classes().includes(c); }, + }; + } + _classes() { return (this.attrs.class || '').split(/\s+/).filter(Boolean); } + get className() { return this.attrs.class || ''; } + set className(v) { this.attrs.class = String(v); } + getAttribute(k) { return Object.prototype.hasOwnProperty.call(this.attrs, k) ? this.attrs[k] : null; } + setAttribute(k, v) { this.attrs[k] = String(v); } + removeAttribute(k) { delete this.attrs[k]; } + appendChild(child) { + if (child.parentNode) child.remove(); + child.parentNode = this; + this.children.push(child); + return child; + } + remove() { + if (!this.parentNode) return; + const sibs = this.parentNode.children; + sibs.splice(sibs.indexOf(this), 1); + this.parentNode = null; + } + // innerHTML is only ever assigned (the sort arrow); keep the markup as text. + set innerHTML(html) { this.children = []; this.text = String(html); } + get innerHTML() { return this.text + this.children.map((c) => c.innerHTML).join(''); } + get textContent() { return this.text + this.children.map((c) => c.textContent).join(''); } + get cells() { return this.children.filter((c) => c.tagName === 'TD' || c.tagName === 'TH'); } + addEventListener(type, fn) { (this.listeners[type] = this.listeners[type] || []).push(fn); } + removeEventListener(type, fn) { this.listeners[type] = (this.listeners[type] || []).filter((f) => f !== fn); } + click() { (this.listeners.click || []).slice().forEach((fn) => fn({ type: 'click', preventDefault() {} })); } + _descendants() { return this.children.flatMap((c) => [c, ...c._descendants()]); } + querySelectorAll(sel) { return this._descendants().filter((el) => matches(el, sel)); } + querySelector(sel) { return this.querySelectorAll(sel)[0] || null; } +} + +function matches(el, sel) { + const m = /^([a-z]*)(?:\.([\w-]+))?(?:\[([\w-]+)(?:="([^"]*)")?\])?$/.exec(sel); + if (!m) throw new Error('FakeEl: unsupported selector ' + sel); + const [, tag, cls, attr, val] = m; + if (tag && el.tagName !== tag.toUpperCase()) return false; + if (cls && !el.classList.contains(cls)) return false; + if (attr && el.getAttribute(attr) == null) return false; + if (val !== undefined && el.getAttribute(attr) !== val) return false; + return true; +} + +function createDOM(table) { + const body = new FakeEl('body'); + if (table) body.appendChild(table); + const store = {}; + const document = { + body, + createElement: (tag) => new FakeEl(tag), + getElementById: (id) => body._descendants().find((el) => el.getAttribute('id') === id) || null, + querySelector: (sel) => body.querySelector(sel), + querySelectorAll: (sel) => body.querySelectorAll(sel), + }; + const window = { document }; + const ctx = { + window, document, + localStorage: { + getItem: (k) => (k in store ? store[k] : null), + setItem: (k, v) => { store[k] = String(v); }, + }, + }; + vm.createContext(ctx); + vm.runInContext(fs.readFileSync(path.join(__dirname, 'public', 'table-sort.js'), 'utf8'), ctx); + return { window }; } function makeTable(headers, rows) { // headers: [{key, type?, label}], rows: [[value, ...]] - let html = ''; + const el = (tag, attrs, text) => { + const e = new FakeEl(tag); + for (const k in attrs || {}) e.setAttribute(k, attrs[k]); + if (text != null) e.text = String(text); + return e; + }; + const table = el('table', { id: 't' }); + const thead = table.appendChild(el('thead')); + const headRow = thead.appendChild(el('tr')); for (const h of headers) { - html += ``; + headRow.appendChild(el('th', Object.assign({ 'data-sort-key': h.key }, h.type ? { 'data-type': h.type } : {}), h.label || h.key)); } - html += ''; + const tbody = table.appendChild(el('tbody')); for (const row of rows) { - html += ''; - for (let i = 0; i < row.length; i++) { - const val = row[i]; - if (typeof val === 'object' && val !== null) { - html += ``; - } else { - html += ``; - } + const tr = tbody.appendChild(el('tr')); + for (const val of row) { + if (typeof val === 'object' && val !== null) tr.appendChild(el('td', { 'data-value': val.dataValue }, val.text || '')); + else tr.appendChild(el('td', { 'data-value': val }, val)); } - html += ''; } - html += '
${h.label || h.key}
${val.text || ''}${val}
'; - return html; + return table; } function getColumnValues(dom, colIndex) { - const rows = dom.window.document.querySelectorAll('tbody tr'); - return Array.from(rows).map(r => r.cells[colIndex].getAttribute('data-value')); + const rows = dom.window.document.querySelector('tbody').children; + return rows.map(r => r.cells[colIndex].getAttribute('data-value')); } +// The arrow table-sort.js renders: since #1648 M2 a Phosphor caret sprite +// (was ▲ / ▼). +const caret = (dir) => '#ph-caret-' + dir + '"'; + console.log('\nTableSort — comparators'); test('text comparator: basic alphabetical', () => { const cmp = (() => { - const dom = createDOM('
'); + const dom = createDOM(null); return dom.window.TableSort.comparators.text; })(); assert.ok(cmp('apple', 'banana') < 0); @@ -75,14 +155,14 @@ test('text comparator: basic alphabetical', () => { }); test('text comparator: null/undefined handling', () => { - const dom = createDOM('
'); + const dom = createDOM(null); const cmp = dom.window.TableSort.comparators.text; assert.strictEqual(cmp(null, null), 0); assert.strictEqual(cmp(undefined, undefined), 0); }); test('numeric comparator: basic numbers', () => { - const dom = createDOM('
'); + const dom = createDOM(null); const cmp = dom.window.TableSort.comparators.numeric; assert.ok(cmp('1', '2') < 0); assert.ok(cmp('10', '2') > 0); @@ -90,7 +170,7 @@ test('numeric comparator: basic numbers', () => { }); test('numeric comparator: NaN sorts last', () => { - const dom = createDOM('
'); + const dom = createDOM(null); const cmp = dom.window.TableSort.comparators.numeric; assert.ok(cmp('abc', '5') > 0); // NaN > number (sorts last) assert.ok(cmp('5', 'abc') < 0); @@ -98,14 +178,14 @@ test('numeric comparator: NaN sorts last', () => { }); test('numeric comparator: negative numbers', () => { - const dom = createDOM('
'); + const dom = createDOM(null); const cmp = dom.window.TableSort.comparators.numeric; assert.ok(cmp('-10', '-5') < 0); assert.ok(cmp('-5', '-10') > 0); }); test('date comparator: ISO dates', () => { - const dom = createDOM('
'); + const dom = createDOM(null); const cmp = dom.window.TableSort.comparators.date; assert.ok(cmp('2024-01-01T00:00:00Z', '2024-06-01T00:00:00Z') < 0); assert.ok(cmp('2024-06-01T00:00:00Z', '2024-01-01T00:00:00Z') > 0); @@ -113,14 +193,14 @@ test('date comparator: ISO dates', () => { }); test('date comparator: invalid dates sort last', () => { - const dom = createDOM('
'); + const dom = createDOM(null); const cmp = dom.window.TableSort.comparators.date; assert.ok(cmp('invalid', '2024-01-01') > 0); assert.ok(cmp('2024-01-01', 'invalid') < 0); }); test('dBm comparator: strips suffix', () => { - const dom = createDOM('
'); + const dom = createDOM(null); const cmp = dom.window.TableSort.comparators.dbm; assert.ok(cmp('-120 dBm', '-80 dBm') < 0); assert.ok(cmp('-80 dBm', '-120 dBm') > 0); @@ -128,7 +208,7 @@ test('dBm comparator: strips suffix', () => { }); test('dBm comparator: works without suffix', () => { - const dom = createDOM('
'); + const dom = createDOM(null); const cmp = dom.window.TableSort.comparators.dbm; assert.ok(cmp('-120', '-80') < 0); }); @@ -136,11 +216,11 @@ test('dBm comparator: works without suffix', () => { console.log('\nTableSort — DOM sorting'); test('sort ascending by text column', () => { - const html = makeTable( + const tableEl = makeTable( [{key: 'name'}], [['Charlie'], ['Alice'], ['Bob']] ); - const dom = createDOM(html); + const dom = createDOM(tableEl); const table = dom.window.document.getElementById('t'); const inst = dom.window.TableSort.init(table, { defaultColumn: 'name', defaultDirection: 'asc' }); const vals = getColumnValues(dom, 0); @@ -148,11 +228,11 @@ test('sort ascending by text column', () => { }); test('sort descending by numeric column', () => { - const html = makeTable( + const tableEl = makeTable( [{key: 'val', type: 'numeric'}], [['3'], ['1'], ['2']] ); - const dom = createDOM(html); + const dom = createDOM(tableEl); const table = dom.window.document.getElementById('t'); dom.window.TableSort.init(table, { defaultColumn: 'val', defaultDirection: 'desc' }); const vals = getColumnValues(dom, 0); @@ -160,11 +240,11 @@ test('sort descending by numeric column', () => { }); test('click toggles direction', () => { - const html = makeTable( + const tableEl = makeTable( [{key: 'name'}], [['B'], ['A'], ['C']] ); - const dom = createDOM(html); + const dom = createDOM(tableEl); const table = dom.window.document.getElementById('t'); const inst = dom.window.TableSort.init(table, { defaultColumn: 'name', defaultDirection: 'asc' }); @@ -184,11 +264,11 @@ test('click toggles direction', () => { console.log('\nTableSort — aria-sort attributes'); test('aria-sort set correctly on active column', () => { - const html = makeTable( + const tableEl = makeTable( [{key: 'a'}, {key: 'b'}], [['1', 'x'], ['2', 'y']] ); - const dom = createDOM(html); + const dom = createDOM(tableEl); const table = dom.window.document.getElementById('t'); dom.window.TableSort.init(table, { defaultColumn: 'a', defaultDirection: 'asc' }); @@ -199,11 +279,11 @@ test('aria-sort set correctly on active column', () => { }); test('aria-sort updates on direction change', () => { - const html = makeTable( + const tableEl = makeTable( [{key: 'a'}], [['1'], ['2']] ); - const dom = createDOM(html); + const dom = createDOM(tableEl); const table = dom.window.document.getElementById('t'); dom.window.TableSort.init(table, { defaultColumn: 'a', defaultDirection: 'asc' }); @@ -215,11 +295,11 @@ test('aria-sort updates on direction change', () => { }); test('aria-sort updates when switching columns', () => { - const html = makeTable( + const tableEl = makeTable( [{key: 'a'}, {key: 'b'}], [['1', 'x'], ['2', 'y']] ); - const dom = createDOM(html); + const dom = createDOM(tableEl); const table = dom.window.document.getElementById('t'); dom.window.TableSort.init(table, { defaultColumn: 'a', defaultDirection: 'asc' }); @@ -234,42 +314,42 @@ test('aria-sort updates when switching columns', () => { console.log('\nTableSort — visual indicator'); test('sort arrow shows on active column', () => { - const html = makeTable( + const tableEl = makeTable( [{key: 'a'}], [['1'], ['2']] ); - const dom = createDOM(html); + const dom = createDOM(tableEl); const table = dom.window.document.getElementById('t'); dom.window.TableSort.init(table, { defaultColumn: 'a', defaultDirection: 'asc' }); const arrow = dom.window.document.querySelector('.sort-arrow'); assert.ok(arrow, 'sort arrow should exist'); - assert.ok(arrow.textContent.includes('▲'), 'ascending should show ▲'); + assert.ok(arrow.innerHTML.includes(caret('up')), 'ascending should show the up caret'); }); test('sort arrow changes on direction toggle', () => { - const html = makeTable( + const tableEl = makeTable( [{key: 'a'}], [['1'], ['2']] ); - const dom = createDOM(html); + const dom = createDOM(tableEl); const table = dom.window.document.getElementById('t'); dom.window.TableSort.init(table, { defaultColumn: 'a', defaultDirection: 'asc' }); const th = dom.window.document.querySelector('th[data-sort-key="a"]'); th.click(); // desc const arrow = dom.window.document.querySelector('.sort-arrow'); - assert.ok(arrow.textContent.includes('▼'), 'descending should show ▼'); + assert.ok(arrow.innerHTML.includes(caret('down')), 'descending should show the down caret'); }); console.log('\nTableSort — onSort callback'); test('onSort fires with column and direction', () => { - const html = makeTable( + const tableEl = makeTable( [{key: 'a'}, {key: 'b'}], [['1', 'x'], ['2', 'y']] ); - const dom = createDOM(html); + const dom = createDOM(tableEl); const table = dom.window.document.getElementById('t'); let called = null; dom.window.TableSort.init(table, { @@ -287,11 +367,11 @@ test('onSort fires with column and direction', () => { console.log('\nTableSort — domReorder: false'); test('domReorder: false skips DOM sorting', () => { - const html = makeTable( + const tableEl = makeTable( [{key: 'name'}], [['C'], ['A'], ['B']] ); - const dom = createDOM(html); + const dom = createDOM(tableEl); const table = dom.window.document.getElementById('t'); dom.window.TableSort.init(table, { defaultColumn: 'name', defaultDirection: 'asc', domReorder: false }); @@ -303,11 +383,11 @@ test('domReorder: false skips DOM sorting', () => { console.log('\nTableSort — destroy'); test('destroy removes event handlers and cleans up', () => { - const html = makeTable( + const tableEl = makeTable( [{key: 'a'}], [['2'], ['1']] ); - const dom = createDOM(html); + const dom = createDOM(tableEl); const table = dom.window.document.getElementById('t'); const inst = dom.window.TableSort.init(table, { defaultColumn: 'a', defaultDirection: 'asc' }); @@ -322,11 +402,11 @@ test('destroy removes event handlers and cleans up', () => { console.log('\nTableSort — custom comparators'); test('custom comparator overrides built-in', () => { - const html = makeTable( + const tableEl = makeTable( [{key: 'val', type: 'numeric'}], [['3'], ['1'], ['2']] ); - const dom = createDOM(html); + const dom = createDOM(tableEl); const table = dom.window.document.getElementById('t'); // Custom: reverse numeric dom.window.TableSort.init(table, { @@ -340,11 +420,11 @@ test('custom comparator overrides built-in', () => { console.log('\nTableSort — date sort with data-type="date"'); test('date column sorts correctly', () => { - const html = makeTable( + const tableEl = makeTable( [{key: 'ts', type: 'date'}], [['2024-06-15T10:00:00Z'], ['2024-01-01T00:00:00Z'], ['2024-12-25T23:59:59Z']] ); - const dom = createDOM(html); + const dom = createDOM(tableEl); const table = dom.window.document.getElementById('t'); dom.window.TableSort.init(table, { defaultColumn: 'ts', defaultDirection: 'asc' }); const vals = getColumnValues(dom, 0); diff --git a/test-test-all.js b/test-test-all.js index f613e4961..08af253e7 100644 --- a/test-test-all.js +++ b/test-test-all.js @@ -130,25 +130,10 @@ test('test-all.sh registers itself through this test', () => { // test past the check below: register it instead. const KNOWN_UNREGISTERED = { // red unit tests (stale after intended UI changes) - 'test-channel-colors.js': 'red: expects 4px border + tint, #675 changed it to 3px', - 'test-channel-ux-followup.js': 'red: copy text changed by the Phosphor migration', - 'test-channel-ux-round2.js': 'red: expects the 📤 glyph, now #ph-share-network', - 'test-drag-manager.js': 'red: removeAttribute mock does not update dataset (#1567)', - 'test-fluid-scaffolding.js': 'red: reads only the first :root block', - 'test-hop-resolver-affinity.js': 'red: fixture geometry wrong since #874', - 'test-issue-1470-card-bg-contrast.js': 'red: indexOf matches a style.css comment', - 'test-issue-1646-compare-polish.js': 'red: font-size parser reads a comment', - 'test-packets.js': 'red: 13 emoji assertions after the Phosphor migration', - 'test-perf-disk-io-1120.js': 'red: ⚠️ is #ph-warning; anomaly detector reworked (#1593)', + 'test-packets.js': 'red on 1 assertion: collapsed group caret is #ph-caret-up, a real bug (#189); register once packets.js is fixed', // need something CI's unit job does not have - 'test-marker-outline-weight.js': 'needs @playwright/test (not a dependency) and a server', - 'test-table-sort.js': 'needs jsdom (not a dependency)', - 'test-touch-targets.js': 'red in Chromium: expects 48px targets, CSS has 44px', + 'test-touch-targets.js': 'red in Chromium: 48px (upstream #2078) vs 44px (#85) is pending a decision (#189); harness also stale', // E2E - 'test-channel-modal-e2e.js': 'red: Add button text and sidebar sections changed', - 'test-issue-1522-trace-url-sync-e2e.js': 'needs @playwright/test (not a dependency)', - 'test-node-reach-e2e.js': 'red: #nqMap .leaflet-container never visible', - 'test-path-inspector-e2e.js': 'needs @playwright/test (not a dependency)', 'test-rx-coverage-mobile-nav-e2e.js': 'skips while clientRxCoverage is off (the default)', };