mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-09-26 03:53:58 +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>
189 lines
8.5 KiB
JavaScript
189 lines
8.5 KiB
JavaScript
/**
|
|
* Playwright E2E — Scope column on the Packets tab.
|
|
*
|
|
* transmissions.scope_name has three states and the column must render each one
|
|
* distinguishably: em dash (not transport-scoped), muted "unknown"
|
|
* (transport-scoped, region unmatched) and the region name on a match.
|
|
*
|
|
* Usage: node test-packets-scope-column.js
|
|
* BASE_URL=https://staging.on8ar.eu node test-packets-scope-column.js
|
|
*/
|
|
const { chromium } = require('playwright');
|
|
|
|
const BASE = process.env.BASE_URL || 'http://localhost:3000';
|
|
const results = [];
|
|
|
|
async function test(name, fn) {
|
|
try {
|
|
await fn();
|
|
results.push({ name, pass: true });
|
|
console.log(` ✅ ${name}`);
|
|
} catch (err) {
|
|
results.push({ name, pass: false, error: err.message });
|
|
console.log(` ❌ ${name}: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
function assert(condition, msg) {
|
|
if (!condition) throw new Error(msg || 'Assertion failed');
|
|
}
|
|
|
|
async function gotoPackets(page) {
|
|
await page.goto(BASE + '/#/packets', { waitUntil: 'domcontentloaded' });
|
|
await page.evaluate(() => {
|
|
localStorage.removeItem('packets-visible-cols');
|
|
localStorage.removeItem('packets-known-cols');
|
|
// The packets page defaults to a 15-minute window (public/packets.js,
|
|
// savedTimeWindowMin). CI freshens the fixture so its newest packet is
|
|
// "now" at the START of the job, and the E2E step runs for a quarter of
|
|
// an hour or more, so a suite that runs late in the list sees an empty
|
|
// table and "No packets found" through no fault of its own. This suite is
|
|
// about the Scope column, not about the window, so pin the window wide.
|
|
localStorage.setItem('meshcore-time-window', '10080');
|
|
});
|
|
await page.reload({ waitUntil: 'networkidle' });
|
|
// Wait for a row that carries real column cells, not merely for any <tr>.
|
|
// The table renders a full-width placeholder row while it loads, which
|
|
// satisfies a bare `tbody tr` selector: the suite then read an empty table
|
|
// and reported "no td.col-scope rendered" / "no packet rows found". That is
|
|
// the 4-passed-3-failed signature this suite showed intermittently while it
|
|
// was unwired, and it is a race in the wait, not a product fault.
|
|
//
|
|
// state:'attached', not the default 'visible': every assertion below reads
|
|
// the DOM through $$eval/evaluate, which sees a cell whose column is hidden
|
|
// by a preference class. Waiting for visibility asks for more than the
|
|
// suite needs and times out on a table that is perfectly ready to inspect.
|
|
try {
|
|
await page.waitForSelector('#pktTable tbody tr:not([id^=vscroll]) td.col-type',
|
|
{ state: 'attached', timeout: 30000 });
|
|
} catch (e) {
|
|
// A bare TimeoutError says nothing about why. Report what the table held.
|
|
const state = await page.evaluate(() => {
|
|
const tb = document.querySelector('#pktTable tbody');
|
|
if (!tb) return { table: false };
|
|
const trs = Array.from(tb.querySelectorAll('tr'));
|
|
return {
|
|
table: true,
|
|
rows: trs.length,
|
|
firstRowHtml: trs.length ? trs[0].innerHTML.slice(0, 200) : null,
|
|
tableClass: document.getElementById('pktTable').className,
|
|
};
|
|
}).catch(() => null);
|
|
throw new Error('packets table never produced a row with td.col-type: ' + JSON.stringify(state));
|
|
}
|
|
}
|
|
|
|
(async () => {
|
|
console.log(`\nPackets Scope column — ${BASE}\n`);
|
|
const browser = await chromium.launch();
|
|
const page = await browser.newPage({ viewport: { width: 1400, height: 1000 } });
|
|
|
|
await gotoPackets(page);
|
|
|
|
await test('Scope header sits between Type and Observer', async () => {
|
|
const headers = await page.$$eval('#pktTable thead th', ths =>
|
|
ths.map(th => th.textContent.trim()));
|
|
const iType = headers.indexOf('Type');
|
|
const iScope = headers.indexOf('Scope');
|
|
const iObserver = headers.indexOf('Observer');
|
|
assert(iScope !== -1, 'Scope header missing, got: ' + headers.join('|'));
|
|
assert(iType < iScope && iScope < iObserver,
|
|
`expected Type < Scope < Observer, got ${iType} < ${iScope} < ${iObserver}`);
|
|
});
|
|
|
|
await test('Scope column is visible by default', async () => {
|
|
const hidden = await page.$eval('#pktTable', t => t.classList.contains('hide-col-scope'));
|
|
assert(!hidden, 'table carries hide-col-scope on a fresh visit');
|
|
const cellCount = await page.$$eval('#pktTable tbody td.col-scope', tds => tds.length);
|
|
assert(cellCount > 0, 'no td.col-scope rendered');
|
|
});
|
|
|
|
await test('every row renders exactly one scope cell', async () => {
|
|
const { rows, cells } = await page.evaluate(() => {
|
|
const trs = Array.from(document.querySelectorAll('#pktTable tbody tr'))
|
|
.filter(tr => !tr.id.startsWith('vscroll') && tr.querySelector('td.col-type'));
|
|
return {
|
|
rows: trs.length,
|
|
cells: trs.filter(tr => tr.querySelectorAll('td.col-scope').length === 1).length,
|
|
};
|
|
});
|
|
assert(rows > 0, 'no packet rows found');
|
|
assert(rows === cells, `${rows} rows but ${cells} have exactly one scope cell`);
|
|
});
|
|
|
|
await test('non-transport rows render an em dash', async () => {
|
|
const found = await page.evaluate(() => {
|
|
for (const tr of document.querySelectorAll('#pktTable tbody tr')) {
|
|
const type = tr.querySelector('td.col-type');
|
|
const scope = tr.querySelector('td.col-scope');
|
|
if (!type || !scope) continue;
|
|
// No T badge → FLOOD or DIRECT → no transport scope possible.
|
|
if (!type.querySelector('.badge-transport')) return scope.textContent.trim();
|
|
}
|
|
return null;
|
|
});
|
|
assert(found !== null, 'no non-transport row on screen to check');
|
|
assert(found === '—', `expected em dash, got "${found}"`);
|
|
});
|
|
|
|
await test('sorting by Scope pins the empties last', async () => {
|
|
await page.click('#pktTable thead th.col-scope');
|
|
await page.waitForTimeout(600);
|
|
const values = await page.$$eval('#pktTable tbody td.col-scope', tds =>
|
|
tds.map(td => td.textContent.trim()));
|
|
const lastScoped = values.reduce((acc, v, i) => (v !== '—' ? i : acc), -1);
|
|
const firstEmpty = values.indexOf('—');
|
|
if (lastScoped === -1 || firstEmpty === -1) {
|
|
console.log(' (only one scope state on screen, ordering not exercised)');
|
|
return;
|
|
}
|
|
assert(firstEmpty > lastScoped,
|
|
`em dashes must follow every scoped row; first dash at ${firstEmpty}, last scoped at ${lastScoped}`);
|
|
});
|
|
|
|
await test('Columns menu can hide and restore the Scope column', async () => {
|
|
// A checkbox click bubbles to the document handler that closes the menu, so
|
|
// each toggle needs its own open.
|
|
const toggleScope = async () => {
|
|
await page.click('#colToggleBtn');
|
|
await page.waitForTimeout(200);
|
|
const box = await page.$('#colToggleMenu input[data-col="scope"]');
|
|
assert(box, 'no Scope checkbox in the Columns menu');
|
|
const wasChecked = await box.isChecked();
|
|
await box.click();
|
|
await page.waitForTimeout(300);
|
|
return wasChecked;
|
|
};
|
|
|
|
assert(await toggleScope(), 'Scope checkbox should start checked');
|
|
assert(await page.$eval('#pktTable', t => t.classList.contains('hide-col-scope')),
|
|
'unchecking should add hide-col-scope');
|
|
assert(!(await toggleScope()), 'Scope checkbox should now be unchecked');
|
|
assert(!(await page.$eval('#pktTable', t => t.classList.contains('hide-col-scope'))),
|
|
're-checking should remove hide-col-scope');
|
|
});
|
|
|
|
await test('a column added after the visitor saved prefs arrives visible', async () => {
|
|
// Simulate a returning visitor whose stored prefs predate the Scope column.
|
|
await page.evaluate(() => {
|
|
localStorage.setItem('packets-visible-cols',
|
|
JSON.stringify(['time', 'hash', 'size', 'type', 'observer', 'path', 'rpt', 'details']));
|
|
localStorage.removeItem('packets-known-cols');
|
|
});
|
|
await page.reload({ waitUntil: 'networkidle' });
|
|
await page.waitForSelector('#pktTable tbody tr:not([id^=vscroll])', { timeout: 30000 });
|
|
assert(!(await page.$eval('#pktTable', t => t.classList.contains('hide-col-scope'))),
|
|
'Scope should be shown for prefs saved before the column existed');
|
|
// Region was explicitly absent from those prefs AND predates the column, so
|
|
// the backfill must not resurrect it — only genuinely new keys get defaulted.
|
|
assert(await page.$eval('#pktTable', t => t.classList.contains('hide-col-region')),
|
|
'backfill must not re-enable a column the visitor had hidden');
|
|
});
|
|
|
|
await browser.close();
|
|
|
|
const failed = results.filter(r => !r.pass);
|
|
console.log(`\n=== ${results.length - failed.length} passed, ${failed.length} failed ===`);
|
|
process.exit(failed.length ? 1 : 0);
|
|
})();
|