/** * #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 / 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 `
` of `.detail-meta` (operator's "lead with * packet type"), and wraps the byte-breakdown / hex-dump / field-table * into a `
` 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( /]*class="col-expand"/.test(pktJs), 'packets.js header has on the first column' ); assert( / 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(']*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(/
([\s\S]*?)<\/dl>/); assert(!!dlMatch, 'renderDetail emits
'); 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( /]*class="detail-technical"/.test(pktJs), 'renderDetail wraps technical fields in
' ); // ── 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( /
[\s\S]{0,200}badge badge-\$\{payloadTypeColor/.test(pktJs), 'detail-title leads with a type badge (semantic identity first)' ); assert( /
/.test(pktJs), 'renderDetail emits a .detail-srcdst row (src → dst summary)' ); // ── 6. #1458 P0-B — raw-bytes disclosure copy ─────────────────────────── assert( /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);