Files
meshcore-analyzer/test-observer-iata-1188-e2e.js
T
Kpa-clawbot b2d654bf61 fix(#1415, #1458): packets layout + mobile chrome + semantic-first detail (#1459)
## Closes #1415 — packets cross-viewport jank
## Closes #1458 — Tufte mobile-packets P0 findings (folded into same
branch)

Single PR covers both issues — they touch the same files
(`public/packets.js`,
`public/style.css`) and a split would invite merge thrash.

### #1415 — column priority + chrome compaction

Locked column-priority tiers (operator spec):

| Tier | Viewport | Columns |
|---|---|---|
| 1 | always (mobile through desktop) | expand · time · type · details |
| 2 | tablet+ (>768px) | path |
| 3 | desktop only (>1024px) | hash · observer · rpt |

Enforced via existing `data-priority` system in `TableResponsive.apply`
(priorities 3 → hide ≤1024, 5 → hide ≤768).

CSS:
- `.col-expand` pinned to `width/min-width/max-width: 32px` at every
viewport
  — kills the 50–180px dead column that pushed every data column right.
- `.col-details` capped at `max-width: 480px` so wide viewports stop
wasting
  hundreds of px on the last column.
- `@media (max-width: 480px)` hides page-header BYOP, shrinks the h2,
and
  tightens row padding → pre-table chrome drops from ~280px to ~140px.

### #1458 — Tufte mobile P0 findings

**P0-A: semantic-first detail panel.** Was: `"Packet Byte Breakdown (134
bytes)"`
title + giant neon hex grid above the meaningful fields. Now: type badge
+
decoded summary + hop count + `src → dst` lead the panel, followed by
the
existing `.detail-meta` dl (reordered: Payload Type → Path → Timestamp →
Observer).

**P0-B: raw-bytes disclosure.** Hex legend / hex dump / field table
wrapped in
`<details class="detail-technical">`. Disclosure copy reads "Show raw
bytes".
Collapsed by default on phones (`window.innerWidth ≤ 480`), expanded on
tablet+.

**P0-C: mobile filter-zone collapse.** The always-on filter-expression
input
above `.filter-bar` is now wrapped with `.pkt-filter-expr` and hidden
under
the `@media (max-width: 480px)` block. Reveals when the existing
"Filters ▾"
toggle adds `.filters-expanded` to the sibling `.filter-bar` (CSS
`:has()`
selector — one tap reveals both chrome rows together).

### TDD

`test-issue-1415-packets-layout.js` — pure source-grep, no browser:
- col-expand class on first `<th>` + `<td>` + CSS 32px pin
- locked column-priority tier values per column
- `.col-details` max-width ≤ 480px
- mobile @media block: hides BYOP, hides `.pkt-filter-expr` (revealed by
  `.filters-expanded`)
- detail-meta order: Payload Type before Observer
- `<details class="detail-technical">` wrapper exists with "Show raw
bytes"
  summary
- detail-title leads with a type badge; `.detail-srcdst` emitted
- old "Packet Byte Breakdown (N bytes)" title literal removed

Red commit `d4372d82` (8 assertion failures, no compile errors), green
commit `4fab9dbd` (#1415 work), follow-up commit `a5218035` (#1458 work)
keeps everything green. 26 assertions, 0 failed.

---------

Co-authored-by: openclaw-bot <bot@openclaw>
2026-05-28 05:38:28 -07:00

135 lines
6.1 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`);
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);
await mpage.goto(`${BASE}/#/packets`, { waitUntil: 'domcontentloaded' });
await mpage.evaluate(() => localStorage.setItem('meshcore-time-window', '525600'));
await mpage.reload({ waitUntil: 'load' });
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 = await mpage.$('table tbody tr[data-hash]');
assert(firstRow, '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);
});