mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-10 05:57:17 +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>
182 lines
8.3 KiB
JavaScript
182 lines
8.3 KiB
JavaScript
/**
|
|
* E2E (#1034 PR1): Channel Add modal + sectioned sidebar.
|
|
*
|
|
* Boots a headless Chromium against a locally running corescope-server and
|
|
* exercises:
|
|
* - 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
|
|
* - PSK add flow: invalid hex → error; valid hex → modal closes
|
|
*
|
|
* Usage: BASE_URL=http://localhost:38201 node test-channel-modal-e2e.js
|
|
*/
|
|
'use strict';
|
|
const { chromium } = require('playwright');
|
|
|
|
const BASE = process.env.BASE_URL || 'http://localhost:38201';
|
|
|
|
let passed = 0, failed = 0;
|
|
async function step(name, fn) {
|
|
try { await fn(); passed++; console.log(' ✓ ' + name); }
|
|
catch (e) { failed++; console.error(' ✗ ' + name + ': ' + e.message); }
|
|
}
|
|
function assert(c, m) { if (!c) throw new Error(m || 'assertion failed'); }
|
|
|
|
(async () => {
|
|
const browser = await chromium.launch({
|
|
headless: true,
|
|
executablePath: process.env.CHROMIUM_PATH || undefined,
|
|
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
|
|
});
|
|
const ctx = await browser.newContext();
|
|
const page = await ctx.newPage();
|
|
page.setDefaultTimeout(8000);
|
|
page.on('pageerror', (e) => console.error('[pageerror]', e.message));
|
|
|
|
console.log(`\n=== #1034 PR1 E2E against ${BASE} ===`);
|
|
|
|
await step('navigate to /channels', async () => {
|
|
await page.goto(BASE + '/#/channels', { waitUntil: 'domcontentloaded' });
|
|
await page.waitForSelector('#chAddChannelBtn', { timeout: 8000 });
|
|
});
|
|
|
|
await step('Add Channel button is visible', async () => {
|
|
// The visible label is "+ Add" (public/channels.js:746): it was shortened
|
|
// to fit the header, and the accessible name moved to aria-label. Assert
|
|
// the accessible name, which is the part a screen reader and this test
|
|
// actually depend on, not the truncated glyph text.
|
|
const label = await page.getAttribute('#chAddChannelBtn', 'aria-label');
|
|
assert(label && /add channel/i.test(label), 'aria-label: ' + label);
|
|
const text = await page.textContent('#chAddChannelBtn');
|
|
assert(text && text.trim().length > 0, 'button must still have a visible label');
|
|
});
|
|
|
|
await step('modal hidden on load', async () => {
|
|
const isHidden = await page.evaluate(() => {
|
|
const m = document.getElementById('chAddChannelModal');
|
|
return !!m && (m.classList.contains('hidden') || m.hasAttribute('hidden'));
|
|
});
|
|
assert(isHidden, 'modal should start hidden');
|
|
});
|
|
|
|
await step('clicking [+ Add Channel] opens modal', async () => {
|
|
await page.click('#chAddChannelBtn');
|
|
await page.waitForSelector('#chAddChannelModal:not(.hidden)', { timeout: 3000 });
|
|
const visible = await page.isVisible('#chAddChannelModal');
|
|
assert(visible, 'modal should be visible after click');
|
|
});
|
|
|
|
await step('modal renders all three section titles', async () => {
|
|
const html = await page.innerHTML('#chAddChannelModal');
|
|
assert(html.includes('Generate PSK Channel'), 'section 1 missing');
|
|
assert(html.includes('Add Private Channel (PSK)'), 'section 2 missing');
|
|
assert(html.includes('Monitor Hashtag Channel'), 'section 3 missing');
|
|
});
|
|
|
|
await step('modal renders QR placeholders', async () => {
|
|
assert(await page.isVisible('#qr-output'), '#qr-output missing');
|
|
const scanBtn = await page.$('#scan-qr-btn');
|
|
assert(scanBtn, '#scan-qr-btn missing');
|
|
const disabled = await scanBtn.getAttribute('disabled');
|
|
assert(disabled === null, '#scan-qr-btn must be enabled (wired in #1034 PR3)');
|
|
});
|
|
|
|
await step('modal renders privacy footer', async () => {
|
|
const footer = await page.textContent('#chAddChannelModal .ch-modal-footer');
|
|
assert(/Keys stay in your browser/.test(footer), 'footer text missing: ' + footer);
|
|
assert(/passive observer/.test(footer), 'passive observer text missing');
|
|
});
|
|
|
|
await step('modal renders case-sensitivity warning', async () => {
|
|
const warn = await page.textContent('#chAddChannelModal .ch-modal-warn');
|
|
assert(/[Cc]ase-sensitive/.test(warn), 'warning missing: ' + warn);
|
|
});
|
|
|
|
await step('PSK add: invalid hex shows inline error', async () => {
|
|
await page.fill('#chPskKey', 'not-hex');
|
|
await page.click('#chPskAddBtn');
|
|
await page.waitForFunction(() => {
|
|
const e = document.getElementById('chPskError');
|
|
return e && e.style.display !== 'none' && /hex/i.test(e.textContent);
|
|
}, { timeout: 3000 });
|
|
});
|
|
|
|
await step('close button (✕) hides modal', async () => {
|
|
await page.click('#chModalClose');
|
|
await page.waitForFunction(() => {
|
|
const m = document.getElementById('chAddChannelModal');
|
|
return m && m.classList.contains('hidden');
|
|
}, { timeout: 3000 });
|
|
});
|
|
|
|
await step('sidebar renders the Network and Encrypted sections', async () => {
|
|
// My Channels is NOT among these: public/channels.js only emits
|
|
// .ch-section-mychannels when the visitor has added a channel
|
|
// (`if (mine.length > 0)`), and this browser profile is fresh. Asserting
|
|
// it here is what made this step time out for as long as the suite went
|
|
// unrun. It is asserted after the PSK add below, where it is guaranteed.
|
|
await page.waitForFunction(() => {
|
|
const el = document.getElementById('chList');
|
|
if (!el) return false;
|
|
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(/Network/.test(joined), 'Network header missing: ' + joined);
|
|
assert(/Encrypted/.test(joined), 'Encrypted header missing: ' + joined);
|
|
const mine = await page.$$eval('.ch-section-mychannels', els => els.length);
|
|
assert(mine === 0, 'a fresh profile must not have a My Channels section, found ' + mine);
|
|
});
|
|
|
|
await step('Encrypted section is collapsed by default', async () => {
|
|
const collapsed = await page.getAttribute('.ch-section-encrypted', 'data-encrypted-collapsed');
|
|
assert(collapsed === 'true', 'expected data-encrypted-collapsed=true, got ' + collapsed);
|
|
const bodyHidden = await page.evaluate(() => {
|
|
const b = document.getElementById('chEncryptedBody');
|
|
return b ? b.hasAttribute('hidden') : null;
|
|
});
|
|
assert(bodyHidden === true, 'encrypted body should be hidden initially');
|
|
});
|
|
|
|
await step('clicking Encrypted toggle expands it', async () => {
|
|
await page.click('#chEncryptedToggle');
|
|
const bodyHidden = await page.evaluate(() => {
|
|
const b = document.getElementById('chEncryptedBody');
|
|
return b ? b.hasAttribute('hidden') : null;
|
|
});
|
|
assert(bodyHidden === false, 'encrypted body should be visible after toggle');
|
|
});
|
|
|
|
await step('PSK add: valid hex closes modal and persists key', async () => {
|
|
await page.click('#chAddChannelBtn');
|
|
await page.waitForSelector('#chAddChannelModal:not(.hidden)');
|
|
const validHex = 'cafebabe' + '00112233' + '44556677' + '8899aabb';
|
|
await page.fill('#chPskKey', validHex);
|
|
await page.fill('#chPskName', 'E2E Test Channel');
|
|
await page.click('#chPskAddBtn');
|
|
await page.waitForFunction(() => {
|
|
const m = document.getElementById('chAddChannelModal');
|
|
return m && m.classList.contains('hidden');
|
|
}, { timeout: 5000 });
|
|
const stored = await page.evaluate(() => localStorage.getItem('corescope_channel_keys') || '');
|
|
assert(/cafebabe/i.test(stored), 'expected stored key in localStorage corescope_channel_keys, got: ' + stored);
|
|
});
|
|
|
|
await step('My Channels appears once the visitor has added one', async () => {
|
|
// The section is conditional (public/channels.js, `if (mine.length > 0)`),
|
|
// so this is the only point in the suite where it can be demanded.
|
|
await page.waitForFunction(() => {
|
|
const el = document.getElementById('chList');
|
|
return !!el && !!el.querySelector('.ch-section-mychannels');
|
|
}, { timeout: 8000 });
|
|
const headers = await page.$$eval('.ch-section-header', els => els.map(e => e.textContent.trim()));
|
|
assert(/My Channels/.test(headers.join(' | ')), 'My Channels header missing: ' + headers.join(' | '));
|
|
});
|
|
|
|
await browser.close();
|
|
|
|
console.log(`\n=== Results: passed ${passed} failed ${failed} ===`);
|
|
process.exit(failed > 0 ? 1 : 0);
|
|
})().catch(e => { console.error(e); process.exit(1); });
|