mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-12 22:18:37 +00:00
Fixes #2054 and #2079. Packet-row tests could outlive the fixture's default 15-minute window. An audit of 115 wired suites identified four needing explicit fixture windows: 180 minutes on mobile, 1440 on desktop. Mobile rejects larger saved windows, so a desktop-only preference was insufficient. Packet-icon checks now require a real path-bearing packet and both detail controls. Default-window coverage exposed #2079: responsive column hiding also hid the spanning empty-message cell. Skip structural single-cell spanning rows when hiding individual columns, preserving status messages and virtual-scroll spacer height. Ordinary and partial-span data cells retain their behavior; application defaults stay unchanged. - Red evidence: `bae5e78` fails the aged-fixture row assertion; `9b75cf6` fails spanning-row visibility. Guards/setup make them pass. - Validation: 183 standalone suites; 82 targeted browser cases on a fixture aged over 30 minutes (one pre-existing node-gesture skip). Broader browser run: 131 passed, 3 fixture-dependent skips. - Browser verified: fixture-backed Go server and Chromium; `coverage/2079-mobile-empty.png`. Real mobile scrolling retains spacer height and has no horizontal overflow. - E2E assertions: `test-observer-iata-1188-e2e.js` default-window cases and `test-table-fluid-e2e.js:58` shared-helper regression, both under `tests/e2e/`. - Three independent reviews completed; packet-icon coverage feedback addressed in `760057d`. - Populated mobile axe coverage remains part of #1995. - One constant-time guard per already-visited row; no new requests or collections. ## Preflight overrides - External `run-all.sh` unavailable; repository checks run directly. Local runs use UTF-8, the release harness's `GITHUB_REF_NAME`, and a 60s navigation budget; repository timeouts are unchanged.
170 lines
8.0 KiB
JavaScript
170 lines
8.0 KiB
JavaScript
/**
|
|
* E2E test (#1188): observer IATA must render alongside observer name
|
|
* on packets rows and in the detail pane. Plus, the wireshark-style
|
|
* filter grammar must accept `observer_iata` / `iata` expressions.
|
|
*
|
|
* Runs against the e2e fixture (see test-fixtures/e2e-fixture.db).
|
|
* Observers in the fixture carry IATA codes (e.g. SJC, OAK, MRY),
|
|
* so once the UI changes land, at least one rendered packet row must
|
|
* carry one of those codes next to its observer name.
|
|
*
|
|
* Usage: BASE_URL=http://localhost:13581 node test-observer-iata-1188-e2e.js
|
|
*/
|
|
const { chromium } = require('playwright');
|
|
|
|
const BASE = process.env.BASE_URL || 'http://localhost:3000';
|
|
|
|
async function test(name, fn) {
|
|
try {
|
|
await fn();
|
|
console.log(` \u2705 ${name}`);
|
|
} catch (err) {
|
|
console.log(` \u274c ${name}: ${err.message}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
function assert(cond, msg) {
|
|
if (!cond) throw new Error(msg || 'Assertion failed');
|
|
}
|
|
|
|
async function run() {
|
|
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({ viewport: { width: 1400, height: 900 } });
|
|
const page = await ctx.newPage();
|
|
page.setDefaultTimeout(15000);
|
|
|
|
console.log(`\nRunning observer-IATA E2E tests against ${BASE}\n`);
|
|
|
|
// #2054: row-dependent tests below opt into a fixture window. Keep the
|
|
// actual first-visit default covered independently of those preferences.
|
|
for (const width of [1400, 375]) {
|
|
await test(`Default 15-minute window renders an empty state at ${width}px`, async () => {
|
|
const emptyContext = await browser.newContext({ viewport: { width, height: 900 } });
|
|
try {
|
|
const emptyPage = await emptyContext.newPage();
|
|
const queries = [];
|
|
await emptyPage.route('**/api/packets?*', async route => {
|
|
queries.push(new URL(route.request().url()).searchParams);
|
|
await route.fulfill({ json: { packets: [], total: 0 } });
|
|
});
|
|
const started = Date.now();
|
|
await emptyPage.goto(`${BASE}/#/packets`, { waitUntil: 'domcontentloaded' });
|
|
await emptyPage.locator('#pktBody').getByText('No packets found', { exact: true }).waitFor();
|
|
assert(await emptyPage.inputValue('#fTimeWindow') === '15', 'first visit must select 15 minutes');
|
|
assert(await emptyPage.evaluate(() => localStorage.getItem('meshcore-time-window')) === null,
|
|
'fresh context must not inherit a fixture window');
|
|
assert(queries.length > 0, 'empty state must follow an API request');
|
|
for (const query of queries) {
|
|
const since = Date.parse(query.get('since'));
|
|
assert(since >= started - 15 * 60000 && since <= Date.now() - 15 * 60000,
|
|
'default request must use the 15-minute cutoff');
|
|
assert(!query.has('hash'), 'default request must not bypass the cutoff with a hash');
|
|
}
|
|
assert(await emptyPage.locator('#pktBody tr[data-hash]').count() === 0,
|
|
'empty response must not render packet rows');
|
|
assert(await emptyPage.locator('#pktTable thead th').count() > 0,
|
|
'empty table must retain its column headings');
|
|
} finally {
|
|
await emptyContext.close();
|
|
}
|
|
});
|
|
}
|
|
|
|
await test('Packets table renders an IATA badge in an observer cell', async () => {
|
|
await page.goto(`${BASE}/#/packets`, { waitUntil: 'domcontentloaded' });
|
|
// Wide time window so fixture rows are in scope
|
|
await page.evaluate(() => localStorage.setItem('meshcore-time-window', '525600'));
|
|
await page.reload({ waitUntil: 'load' });
|
|
await page.waitForSelector('[data-loaded="true"]', { timeout: 20000 });
|
|
await page.waitForSelector('table tbody tr:not([id^=vscroll])', { timeout: 15000 });
|
|
|
|
// Cells in the Observer column should contain a `.badge-iata` element
|
|
// for at least one row that has a known IATA.
|
|
const iataBadges = await page.$$('td.col-observer .badge-iata');
|
|
assert(iataBadges.length > 0,
|
|
`expected at least one .badge-iata inside a .col-observer cell; got ${iataBadges.length}`);
|
|
|
|
// The badge text should be a recognizable IATA code (3 uppercase letters)
|
|
const text = (await iataBadges[0].textContent() || '').trim();
|
|
assert(/^[A-Z]{3}$/.test(text), `expected 3-letter IATA in badge, got "${text}"`);
|
|
});
|
|
|
|
await test('Filter grammar: observer_iata == "<code>" narrows the table', async () => {
|
|
await page.goto(`${BASE}/#/packets`, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForSelector('[data-loaded="true"]', { timeout: 20000 });
|
|
await page.waitForSelector('table tbody tr:not([id^=vscroll])', { timeout: 15000 });
|
|
|
|
// Pick the first IATA shown in any badge in the table
|
|
const firstBadge = await page.$('td.col-observer .badge-iata');
|
|
assert(firstBadge, 'no .badge-iata found to pick a filter value from');
|
|
const iata = (await firstBadge.textContent() || '').trim();
|
|
assert(/^[A-Z]{3}$/.test(iata), `expected IATA, got "${iata}"`);
|
|
|
|
// Apply the filter — `iata == "XXX"` should leave rows visible
|
|
const input = await page.$('#packetFilterInput');
|
|
assert(input, 'packet filter input not found');
|
|
await input.fill(`iata == "${iata}"`);
|
|
await page.waitForTimeout(500); // debounce
|
|
|
|
const rowsAfter = await page.$$('table tbody tr:not([id^=vscroll])');
|
|
assert(rowsAfter.length > 0, `expected matching rows for iata == "${iata}"`);
|
|
|
|
// Every remaining observer cell should carry the same IATA
|
|
const badges = await page.$$('td.col-observer .badge-iata');
|
|
for (const b of badges) {
|
|
const t = (await b.textContent() || '').trim();
|
|
assert(t === iata, `unexpected IATA ${t} in row when filter is iata == "${iata}"`);
|
|
}
|
|
});
|
|
|
|
await test('Mobile viewport (375px): observer column drops from row, but IATA badge still appears in expanded detail panel (#1415 locked spec)', async () => {
|
|
// #1415 locked column-priority spec: col-observer is tier-3 (desktop only,
|
|
// hidden ≤1024px). The iron rule: anything hidden from the row at the
|
|
// current viewport MUST appear in the expanded Details pane. So at 375px:
|
|
// (a) `td.col-observer` is hidden (display:none via .col-hidden)
|
|
// (b) tapping a row opens the panel whose .detail-meta carries the
|
|
// Observer row + .badge-iata next to the observer name.
|
|
const mobile = await browser.newContext({ viewport: { width: 375, height: 812 } });
|
|
const mpage = await mobile.newPage();
|
|
mpage.setDefaultTimeout(15000);
|
|
// #2054: mobile clamps the desktop fixture window (525600) back to 15.
|
|
await mpage.addInitScript(() => localStorage.setItem('meshcore-time-window', '180'));
|
|
await mpage.goto(`${BASE}/#/packets`, { waitUntil: 'domcontentloaded' });
|
|
await mpage.waitForSelector('[data-loaded="true"]', { timeout: 20000 });
|
|
await mpage.waitForSelector('table tbody tr:not([id^=vscroll])', { timeout: 15000 });
|
|
|
|
// (a) observer column hidden in the row at 375px
|
|
const rowObserverVisible = await mpage.$eval(
|
|
'td.col-observer',
|
|
el => window.getComputedStyle(el).display !== 'none'
|
|
).catch(() => false);
|
|
assert(!rowObserverVisible,
|
|
'observer column should be hidden in rows at 375px (tier-3, desktop-only per #1415 spec)');
|
|
|
|
// (b) tap first row → detail panel renders observer + IATA badge
|
|
const firstRow = mpage.locator('#pktBody tr[data-hash]').first();
|
|
assert(await firstRow.count() > 0, 'no packet row found to tap');
|
|
await firstRow.click();
|
|
await mpage.waitForSelector('.detail-meta', { timeout: 10000 });
|
|
const detailIata = await mpage.$('.detail-meta .badge-iata');
|
|
assert(detailIata, '.badge-iata must appear in .detail-meta after tapping a row at 375px');
|
|
const box = await detailIata.boundingBox();
|
|
assert(box && box.width > 0 && box.height > 0,
|
|
`.detail-meta .badge-iata has zero/no dimensions: ${box && (box.width + 'x' + box.height)}`);
|
|
await mobile.close();
|
|
});
|
|
|
|
await browser.close();
|
|
console.log(`\nAll observer-IATA E2E tests passed.\n`);
|
|
}
|
|
|
|
run().catch(err => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|