mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-09-26 01:23:54 +00:00
Closes #2037. Step 1 (#2045) wired in the nine suites that already passed. This is steps 2 and 3: the four that ran and failed, and the five that could not run at all. After this, the count of suites in `tests/e2e` invoked by nothing goes from 18 to 0. ## Step 2 — the four that ran and failed Triaged against a fixture server in CI with full output kept, not the four-line tail the first probe saved. | suite | verdict | |---|---| | `test-packets-scope-column.js` | passes on master today. It failed on 2026-09-18, so something between the two fixed it. Wired in unchanged rather than investigated. | | `test-node-reach-e2e.js` | test wrong. It waited for the reach map whenever any *link* had GPS; `public/node-reach.js` builds the map only when the *node* has coordinates. Guaranteed 10s timeout on a node with positioned neighbours and no position of its own. | | `test-channel-modal-e2e.js` | both failures test-side. The Add button's visible label was shortened to "+ Add" with the accessible name moved to `aria-label` (`public/channels.js:746`); and `.ch-section-mychannels` is conditional on the visitor having added a channel, which has not happened at that point in the suite. | | `test-touch-targets.js` | three test-side, two a product finding. | The three test-side touch-target failures were the harness measuring controls that are not shown: `.compare-btn` (the CTA was removed in #1646, and `style.css` says so), `.ch-back-btn` (`display:none` outside the mobile channels layout), and `.filter-toggle-btn` (`display:none` on mobile since #1461; the control shown is the navbar mirror, which `mobile-page-actions.js:70` builds as a `.nav-btn`, so it was already measured). All three are dropped from the table with the reason recorded in the file. The remaining two are **not** a test problem: `.nav-btn` and `.ch-icon-btn` are each declared twice in `public/style.css`, 48px in the touch-target block and 44px in their own component rule, and the later one wins. Rather than lower the blanket or hide the failures, the suite now has `DEFAULT_MIN = 48` plus a `MIN_OVERRIDES` table holding those two at their effective 44, so a third selector dropping to 44 still fails the build. The contradiction is **#2052**, with both ways out costed; the override entries should go when it is settled. ## Step 3 — the five that could not run None is deleted. I checked each selector and seam against the product before deciding, and every one still targets a surface that exists and that nothing else covers. Four were written against `@playwright/test`, a runner the project neither installs nor uses anywhere else. Adopting a second runner for twelve tests costs more than porting them, and the precedent is already set: `test-path-inspector-coverage-e2e.js` exists, as its own header says, because `test-path-inspector-e2e.js` could not run. So they are ported to the plain-node Chromium pattern the other 109 suites use. - **`test-issue-1522-trace-url-sync-e2e.js`** — the trace hash in the URL, both directions. `test-e2e-playwright.js` covers that the page loads and searches; it never looks at the URL, which is the whole of #1522. - **`test-marker-outline-weight.js`** — the canvas pulse ring never thins below 2px. There is no CSS rule to read and axe cannot see inside a canvas, so sampling the seam is the only way. Added a guard that the ring was actually visible, so the weight check cannot pass vacuously on a pulse that never rendered. - **`test-pr-1490-live-map-gpu-animations-e2e.js`** — the queue drains, the engine sleeps again, the fading trails stay under the cap of 5, and the canvas sits on `animationsPane` rather than under the markers. - **`test-path-inspector-e2e.js`** — reduced to what nothing else covers: the map side pane, the `/#/traces/<hash>` redirect, the tools landing. Its standalone-page test duplicated the wired coverage suite and is dropped. Its "switching candidate clears prior polyline" case ended after the click with a comment and no assertion, which is the same green-but-empty problem this issue is about; it now compares path counts, and skips loudly when the fixture yields too few candidates. The fifth, **`test-table-sort.js`**, needed `jsdom`, which was declared nowhere. It is a unit test of `public/table-sort.js` filed under `tests/e2e`, so: `jsdom` is a devDependency (lockfile updated, `npm ci` stays consistent), the file moved to `tests/unit/`, and the `domIntegration` group in `scripts/non-unit-tests.json` is gone with its only member. It runs 22 tests. 20 passed immediately; 2 had rotted, because #1648 M2 replaced the up/down glyphs with Phosphor sprites and the direction moved out of `textContent` into the `<use href>`. Those two now read the sprite ref and the `aria-sort` value, so they also guard the accessible announcement. ## Verification `tests/unit/test-table-sort.js` 22/22 and `test-test-inventory.js` pass locally; the E2E suites need a fixture server, which I cannot build here (no cgo toolchain since #1992), so CI is their first run as committed. The triage above was measured in CI, not assumed. ## Not done The per-assertion skips named in the second comment on #2037 are untouched: the two flaky packet-detail cases, the fixture-data ones, and the two `clientRxCoverage` suites that skip wholesale while reporting success. Those need a fixture deployment with coverage enabled, which is its own change. I have not opened it. Option 2 from the issue, making `test-test-inventory.js` require a `deploy.yml` line for every `tests/e2e` file, is also not here. It is the right guard and it is now enforceable, since the list is finally at zero, but it belongs in its own change where a red build means what it says. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
374 lines
13 KiB
JavaScript
374 lines
13 KiB
JavaScript
/* test-table-sort.js — Unit tests for TableSort utility */
|
|
'use strict';
|
|
const REPO_ROOT = require('path').resolve(__dirname, '..', '..');
|
|
|
|
const { JSDOM } = require('jsdom');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const assert = require('assert');
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
function test(name, fn) {
|
|
try {
|
|
fn();
|
|
passed++;
|
|
console.log(` ✓ ${name}`);
|
|
} catch (e) {
|
|
failed++;
|
|
console.log(` ✗ ${name}`);
|
|
console.log(` ${e.message}`);
|
|
}
|
|
}
|
|
|
|
function createDOM(html) {
|
|
const dom = new JSDOM(`<!DOCTYPE html><html><body>${html}</body></html>`, {
|
|
url: 'http://localhost',
|
|
runScripts: 'dangerously'
|
|
});
|
|
// Load TableSort into this DOM
|
|
const script = fs.readFileSync(path.join(REPO_ROOT, 'public', 'table-sort.js'), 'utf8');
|
|
const el = dom.window.document.createElement('script');
|
|
el.textContent = script;
|
|
dom.window.document.head.appendChild(el);
|
|
return dom;
|
|
}
|
|
|
|
function makeTable(headers, rows) {
|
|
// headers: [{key, type?, label}], rows: [[value, ...]]
|
|
let html = '<table id="t"><thead><tr>';
|
|
for (const h of headers) {
|
|
html += `<th data-sort-key="${h.key}"${h.type ? ` data-type="${h.type}"` : ''}>${h.label || h.key}</th>`;
|
|
}
|
|
html += '</tr></thead><tbody>';
|
|
for (const row of rows) {
|
|
html += '<tr>';
|
|
for (let i = 0; i < row.length; i++) {
|
|
const val = row[i];
|
|
if (typeof val === 'object' && val !== null) {
|
|
html += `<td data-value="${val.dataValue}">${val.text || ''}</td>`;
|
|
} else {
|
|
html += `<td data-value="${val}">${val}</td>`;
|
|
}
|
|
}
|
|
html += '</tr>';
|
|
}
|
|
html += '</tbody></table>';
|
|
return html;
|
|
}
|
|
|
|
function getColumnValues(dom, colIndex) {
|
|
const rows = dom.window.document.querySelectorAll('tbody tr');
|
|
return Array.from(rows).map(r => r.cells[colIndex].getAttribute('data-value'));
|
|
}
|
|
|
|
console.log('\nTableSort — comparators');
|
|
|
|
test('text comparator: basic alphabetical', () => {
|
|
const cmp = (() => {
|
|
const dom = createDOM('<div></div>');
|
|
return dom.window.TableSort.comparators.text;
|
|
})();
|
|
assert.ok(cmp('apple', 'banana') < 0);
|
|
assert.ok(cmp('banana', 'apple') > 0);
|
|
assert.strictEqual(cmp('same', 'same'), 0);
|
|
});
|
|
|
|
test('text comparator: null/undefined handling', () => {
|
|
const dom = createDOM('<div></div>');
|
|
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('<div></div>');
|
|
const cmp = dom.window.TableSort.comparators.numeric;
|
|
assert.ok(cmp('1', '2') < 0);
|
|
assert.ok(cmp('10', '2') > 0);
|
|
assert.strictEqual(cmp('5', '5'), 0);
|
|
});
|
|
|
|
test('numeric comparator: NaN sorts last', () => {
|
|
const dom = createDOM('<div></div>');
|
|
const cmp = dom.window.TableSort.comparators.numeric;
|
|
assert.ok(cmp('abc', '5') > 0); // NaN > number (sorts last)
|
|
assert.ok(cmp('5', 'abc') < 0);
|
|
assert.strictEqual(cmp('abc', 'xyz'), 0); // both NaN
|
|
});
|
|
|
|
test('numeric comparator: negative numbers', () => {
|
|
const dom = createDOM('<div></div>');
|
|
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('<div></div>');
|
|
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);
|
|
assert.strictEqual(cmp('2024-01-01', '2024-01-01'), 0);
|
|
});
|
|
|
|
test('date comparator: invalid dates sort last', () => {
|
|
const dom = createDOM('<div></div>');
|
|
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('<div></div>');
|
|
const cmp = dom.window.TableSort.comparators.dbm;
|
|
assert.ok(cmp('-120 dBm', '-80 dBm') < 0);
|
|
assert.ok(cmp('-80 dBm', '-120 dBm') > 0);
|
|
assert.strictEqual(cmp('-95 dBm', '-95 dBm'), 0);
|
|
});
|
|
|
|
test('dBm comparator: works without suffix', () => {
|
|
const dom = createDOM('<div></div>');
|
|
const cmp = dom.window.TableSort.comparators.dbm;
|
|
assert.ok(cmp('-120', '-80') < 0);
|
|
});
|
|
|
|
console.log('\nTableSort — DOM sorting');
|
|
|
|
test('sort ascending by text column', () => {
|
|
const html = makeTable(
|
|
[{key: 'name'}],
|
|
[['Charlie'], ['Alice'], ['Bob']]
|
|
);
|
|
const dom = createDOM(html);
|
|
const table = dom.window.document.getElementById('t');
|
|
const inst = dom.window.TableSort.init(table, { defaultColumn: 'name', defaultDirection: 'asc' });
|
|
const vals = getColumnValues(dom, 0);
|
|
assert.deepStrictEqual(vals, ['Alice', 'Bob', 'Charlie']);
|
|
});
|
|
|
|
test('sort descending by numeric column', () => {
|
|
const html = makeTable(
|
|
[{key: 'val', type: 'numeric'}],
|
|
[['3'], ['1'], ['2']]
|
|
);
|
|
const dom = createDOM(html);
|
|
const table = dom.window.document.getElementById('t');
|
|
dom.window.TableSort.init(table, { defaultColumn: 'val', defaultDirection: 'desc' });
|
|
const vals = getColumnValues(dom, 0);
|
|
assert.deepStrictEqual(vals, ['3', '2', '1']);
|
|
});
|
|
|
|
test('click toggles direction', () => {
|
|
const html = makeTable(
|
|
[{key: 'name'}],
|
|
[['B'], ['A'], ['C']]
|
|
);
|
|
const dom = createDOM(html);
|
|
const table = dom.window.document.getElementById('t');
|
|
const inst = dom.window.TableSort.init(table, { defaultColumn: 'name', defaultDirection: 'asc' });
|
|
|
|
// Initially ascending
|
|
assert.deepStrictEqual(getColumnValues(dom, 0), ['A', 'B', 'C']);
|
|
|
|
// Click same header → descending
|
|
const th = dom.window.document.querySelector('th[data-sort-key="name"]');
|
|
th.click();
|
|
assert.deepStrictEqual(getColumnValues(dom, 0), ['C', 'B', 'A']);
|
|
|
|
// Click again → ascending
|
|
th.click();
|
|
assert.deepStrictEqual(getColumnValues(dom, 0), ['A', 'B', 'C']);
|
|
});
|
|
|
|
console.log('\nTableSort — aria-sort attributes');
|
|
|
|
test('aria-sort set correctly on active column', () => {
|
|
const html = makeTable(
|
|
[{key: 'a'}, {key: 'b'}],
|
|
[['1', 'x'], ['2', 'y']]
|
|
);
|
|
const dom = createDOM(html);
|
|
const table = dom.window.document.getElementById('t');
|
|
dom.window.TableSort.init(table, { defaultColumn: 'a', defaultDirection: 'asc' });
|
|
|
|
const thA = dom.window.document.querySelector('th[data-sort-key="a"]');
|
|
const thB = dom.window.document.querySelector('th[data-sort-key="b"]');
|
|
assert.strictEqual(thA.getAttribute('aria-sort'), 'ascending');
|
|
assert.strictEqual(thB.getAttribute('aria-sort'), 'none');
|
|
});
|
|
|
|
test('aria-sort updates on direction change', () => {
|
|
const html = makeTable(
|
|
[{key: 'a'}],
|
|
[['1'], ['2']]
|
|
);
|
|
const dom = createDOM(html);
|
|
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"]');
|
|
assert.strictEqual(th.getAttribute('aria-sort'), 'ascending');
|
|
|
|
th.click(); // toggle to desc
|
|
assert.strictEqual(th.getAttribute('aria-sort'), 'descending');
|
|
});
|
|
|
|
test('aria-sort updates when switching columns', () => {
|
|
const html = makeTable(
|
|
[{key: 'a'}, {key: 'b'}],
|
|
[['1', 'x'], ['2', 'y']]
|
|
);
|
|
const dom = createDOM(html);
|
|
const table = dom.window.document.getElementById('t');
|
|
dom.window.TableSort.init(table, { defaultColumn: 'a', defaultDirection: 'asc' });
|
|
|
|
const thB = dom.window.document.querySelector('th[data-sort-key="b"]');
|
|
thB.click(); // switch to column b
|
|
|
|
const thA = dom.window.document.querySelector('th[data-sort-key="a"]');
|
|
assert.strictEqual(thA.getAttribute('aria-sort'), 'none');
|
|
assert.strictEqual(thB.getAttribute('aria-sort'), 'ascending');
|
|
});
|
|
|
|
console.log('\nTableSort — visual indicator');
|
|
|
|
// The sort indicator is a Phosphor sprite since #1648 M2: the direction is
|
|
// carried by the <use href="...#ph-caret-up|down">, not by a text glyph.
|
|
// Reading it through the href keeps this test honest if the icon set changes
|
|
// again, because it then fails rather than passing on an empty textContent.
|
|
function sortArrowDirection(arrow) {
|
|
const use = arrow && arrow.querySelector('use');
|
|
const href = use && (use.getAttribute('href') || use.getAttribute('xlink:href'));
|
|
const m = href && href.match(/#ph-caret-(up|down)$/);
|
|
return m ? m[1] : null;
|
|
}
|
|
|
|
test('sort arrow shows on active column', () => {
|
|
const html = makeTable(
|
|
[{key: 'a'}],
|
|
[['1'], ['2']]
|
|
);
|
|
const dom = createDOM(html);
|
|
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');
|
|
// #1648 M2 replaced the up/down glyphs with Phosphor sprite refs, so the
|
|
// direction now lives in the <use href>, not in textContent.
|
|
assert.strictEqual(sortArrowDirection(arrow), 'up', 'ascending should point the caret up');
|
|
const activeTh = dom.window.document.querySelector('th[data-sort-key="a"]');
|
|
assert.strictEqual(activeTh.getAttribute('aria-sort'), 'ascending', 'ascending must be announced');
|
|
});
|
|
|
|
test('sort arrow changes on direction toggle', () => {
|
|
const html = makeTable(
|
|
[{key: 'a'}],
|
|
[['1'], ['2']]
|
|
);
|
|
const dom = createDOM(html);
|
|
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.strictEqual(sortArrowDirection(arrow), 'down', 'descending should point the caret down');
|
|
assert.strictEqual(th.getAttribute('aria-sort'), 'descending', 'descending must be announced');
|
|
});
|
|
|
|
console.log('\nTableSort — onSort callback');
|
|
|
|
test('onSort fires with column and direction', () => {
|
|
const html = makeTable(
|
|
[{key: 'a'}, {key: 'b'}],
|
|
[['1', 'x'], ['2', 'y']]
|
|
);
|
|
const dom = createDOM(html);
|
|
const table = dom.window.document.getElementById('t');
|
|
let called = null;
|
|
dom.window.TableSort.init(table, {
|
|
domReorder: false,
|
|
onSort: function(col, dir) { called = { col, dir }; }
|
|
});
|
|
|
|
const th = dom.window.document.querySelector('th[data-sort-key="a"]');
|
|
th.click();
|
|
assert.ok(called, 'onSort should fire');
|
|
assert.strictEqual(called.col, 'a');
|
|
assert.strictEqual(called.dir, 'asc');
|
|
});
|
|
|
|
console.log('\nTableSort — domReorder: false');
|
|
|
|
test('domReorder: false skips DOM sorting', () => {
|
|
const html = makeTable(
|
|
[{key: 'name'}],
|
|
[['C'], ['A'], ['B']]
|
|
);
|
|
const dom = createDOM(html);
|
|
const table = dom.window.document.getElementById('t');
|
|
dom.window.TableSort.init(table, { defaultColumn: 'name', defaultDirection: 'asc', domReorder: false });
|
|
|
|
// DOM order should NOT change
|
|
const vals = getColumnValues(dom, 0);
|
|
assert.deepStrictEqual(vals, ['C', 'A', 'B']);
|
|
});
|
|
|
|
console.log('\nTableSort — destroy');
|
|
|
|
test('destroy removes event handlers and cleans up', () => {
|
|
const html = makeTable(
|
|
[{key: 'a'}],
|
|
[['2'], ['1']]
|
|
);
|
|
const dom = createDOM(html);
|
|
const table = dom.window.document.getElementById('t');
|
|
const inst = dom.window.TableSort.init(table, { defaultColumn: 'a', defaultDirection: 'asc' });
|
|
|
|
inst.destroy();
|
|
|
|
const th = dom.window.document.querySelector('th[data-sort-key="a"]');
|
|
assert.strictEqual(th.getAttribute('aria-sort'), null, 'aria-sort should be removed');
|
|
assert.ok(!th.classList.contains('sort-active'), 'sort-active should be removed');
|
|
assert.strictEqual(th.querySelector('.sort-arrow'), null, 'arrow should be removed');
|
|
});
|
|
|
|
console.log('\nTableSort — custom comparators');
|
|
|
|
test('custom comparator overrides built-in', () => {
|
|
const html = makeTable(
|
|
[{key: 'val', type: 'numeric'}],
|
|
[['3'], ['1'], ['2']]
|
|
);
|
|
const dom = createDOM(html);
|
|
const table = dom.window.document.getElementById('t');
|
|
// Custom: reverse numeric
|
|
dom.window.TableSort.init(table, {
|
|
defaultColumn: 'val', defaultDirection: 'asc',
|
|
comparators: { val: function(a, b) { return Number(b) - Number(a); } }
|
|
});
|
|
const vals = getColumnValues(dom, 0);
|
|
assert.deepStrictEqual(vals, ['3', '2', '1']); // reversed
|
|
});
|
|
|
|
console.log('\nTableSort — date sort with data-type="date"');
|
|
|
|
test('date column sorts correctly', () => {
|
|
const html = 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 table = dom.window.document.getElementById('t');
|
|
dom.window.TableSort.init(table, { defaultColumn: 'ts', defaultDirection: 'asc' });
|
|
const vals = getColumnValues(dom, 0);
|
|
assert.deepStrictEqual(vals, ['2024-01-01T00:00:00Z', '2024-06-15T10:00:00Z', '2024-12-25T23:59:59Z']);
|
|
});
|
|
|
|
// Summary
|
|
console.log(`\n${passed + failed} tests, ${passed} passed, ${failed} failed\n`);
|
|
process.exit(failed > 0 ? 1 : 0);
|