Files
meshcore-analyzer/test-issue-1415-packets-layout.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

170 lines
8.3 KiB
JavaScript

/**
* #1415 — Packets cross-viewport jank source-grep test.
*
* Asserts the four code-level invariants required by the layout fix:
*
* 1. Expand-chevron column is pinned narrow at every viewport via an
* explicit `.col-expand` class on the first <th>/<td> AND a CSS rule
* pinning its width to ~32px (max-width ≤ 36px).
* 2. DETAILS column is capped — `.col-details` has a `max-width` ≤ 480px
* so wide viewports stop wasting hundreds of px on the last column.
* 3. Mobile chrome compaction — the `@media (max-width: 480px)` block
* hides `.col-details` (so the table doesn't carry the dead column to
* mobile) AND hides the BYOP button in `.page-header` (operator
* request: reclaim 60+ px of pre-table chrome).
* 4. Mobile-priority detail order — `renderDetail()` renders the Payload
* Type as the FIRST `<dt>` of `.detail-meta` (operator's "lead with
* packet type"), and wraps the byte-breakdown / hex-dump / field-table
* into a `<details class="detail-technical">` element so the
* technical fields collapse on mobile (collapsed by default, open on
* desktop via the `open` attribute being conditionally set).
*
* Strategy: pure source-grep — no browser, no playwright. The grep is the
* gate. If someone reverts any of the four fixes, the corresponding assert
* fails. Cheap to run, deterministic, runs in CI without browser deps.
*/
'use strict';
const fs = require('fs');
const path = require('path');
let passed = 0, failed = 0;
function assert(cond, msg) {
if (cond) { passed++; console.log(' \u2705 ' + msg); }
else { failed++; console.error(' \u274c ' + msg); }
}
const pktJs = fs.readFileSync(path.join(__dirname, 'public/packets.js'), 'utf8');
const css = fs.readFileSync(path.join(__dirname, 'public/style.css'), 'utf8');
// ── 1. col-expand class + CSS pin ────────────────────────────────────────
assert(
/<th[^>]*class="col-expand"/.test(pktJs),
'packets.js header has <th class="col-expand"> on the first column'
);
assert(
/<td class="col-expand"/.test(pktJs),
'packets.js row builders emit <td class="col-expand"> for the chevron cell'
);
// CSS must pin width somewhere in the .col-expand selector.
var colExpandBlocks = css.match(/\.col-expand\b[^{}]*\{[^}]*\}/g) || [];
var pinned = colExpandBlocks.some(function (b) {
return /max-width:\s*3[26]px/.test(b) && /min-width:\s*3[26]px/.test(b);
});
assert(pinned, 'style.css .col-expand pins min-width AND max-width to ~32px');
// ── 1b. Locked column-priority tiers (operator spec) ─────────────────────
// Tier 1 (always — even on smallest mobile): expand, time, type, details
// Tier 2 (tablet+): path
// Tier 3 (desktop only): hash, observer, rpt
// Region/Size/HB stay at the existing low-priority tiers (already 3-5).
//
// Mapping to priority values (see TableResponsive doc at top of packets.js):
// priority 1 → always visible
// priority 3 → hidden ≤ 1024 (desktop-only)
// priority 5 → hidden ≤ 768 (tablet+ only)
function colPriority(klass) {
var re = new RegExp('<th[^>]*class="' + klass + '"[^>]*data-priority="(\\d+)"');
var m = pktJs.match(re);
return m ? parseInt(m[1], 10) : null;
}
assert(colPriority('col-expand') === 1, 'col-expand is tier-1 priority (always visible)');
assert(colPriority('col-time') === 1, 'col-time is tier-1 priority (always visible)');
assert(colPriority('col-type') === 1, 'col-type is tier-1 priority (always visible)');
assert(colPriority('col-details') === 1, 'col-details is tier-1 priority (always visible)');
assert(colPriority('col-path') === 5, 'col-path is tier-2 (hidden ≤768, tablet+ only)');
assert(colPriority('col-hash') === 3, 'col-hash is tier-3 (desktop only, hidden ≤1024)');
assert(colPriority('col-observer') === 3, 'col-observer is tier-3 (desktop only, hidden ≤1024)');
assert(colPriority('col-rpt') === 3, 'col-rpt is tier-3 (desktop only, hidden ≤1024)');
// ── 2. DETAILS column capped ─────────────────────────────────────────────
var colDetailsBlocks = css.match(/\.col-details\b[^{}]*\{[^}]*\}/g) || [];
var capped = colDetailsBlocks.some(function (b) {
var m = b.match(/max-width:\s*(\d+)px/);
return m && parseInt(m[1], 10) <= 480 && parseInt(m[1], 10) >= 200;
});
assert(capped, 'style.css caps .col-details with max-width ≤ 480px');
// ── 3. Mobile compaction — DETAILS hidden + BYOP hidden under 480 ────────
var mobileBlock = (function () {
var idx = css.indexOf('@media (max-width: 480px)');
if (idx < 0) return '';
var depth = 0, start = -1, end = -1;
for (var i = idx; i < css.length; i++) {
var c = css[i];
if (c === '{') { if (depth === 0) start = i; depth++; }
else if (c === '}') { depth--; if (depth === 0) { end = i; break; } }
}
return start > 0 && end > 0 ? css.slice(start, end + 1) : '';
})();
assert(mobileBlock.length > 0, 'style.css has a @media (max-width: 480px) block');
assert(
/pkt-byop[^{}]*\{[^}]*display:\s*none/.test(mobileBlock),
'mobile @media block hides the BYOP button (chrome compaction)'
);
// Note: per LOCKED spec, col-details is tier-1 and stays visible at mobile.
// It is the col-path / col-hash / col-observer / col-rpt that drop on mobile,
// already enforced via data-priority above (TableResponsive.apply).
// ── 4. renderDetail mobile-priority ordering ────────────────────────────
var dlMatch = pktJs.match(/<dl class="detail-meta">([\s\S]*?)<\/dl>/);
assert(!!dlMatch, 'renderDetail emits <dl class="detail-meta">');
if (dlMatch) {
var dlBody = dlMatch[1];
var idxType = dlBody.indexOf('Payload Type');
var idxObs = dlBody.indexOf('Observer');
assert(idxType >= 0, '.detail-meta still includes Payload Type row');
assert(idxObs >= 0, '.detail-meta still includes Observer row');
assert(
idxType >= 0 && idxObs >= 0 && idxType < idxObs,
'.detail-meta lists Payload Type BEFORE Observer (mobile-priority order)'
);
}
// Wrap hex / breakdown / observations in a collapsible technical section.
assert(
/<details[^>]*class="detail-technical"/.test(pktJs),
'renderDetail wraps technical fields in <details class="detail-technical">'
);
// ── 5. #1458 P0-A — semantic-first detail title ─────────────────────────
// Previously the title hard-coded "Packet Byte Breakdown (N bytes)" when
// raw_hex was present. Must be replaced by a type-badge + summary header.
assert(
!/Packet Byte Breakdown/.test(pktJs),
'renderDetail no longer leads with "Packet Byte Breakdown (N bytes)" title'
);
assert(
/<div class="detail-title">[\s\S]{0,200}badge badge-\$\{payloadTypeColor/.test(pktJs),
'detail-title leads with a type badge (semantic identity first)'
);
assert(
/<div class="detail-srcdst">/.test(pktJs),
'renderDetail emits a .detail-srcdst row (src → dst summary)'
);
// ── 6. #1458 P0-B — raw-bytes disclosure copy ───────────────────────────
assert(
/<summary>Show raw bytes<\/summary>/.test(pktJs),
'detail-technical disclosure summary reads "Show raw bytes" (per spec)'
);
// ── 7. #1458 P0-C — mobile filter-zone collapse ─────────────────────────
assert(
/pkt-filter-expr/.test(pktJs),
'always-on filter input wrapper carries class .pkt-filter-expr'
);
assert(
/\.pkt-filter-expr[^{}]*\{[^}]*display:\s*none/.test(mobileBlock),
'mobile @media (max-width: 480px) hides .pkt-filter-expr by default'
);
assert(
/\.filter-bar\.filters-expanded[^{}]*\.pkt-filter-expr[^{}]*\{[^}]*display:/.test(mobileBlock) ||
/:has\(\.filter-bar\.filters-expanded\)[^{}]*\.pkt-filter-expr[^{}]*\{[^}]*display:/.test(mobileBlock),
'expanded filters reveal .pkt-filter-expr on mobile (Filters ▾ toggle)'
);
// ── Summary ──────────────────────────────────────────────────────────────
console.log('\n' + passed + ' passed, ' + failed + ' failed');
process.exit(failed === 0 ? 0 : 1);