mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-06-28 20:01:55 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 707d70c738 | |||
| b3189c613a |
@@ -157,6 +157,7 @@ jobs:
|
||||
node test-a11y-axe-1668-selftest.js
|
||||
node test-a11y-1716-rf-range-btn-active.js
|
||||
node test-issue-1705-subpath-contrast.js
|
||||
node test-issue-1770-mobile-row-clamp.js
|
||||
node test-a11y-axe-routes-coverage.js
|
||||
|
||||
- name: 🛡️ Preflight XSS gate — actual --diff check (PR only)
|
||||
|
||||
+65
-3
@@ -169,6 +169,18 @@ type Payload struct {
|
||||
CtrlFlags string `json:"ctrlFlags,omitempty"`
|
||||
CtrlZeroHop *bool `json:"ctrlZeroHop,omitempty"`
|
||||
CtrlLength *int `json:"ctrlLength,omitempty"`
|
||||
// CONTROL DISCOVER_REQ / DISCOVER_RESP body fields (#1802). Subtype is
|
||||
// "DISCOVER_REQ" | "DISCOVER_RESP" | "UNKNOWN". For REQ: filter, tag, and
|
||||
// optional since. For RESP: node_type (low nibble of byte0), snr, tag, and
|
||||
// pubkey (hex; 32 bytes or 8 bytes when prefix_only). All optional —
|
||||
// emitted only when the body length is sufficient.
|
||||
CtrlSubtype string `json:"ctrlSubtype,omitempty"`
|
||||
CtrlFilter *int `json:"ctrlFilter,omitempty"`
|
||||
CtrlTag *uint32 `json:"ctrlTag,omitempty"`
|
||||
CtrlSince *uint32 `json:"ctrlSince,omitempty"`
|
||||
CtrlNodeType *int `json:"ctrlNodeType,omitempty"`
|
||||
CtrlSNR *int `json:"ctrlSNR,omitempty"`
|
||||
CtrlPubKey string `json:"ctrlPubKey,omitempty"`
|
||||
// RAW_CUSTOM (PAYLOAD_TYPE_RAW_CUSTOM=0x0F) — application-defined per
|
||||
// firmware/src/Mesh.cpp:577 (createRawData). Exposes the bare envelope
|
||||
// shape (length + leading tag) so consumers can triage by app id.
|
||||
@@ -717,21 +729,71 @@ func decodeMultipart(buf []byte) Payload {
|
||||
return p
|
||||
}
|
||||
|
||||
// decodeControl decodes PAYLOAD_TYPE_CONTROL (0x0B) byte0 flags per
|
||||
// firmware/src/Mesh.cpp:69 (high-bit set ⇒ zero-hop direct subset).
|
||||
// decodeControl decodes PAYLOAD_TYPE_CONTROL (0x0B).
|
||||
//
|
||||
// byte0 high nibble is the control subtype (per firmware/src/Mesh.cpp:69
|
||||
// and firmware/examples/simple_repeater/MyMesh.cpp:773-820):
|
||||
// 0x80 = CTL_TYPE_NODE_DISCOVER_REQ — body: filter:u8 | tag:u32 LE | since:u32 LE (optional)
|
||||
// 0x90 = CTL_TYPE_NODE_DISCOVER_RESP — body: snr:i8 | tag:u32 LE | pubkey (32B full, or 8B prefix)
|
||||
// low nibble of byte0 is node_type
|
||||
//
|
||||
// The legacy CtrlZeroHop bool (bit7 of byte0) is retained for backwards
|
||||
// compatibility — it is misleading (bit7 is also set for DISCOVER_RESP)
|
||||
// and is flagged for follow-up renaming. Length checks gate every field
|
||||
// extraction; short/truncated bodies emit the subtype only and never panic.
|
||||
func decodeControl(buf []byte) Payload {
|
||||
if len(buf) < 1 {
|
||||
return Payload{Type: "CONTROL", Error: "too short", RawHex: hex.EncodeToString(buf)}
|
||||
}
|
||||
zeroHop := buf[0]&0x80 != 0
|
||||
length := len(buf)
|
||||
return Payload{
|
||||
p := Payload{
|
||||
Type: "CONTROL",
|
||||
CtrlFlags: fmt.Sprintf("%02x", buf[0]),
|
||||
CtrlZeroHop: &zeroHop,
|
||||
CtrlLength: &length,
|
||||
RawHex: hex.EncodeToString(buf),
|
||||
}
|
||||
|
||||
switch buf[0] & 0xF0 {
|
||||
case 0x80:
|
||||
p.CtrlSubtype = "DISCOVER_REQ"
|
||||
// REQ body: filter:u8 | tag:u32 LE | since:u32 LE (optional).
|
||||
// Total body (incl. byte0) >= 6 = 1 + 1 + 4.
|
||||
if len(buf) >= 6 {
|
||||
filter := int(buf[1])
|
||||
p.CtrlFilter = &filter
|
||||
tag := binary.LittleEndian.Uint32(buf[2:6])
|
||||
p.CtrlTag = &tag
|
||||
if len(buf) >= 10 {
|
||||
since := binary.LittleEndian.Uint32(buf[6:10])
|
||||
p.CtrlSince = &since
|
||||
}
|
||||
}
|
||||
case 0x90:
|
||||
p.CtrlSubtype = "DISCOVER_RESP"
|
||||
nodeType := int(buf[0] & 0x0F)
|
||||
p.CtrlNodeType = &nodeType
|
||||
// RESP body: snr:i8 | tag:u32 LE | pubkey. Header is 6 bytes
|
||||
// (byte0 + snr + 4B tag). Pubkey is 32B full or 8B prefix.
|
||||
if len(buf) >= 6 {
|
||||
snr := int(int8(buf[1]))
|
||||
p.CtrlSNR = &snr
|
||||
tag := binary.LittleEndian.Uint32(buf[2:6])
|
||||
p.CtrlTag = &tag
|
||||
remaining := len(buf) - 6
|
||||
if remaining >= 32 {
|
||||
p.CtrlPubKey = hex.EncodeToString(buf[6:38])
|
||||
} else if remaining >= 8 && remaining < 32 {
|
||||
p.CtrlPubKey = hex.EncodeToString(buf[6:14])
|
||||
}
|
||||
// Other lengths (e.g. 4B): omit pubkey rather than emit a
|
||||
// partial/ambiguous value.
|
||||
}
|
||||
default:
|
||||
p.CtrlSubtype = "UNKNOWN"
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// decodeRawCustom decodes PAYLOAD_TYPE_RAW_CUSTOM (0x0F). Application-defined
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
package main
|
||||
|
||||
// Tests for #1802 — CONTROL DISCOVER_REQ / DISCOVER_RESP decode.
|
||||
// Wire format references:
|
||||
// firmware/src/Mesh.cpp:69 (CTL_TYPE constants)
|
||||
// firmware/examples/simple_repeater/MyMesh.cpp:773-820
|
||||
//
|
||||
// Subtype = byte0 & 0xF0. 0x80 = DISCOVER_REQ, 0x90 = DISCOVER_RESP.
|
||||
// REQ body (>=6B after byte0): filter:u8 | tag:u32 LE | since:u32 LE (optional)
|
||||
// RESP body (>=6+pubkey): snr:i8 | tag:u32 LE | pubkey (32B full, or 8B prefix)
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDecodeControl_DiscoverReq_FullBody(t *testing.T) {
|
||||
// byte0 = 0x80 (DISCOVER_REQ), filter=0x02, tag=0xDEADBEEF, since=0x11223344.
|
||||
buf := []byte{0x80, 0x02, 0xEF, 0xBE, 0xAD, 0xDE, 0x44, 0x33, 0x22, 0x11}
|
||||
p := decodeControl(buf)
|
||||
if p.Type != "CONTROL" {
|
||||
t.Fatalf("type=%q want CONTROL", p.Type)
|
||||
}
|
||||
if p.CtrlSubtype != "DISCOVER_REQ" {
|
||||
t.Errorf("ctrlSubtype=%q want DISCOVER_REQ", p.CtrlSubtype)
|
||||
}
|
||||
if p.CtrlFilter == nil || *p.CtrlFilter != 0x02 {
|
||||
t.Errorf("ctrlFilter=%v want 2", p.CtrlFilter)
|
||||
}
|
||||
if p.CtrlTag == nil || *p.CtrlTag != 0xDEADBEEF {
|
||||
t.Errorf("ctrlTag=%v want 0xDEADBEEF", p.CtrlTag)
|
||||
}
|
||||
if p.CtrlSince == nil || *p.CtrlSince != 0x11223344 {
|
||||
t.Errorf("ctrlSince=%v want 0x11223344", p.CtrlSince)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeControl_DiscoverReq_NoSince(t *testing.T) {
|
||||
// 6-byte body (no optional since): byte0=0x80, filter, tag.
|
||||
buf := []byte{0x80, 0x04, 0x01, 0x02, 0x03, 0x04}
|
||||
p := decodeControl(buf)
|
||||
if p.CtrlSubtype != "DISCOVER_REQ" {
|
||||
t.Errorf("ctrlSubtype=%q want DISCOVER_REQ", p.CtrlSubtype)
|
||||
}
|
||||
if p.CtrlFilter == nil || *p.CtrlFilter != 0x04 {
|
||||
t.Errorf("ctrlFilter=%v want 4", p.CtrlFilter)
|
||||
}
|
||||
wantTag := binary.LittleEndian.Uint32([]byte{0x01, 0x02, 0x03, 0x04})
|
||||
if p.CtrlTag == nil || *p.CtrlTag != wantTag {
|
||||
t.Errorf("ctrlTag=%v want %#x", p.CtrlTag, wantTag)
|
||||
}
|
||||
if p.CtrlSince != nil {
|
||||
t.Errorf("ctrlSince should be nil for 6B REQ body, got %v", p.CtrlSince)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeControl_DiscoverResp_FullPubKey(t *testing.T) {
|
||||
// byte0 = 0x90 | 0x02 (node_type=2, REPEATER), snr=0x10, tag, 32B pubkey.
|
||||
body := []byte{0x92, 0x10, 0x44, 0x33, 0x22, 0x11}
|
||||
for i := 0; i < 32; i++ {
|
||||
body = append(body, byte(i))
|
||||
}
|
||||
p := decodeControl(body)
|
||||
if p.CtrlSubtype != "DISCOVER_RESP" {
|
||||
t.Errorf("ctrlSubtype=%q want DISCOVER_RESP", p.CtrlSubtype)
|
||||
}
|
||||
if p.CtrlNodeType == nil || *p.CtrlNodeType != 2 {
|
||||
t.Errorf("ctrlNodeType=%v want 2", p.CtrlNodeType)
|
||||
}
|
||||
if p.CtrlSNR == nil || *p.CtrlSNR != 0x10 {
|
||||
t.Errorf("ctrlSNR=%v want 16", p.CtrlSNR)
|
||||
}
|
||||
wantTag := binary.LittleEndian.Uint32([]byte{0x44, 0x33, 0x22, 0x11})
|
||||
if p.CtrlTag == nil || *p.CtrlTag != wantTag {
|
||||
t.Errorf("ctrlTag=%v want %#x", p.CtrlTag, wantTag)
|
||||
}
|
||||
if len(p.CtrlPubKey) != 64 {
|
||||
t.Fatalf("ctrlPubKey hex len=%d want 64 (32B)", len(p.CtrlPubKey))
|
||||
}
|
||||
if p.CtrlPubKey[:4] != "0001" {
|
||||
t.Errorf("ctrlPubKey prefix=%q want 0001…", p.CtrlPubKey[:4])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeControl_DiscoverResp_PrefixPubKey(t *testing.T) {
|
||||
// 6 header bytes + 8 pubkey bytes only (prefix_only path).
|
||||
body := []byte{0x91, 0xF0, 0xAA, 0xBB, 0xCC, 0xDD, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88}
|
||||
p := decodeControl(body)
|
||||
if p.CtrlSubtype != "DISCOVER_RESP" {
|
||||
t.Errorf("ctrlSubtype=%q want DISCOVER_RESP", p.CtrlSubtype)
|
||||
}
|
||||
if p.CtrlNodeType == nil || *p.CtrlNodeType != 1 {
|
||||
t.Errorf("ctrlNodeType=%v want 1", p.CtrlNodeType)
|
||||
}
|
||||
// snr = signed 0xF0 = -16
|
||||
if p.CtrlSNR == nil || *p.CtrlSNR != -16 {
|
||||
t.Errorf("ctrlSNR=%v want -16", p.CtrlSNR)
|
||||
}
|
||||
if len(p.CtrlPubKey) != 16 {
|
||||
t.Errorf("ctrlPubKey hex len=%d want 16 (8B)", len(p.CtrlPubKey))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeControl_DiscoverResp_TruncatedPubKey(t *testing.T) {
|
||||
// 6 header bytes + 4 pubkey bytes — neither 8 nor 32. Must NOT panic;
|
||||
// subtype emitted, pubkey omitted.
|
||||
body := []byte{0x91, 0x05, 0xAA, 0xBB, 0xCC, 0xDD, 0x11, 0x22, 0x33, 0x44}
|
||||
p := decodeControl(body)
|
||||
if p.CtrlSubtype != "DISCOVER_RESP" {
|
||||
t.Errorf("ctrlSubtype=%q want DISCOVER_RESP", p.CtrlSubtype)
|
||||
}
|
||||
if p.CtrlPubKey != "" {
|
||||
t.Errorf("ctrlPubKey=%q want empty for 4B pubkey blob", p.CtrlPubKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeControl_ShortBody_NoSubtypeFields(t *testing.T) {
|
||||
// byte0=0x80 only (no body). Must emit subtype, no panic, no body fields.
|
||||
buf := []byte{0x80}
|
||||
p := decodeControl(buf)
|
||||
if p.CtrlSubtype != "DISCOVER_REQ" {
|
||||
t.Errorf("ctrlSubtype=%q want DISCOVER_REQ", p.CtrlSubtype)
|
||||
}
|
||||
if p.CtrlFilter != nil {
|
||||
t.Errorf("ctrlFilter should be nil, got %v", p.CtrlFilter)
|
||||
}
|
||||
if p.CtrlTag != nil {
|
||||
t.Errorf("ctrlTag should be nil, got %v", p.CtrlTag)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeControl_UnknownSubtype(t *testing.T) {
|
||||
// byte0 = 0xA0 — not DISCOVER_REQ/RESP. Subtype = "UNKNOWN".
|
||||
buf := []byte{0xA0, 0x11, 0x22}
|
||||
p := decodeControl(buf)
|
||||
if p.CtrlSubtype != "UNKNOWN" {
|
||||
t.Errorf("ctrlSubtype=%q want UNKNOWN", p.CtrlSubtype)
|
||||
}
|
||||
}
|
||||
+42
-3
@@ -2252,7 +2252,7 @@
|
||||
<td class="col-observer" data-filter-field="observer" data-filter-value="${escapeHtml(obsNameOnly(headerObserverId) || '')}">${isSingle ? escapeHtml(truncate(obsNameOnly(headerObserverId), 16)) + obsIataBadge(p) : escapeHtml(truncate(obsNameOnly(headerObserverId), 10)) + groupedObserverIataBadgesHtml(p)}</td>
|
||||
<td class="col-path"><span class="path-hops">${groupPathStr}</span></td>
|
||||
<td class="col-rpt">${p.observation_count > 1 ? '<span class="badge badge-obs" title="Seen ' + p.observation_count + ' times"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-eye"/></svg> ' + p.observation_count + '</span>' : (isSingle ? '' : p.count)}</td>
|
||||
<td class="col-details">${getDetailPreview(getParsedDecoded(p))}</td>
|
||||
<td class="col-details"><span class="col-details-clip">${getDetailPreview(getParsedDecoded(p))}</span></td>
|
||||
</tr>`;
|
||||
if (isExpanded && p._children) {
|
||||
let visibleChildren = p._children;
|
||||
@@ -2281,7 +2281,7 @@
|
||||
<td class="col-observer" data-filter-field="observer" data-filter-value="${escapeHtml(obsNameOnly(c.observer_id) || '')}">${escapeHtml(truncate(obsNameOnly(c.observer_id), 16))}${obsIataBadge(c)}</td>
|
||||
<td class="col-path"><span class="path-hops">${childPathStr}</span></td>
|
||||
<td class="col-rpt"></td>
|
||||
<td class="col-details">${getDetailPreview(getParsedDecoded(c))}</td>
|
||||
<td class="col-details"><span class="col-details-clip">${getDetailPreview(getParsedDecoded(c))}</span></td>
|
||||
</tr>`;
|
||||
}
|
||||
}
|
||||
@@ -2314,7 +2314,7 @@
|
||||
<td class="col-observer" data-filter-field="observer" data-filter-value="${escapeHtml(obsNameOnly(p.observer_id) || '')}">${escapeHtml(truncate(obsNameOnly(p.observer_id), 16))}${obsIataBadge(p)}</td>
|
||||
<td class="col-path"><span class="path-hops">${pathStr}</span></td>
|
||||
<td class="col-rpt"></td>
|
||||
<td class="col-details">${detail}</td>
|
||||
<td class="col-details"><span class="col-details-clip">${detail}</span></td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
@@ -2874,6 +2874,45 @@
|
||||
if (decoded.type === 'REQ' || decoded.type === 'RESPONSE') return `<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-lock"/></svg> ${decoded.srcHash?.slice(0,8) || '?'} → ${decoded.destHash?.slice(0,8) || '?'}`;
|
||||
// Anonymous requests
|
||||
if (decoded.type === 'ANON_REQ') return `<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-lock"/></svg> anon → ${decoded.destHash?.slice(0,8) || '?'}`;
|
||||
// CONTROL packets (#1802) — DISCOVER_REQ / DISCOVER_RESP body fields,
|
||||
// decoded by cmd/ingestor/decoder.go decodeControl(). Wire format:
|
||||
// firmware/src/Mesh.cpp:69
|
||||
// firmware/examples/simple_repeater/MyMesh.cpp:773-820
|
||||
// Subtype enum on byte0 high nibble; body fields are length-gated and
|
||||
// may be absent on truncated packets, so each is rendered only when set.
|
||||
if (decoded.type === 'CONTROL') {
|
||||
const subtype = decoded.ctrlSubtype || 'CONTROL';
|
||||
const icon = `<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-broadcast"/></svg>`;
|
||||
const parts = [];
|
||||
if (subtype === 'DISCOVER_REQ') {
|
||||
if (decoded.ctrlFilter != null) {
|
||||
parts.push(`filter=0x${Number(decoded.ctrlFilter).toString(16).padStart(2, '0')}`);
|
||||
}
|
||||
if (decoded.ctrlTag != null) {
|
||||
parts.push(`tag=0x${(Number(decoded.ctrlTag) >>> 0).toString(16).padStart(8, '0')}`);
|
||||
}
|
||||
if (decoded.ctrlSince != null) {
|
||||
parts.push(`since=${Number(decoded.ctrlSince) >>> 0}`);
|
||||
}
|
||||
} else if (subtype === 'DISCOVER_RESP') {
|
||||
if (decoded.ctrlNodeType != null) {
|
||||
parts.push(`type=${Number(decoded.ctrlNodeType)}`);
|
||||
}
|
||||
if (decoded.ctrlSNR != null) {
|
||||
parts.push(`snr=${Number(decoded.ctrlSNR)}`);
|
||||
}
|
||||
if (decoded.ctrlTag != null) {
|
||||
parts.push(`tag=0x${(Number(decoded.ctrlTag) >>> 0).toString(16).padStart(8, '0')}`);
|
||||
}
|
||||
if (decoded.ctrlPubKey) {
|
||||
parts.push(`pubkey=${escapeHtml(decoded.ctrlPubKey)}`);
|
||||
}
|
||||
} else if (decoded.ctrlFlags) {
|
||||
parts.push(`flags=0x${escapeHtml(decoded.ctrlFlags)}`);
|
||||
}
|
||||
const tail = parts.length ? ` <span class="muted">${parts.join(' ')}</span>` : '';
|
||||
return `${icon} ${escapeHtml(subtype)}${tail}`;
|
||||
}
|
||||
// Companion bridge text
|
||||
if (decoded.text) return escapeHtml(decoded.text.length > 80 ? decoded.text.slice(0, 80) + '…' : decoded.text);
|
||||
// Bare adverts with just pubkey
|
||||
|
||||
@@ -2433,6 +2433,32 @@ button.ch-item:hover .ch-icon-btn { opacity: 1; }
|
||||
.data-table .col-time { min-width: 64px; }
|
||||
.panel-left { overflow-x: auto; }
|
||||
|
||||
/* #1770: packets-view virtual-scroller assumes constant row height
|
||||
(VSCROLL_ROW_HEIGHT). The base rule at ~L1097 sets
|
||||
`.data-table td.col-details { white-space: normal; word-break: break-word }`
|
||||
which lets the "Details" cell wrap on narrow viewports, producing
|
||||
variable row heights → visible jitter when scrolling past ~900px on
|
||||
iOS. Quick-fix (S path): clamp the cell to a single line on mobile.
|
||||
The proper L-path fix (per-row measurement) tracks separately.
|
||||
|
||||
v2 (#1805 follow-up): the clamp lives on an INNER `.col-details-clip`
|
||||
wrapper, not the `<td>` itself. Reason: with `white-space: nowrap` on
|
||||
the td and `table-layout: auto`, the cell's min-content becomes the
|
||||
full single-line text width, blowing past the `max-width: 100px`
|
||||
mobile hint. Rows then grow wider than the 360px viewport — which
|
||||
breaks `test-gestures-1062-e2e.js` (a)/(h) because the swipe coords
|
||||
fall outside the viewport and `elementFromPoint` no longer hits the
|
||||
row. Inline-block + fixed `max-width` on the clip caps the column's
|
||||
preferred width so the row stays viewport-bounded and gestures work. */
|
||||
.data-table td.col-details > .col-details-clip {
|
||||
display: inline-block;
|
||||
max-width: 140px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
/* Filters: collapse on mobile */
|
||||
.filter-bar { flex-direction: row; flex-wrap: wrap; gap: 4px; }
|
||||
.filter-toggle-btn { display: inline-flex !important; }
|
||||
|
||||
@@ -63,6 +63,7 @@ node test-issue-1532-live-fullscreen.js
|
||||
node test-naive-banner-tone.js
|
||||
node test-issue-1473-reserved-prefixes.js
|
||||
node test-issue-1473-prefix-generator.js
|
||||
node test-issue-1770-mobile-row-clamp.js
|
||||
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════"
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* #1770 — Packets view jitters on mobile because variable row heights
|
||||
* break the virtual-scroller's constant-row-height assumption.
|
||||
*
|
||||
* S quick-fix path: under the mobile breakpoint, clamp .col-details
|
||||
* (the packet "Details" cell in the packets table) to a single line via
|
||||
* white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
* The base rule at line ~1097 currently sets `white-space: normal` which
|
||||
* allows wrapping → variable row heights → virtual-scroll jitter.
|
||||
*
|
||||
* This is a CSS-grep test: it parses public/style.css, locates the
|
||||
* `@media (max-width: 640px)` block used by the packets view on mobile,
|
||||
* and asserts that block declares a `.col-details` clamp rule with
|
||||
* `white-space: nowrap`. Cheap to run, no browser required.
|
||||
*
|
||||
* Pattern borrowed from test-issue-1364-pill-no-clamp.js.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
let passed = 0, failed = 0;
|
||||
function assert(cond, msg) {
|
||||
if (cond) { passed++; console.log(' ✓ ' + msg); }
|
||||
else { failed++; console.error(' ✗ ' + msg); }
|
||||
}
|
||||
|
||||
const cssSrc = fs.readFileSync(path.join(__dirname, 'public', 'style.css'), 'utf8');
|
||||
|
||||
// Pull the existing mobile breakpoint block. The packets-table mobile
|
||||
// overrides (.data-table font-size, .data-table td max-width, etc.) live
|
||||
// inside `@media (max-width: 640px)` at ~line 2361.
|
||||
// Pull every `@media (max-width: 640px)` block. There are several in
|
||||
// style.css (the packets-view block is at ~L2362 but many feature
|
||||
// sections add their own); the clamp can live in any of them.
|
||||
function extractAllMediaBlocks(src, header) {
|
||||
const out = [];
|
||||
let from = 0;
|
||||
while (true) {
|
||||
const idx = src.indexOf(header, from);
|
||||
if (idx === -1) break;
|
||||
let depth = 0, started = false, end = -1;
|
||||
for (let i = idx; i < src.length; i++) {
|
||||
const c = src[i];
|
||||
if (c === '{') { depth++; started = true; }
|
||||
else if (c === '}') {
|
||||
depth--;
|
||||
if (started && depth === 0) { end = i + 1; break; }
|
||||
}
|
||||
}
|
||||
if (end === -1) break;
|
||||
out.push(src.slice(idx, end));
|
||||
from = end;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const mobileBlocks = extractAllMediaBlocks(cssSrc, '@media (max-width: 640px)');
|
||||
|
||||
console.log('\n=== #1770: .col-details clamps to one line under mobile breakpoint ===');
|
||||
|
||||
assert(mobileBlocks.length > 0, '`@media (max-width: 640px)` block(s) found in style.css');
|
||||
|
||||
if (mobileBlocks.length > 0) {
|
||||
// Find the .col-details rule body inside any mobile block. Accepts any
|
||||
// selector that includes `.col-details` (e.g. `.col-details { ... }` or
|
||||
// `.data-table td.col-details { ... }` or — post-#1805 — an inner
|
||||
// `.col-details > .col-details-clip` wrapper). The clamp invariant is
|
||||
// what matters; the selector scope was tightened in #1805 to avoid
|
||||
// blowing past `max-width` and breaking row-action gestures (#1062).
|
||||
const ruleRe = /([^{}]*\.col-details\b[^{}]*)\{([^{}]*)\}/g;
|
||||
let clampBody = null;
|
||||
for (const block of mobileBlocks) {
|
||||
let m;
|
||||
ruleRe.lastIndex = 0;
|
||||
while ((m = ruleRe.exec(block)) !== null) {
|
||||
if (/white-space\s*:\s*nowrap/.test(m[2])) { clampBody = m[2]; break; }
|
||||
}
|
||||
if (clampBody) break;
|
||||
}
|
||||
|
||||
assert(
|
||||
!!clampBody,
|
||||
'.col-details mobile rule declares `white-space: nowrap` (single-line clamp)'
|
||||
);
|
||||
|
||||
if (clampBody) {
|
||||
assert(
|
||||
/overflow\s*:\s*hidden/.test(clampBody),
|
||||
'.col-details mobile rule declares `overflow: hidden`'
|
||||
);
|
||||
assert(
|
||||
/text-overflow\s*:\s*ellipsis/.test(clampBody),
|
||||
'.col-details mobile rule declares `text-overflow: ellipsis`'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n=== Summary ===');
|
||||
console.log(' Passed: ' + passed);
|
||||
console.log(' Failed: ' + failed);
|
||||
console.log('\n#1770 ' + (failed === 0 ? 'PASS' : 'FAIL'));
|
||||
process.exit(failed === 0 ? 0 : 1);
|
||||
@@ -499,6 +499,46 @@ console.log('\n=== packets.js: getDetailPreview ===');
|
||||
test('getDetailPreview returns empty for empty decoded', () => {
|
||||
assert.strictEqual(api.getDetailPreview({}), '');
|
||||
});
|
||||
|
||||
// #1802 — CONTROL DISCOVER_REQ / DISCOVER_RESP should be rendered (not just
|
||||
// hex). Backend cmd/ingestor/decoder.go decodeControl() emits ctrlSubtype +
|
||||
// body fields; the detail preview must surface them.
|
||||
test('getDetailPreview handles CONTROL DISCOVER_REQ', () => {
|
||||
const result = api.getDetailPreview({
|
||||
type: 'CONTROL',
|
||||
ctrlSubtype: 'DISCOVER_REQ',
|
||||
ctrlFilter: 2,
|
||||
ctrlTag: 0xDEADBEEF,
|
||||
ctrlSince: 0x11223344,
|
||||
});
|
||||
assert(result.includes('DISCOVER_REQ'), 'should label subtype');
|
||||
assert(result.includes('filter'), 'should render filter field');
|
||||
assert(result.includes('tag'), 'should render tag field');
|
||||
});
|
||||
|
||||
test('getDetailPreview handles CONTROL DISCOVER_RESP', () => {
|
||||
const result = api.getDetailPreview({
|
||||
type: 'CONTROL',
|
||||
ctrlSubtype: 'DISCOVER_RESP',
|
||||
ctrlNodeType: 2,
|
||||
ctrlSNR: 16,
|
||||
ctrlTag: 0x11223344,
|
||||
ctrlPubKey: '0001020304050607',
|
||||
});
|
||||
assert(result.includes('DISCOVER_RESP'), 'should label subtype');
|
||||
assert(result.includes('snr') || result.includes('SNR'), 'should render snr');
|
||||
assert(result.includes('0001020304050607'), 'should render pubkey hex');
|
||||
});
|
||||
|
||||
test('getDetailPreview handles CONTROL UNKNOWN subtype', () => {
|
||||
const result = api.getDetailPreview({
|
||||
type: 'CONTROL',
|
||||
ctrlSubtype: 'UNKNOWN',
|
||||
ctrlFlags: 'a0',
|
||||
});
|
||||
assert(result.includes('UNKNOWN') || result.includes('CONTROL'),
|
||||
'should at least label the unknown subtype');
|
||||
});
|
||||
}
|
||||
|
||||
console.log('\n=== packets.js: getPathHopCount ===');
|
||||
|
||||
Reference in New Issue
Block a user