mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-07-02 17:53:04 +00:00
Draft for milestone 4 of #1648 — emoji → Phosphor Icons (map & route overlays). Currently at the red commit (failing test only). Implementation follows. Partial fix for #1648 (M4 of 6). Do NOT close the tracking issue. --------- Co-authored-by: bot <bot@corescope>
This commit is contained in:
@@ -139,6 +139,7 @@ jobs:
|
||||
node test-xss-escape-sinks.js
|
||||
node test-preflight-xss-gate.js
|
||||
node test-traces.js
|
||||
node test-issue-1648-m4-emoji-scan.js
|
||||
|
||||
- name: 🛡️ Preflight XSS gate — actual --diff check (PR only)
|
||||
# The fixture self-test above (test-preflight-xss-gate.js) only
|
||||
@@ -401,6 +402,7 @@ jobs:
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1648-m1-icons-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1648-m2-icons-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1648-m3-icons-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-issue-1648-m4-icons-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
BASE_URL=http://localhost:13581 node test-issue-1224-channels-mobile-ux-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
BASE_URL=http://localhost:13581 node test-issue-1367-channels-chat-app-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
BASE_URL=http://localhost:13581 node test-issue-1236-map-mobile-e2e.js 2>&1 | tee -a e2e-output.txt
|
||||
|
||||
+1
-1
@@ -3026,7 +3026,7 @@ function destroy() { _stopRolesRefresh(); _stopScopesRefresh(); _analyticsData =
|
||||
el.innerHTML = `
|
||||
<div class="analytics-card" id="ptOverview">
|
||||
<div style="display:flex;align-items:center;gap:8px;cursor:pointer;user-select:none" id="ptOverviewToggle">
|
||||
<span id="ptOverviewChevron" style="font-size:0.75em;color:var(--text-muted);transition:transform 0.2s">▶</span>
|
||||
<span id="ptOverviewChevron" class="pt-overview-chevron" style="color:var(--text-muted);transition:transform 0.2s;display:inline-flex;align-items:center"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-caret-right"/></svg></span>
|
||||
<h3 style="margin:0">Network Overview</h3>
|
||||
</div>
|
||||
<div id="ptOverviewBody" style="display:none">
|
||||
|
||||
@@ -60,6 +60,8 @@
|
||||
#map { flex: 1; }
|
||||
.node-popup { font-size: 0.8rem; line-height: 1.5; }
|
||||
.node-popup strong { color: #1a6abf; }
|
||||
/* Phosphor icon helper (mirrors style.css .ph-icon — area-map.html is standalone) */
|
||||
.ph-icon { width: 1em; height: 1em; fill: currentColor; vertical-align: -0.125em; display: inline-block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -98,8 +100,8 @@
|
||||
</div>
|
||||
<div id="builder-controls">
|
||||
<button id="btn-draw">Draw</button>
|
||||
<button id="btn-undo">↩ Undo</button>
|
||||
<button id="btn-clear-draw">✕ Clear</button>
|
||||
<button id="btn-undo"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-arrow-u-up-left"/></svg> Undo</button>
|
||||
<button id="btn-clear-draw"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-x"/></svg> Clear</button>
|
||||
</div>
|
||||
<div id="draw-hint">0 points — need ≥ 3</div>
|
||||
<div id="builder-output" class="empty">Fill key + label, then draw polygon points…</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">
|
||||
<symbol id="ph-arrow-clockwise" viewBox="0 0 256 256"><path d="M240,56v48a8,8,0,0,1-8,8H184a8,8,0,0,1,0-16H211.4L184.81,71.64l-.25-.24a80,80,0,1,0-1.67,114.78,8,8,0,0,1,11,11.63A95.44,95.44,0,0,1,128,224h-1.32A96,96,0,1,1,195.75,60L224,85.8V56a8,8,0,1,1,16,0Z"/></symbol>
|
||||
<symbol id="ph-arrow-u-up-left" viewBox="0 0 256 256"><path d="M232,144a64.07,64.07,0,0,1-64,64H80a8,8,0,0,1,0-16h88a48,48,0,0,0,0-96H51.31l34.35,34.34a8,8,0,0,1-11.32,11.32l-48-48a8,8,0,0,1,0-11.32l48-48A8,8,0,0,1,85.66,45.66L51.31,80H168A64.07,64.07,0,0,1,232,144Z"/></symbol>
|
||||
<symbol id="ph-arrows-out" viewBox="0 0 256 256"><path d="M216,48V96a8,8,0,0,1-16,0V67.31l-42.34,42.35a8,8,0,0,1-11.32-11.32L188.69,56H160a8,8,0,0,1,0-16h48A8,8,0,0,1,216,48ZM98.34,146.34,56,188.69V160a8,8,0,0,0-16,0v48a8,8,0,0,0,8,8H96a8,8,0,0,0,0-16H67.31l42.35-42.34a8,8,0,0,0-11.32-11.32ZM208,152a8,8,0,0,0-8,8v28.69l-42.34-42.35a8,8,0,0,0-11.32,11.32L188.69,200H160a8,8,0,0,0,0,16h48a8,8,0,0,0,8-8V160A8,8,0,0,0,208,152ZM67.31,56H96a8,8,0,0,0,0-16H48a8,8,0,0,0-8,8V96a8,8,0,0,0,16,0V67.31l42.34,42.35a8,8,0,0,0,11.32-11.32Z"/></symbol>
|
||||
<symbol id="ph-battery-high" viewBox="0 0 256 256"><path d="M200,56H32A24,24,0,0,0,8,80v96a24,24,0,0,0,24,24H200a24,24,0,0,0,24-24V80A24,24,0,0,0,200,56Zm8,120a8,8,0,0,1-8,8H32a8,8,0,0,1-8-8V80a8,8,0,0,1,8-8H200a8,8,0,0,1,8,8ZM144,96v64a8,8,0,0,1-16,0V96a8,8,0,0,1,16,0Zm-40,0v64a8,8,0,0,1-16,0V96a8,8,0,0,1,16,0ZM64,96v64a8,8,0,0,1-16,0V96a8,8,0,0,1,16,0Zm192,0v64a8,8,0,0,1-16,0V96a8,8,0,0,1,16,0Z"/></symbol>
|
||||
<symbol id="ph-battery-low" viewBox="0 0 256 256"><path d="M200,56H32A24,24,0,0,0,8,80v96a24,24,0,0,0,24,24H200a24,24,0,0,0,24-24V80A24,24,0,0,0,200,56Zm8,120a8,8,0,0,1-8,8H32a8,8,0,0,1-8-8V80a8,8,0,0,1,8-8H200a8,8,0,0,1,8,8ZM64,96v64a8,8,0,0,1-16,0V96a8,8,0,0,1,16,0Zm192,0v64a8,8,0,0,1-16,0V96a8,8,0,0,1,16,0Z"/></symbol>
|
||||
@@ -9,6 +10,8 @@
|
||||
<symbol id="ph-broadcast" viewBox="0 0 256 256"><path d="M128,88a40,40,0,1,0,40,40A40,40,0,0,0,128,88Zm0,64a24,24,0,1,1,24-24A24,24,0,0,1,128,152Zm73.71,7.14a80,80,0,0,1-14.08,22.2,8,8,0,0,1-11.92-10.67,63.95,63.95,0,0,0,0-85.33,8,8,0,1,1,11.92-10.67,80.08,80.08,0,0,1,14.08,84.47ZM69,103.09a64,64,0,0,0,11.26,67.58,8,8,0,0,1-11.92,10.67,79.93,79.93,0,0,1,0-106.67A8,8,0,1,1,80.29,85.34,63.77,63.77,0,0,0,69,103.09ZM248,128a119.58,119.58,0,0,1-34.29,84,8,8,0,1,1-11.42-11.2,103.9,103.9,0,0,0,0-145.56A8,8,0,1,1,213.71,44,119.58,119.58,0,0,1,248,128ZM53.71,200.78A8,8,0,1,1,42.29,212a119.87,119.87,0,0,1,0-168,8,8,0,1,1,11.42,11.2,103.9,103.9,0,0,0,0,145.56Z"/></symbol>
|
||||
<symbol id="ph-buildings" viewBox="0 0 256 256"><path d="M240,208H224V96a16,16,0,0,0-16-16H144V32a16,16,0,0,0-24.88-13.32L39.12,72A16,16,0,0,0,32,85.34V208H16a8,8,0,0,0,0,16H240a8,8,0,0,0,0-16ZM208,96V208H144V96ZM48,85.34,128,32V208H48ZM112,112v16a8,8,0,0,1-16,0V112a8,8,0,1,1,16,0Zm-32,0v16a8,8,0,0,1-16,0V112a8,8,0,1,1,16,0Zm0,56v16a8,8,0,0,1-16,0V168a8,8,0,0,1,16,0Zm32,0v16a8,8,0,0,1-16,0V168a8,8,0,0,1,16,0Z"/></symbol>
|
||||
<symbol id="ph-caret-down" viewBox="0 0 256 256"><path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,53.66,90.34L128,164.69l74.34-74.35a8,8,0,0,1,11.32,11.32Z"/></symbol>
|
||||
<symbol id="ph-caret-left" viewBox="0 0 256 256"><path d="M165.66,202.34a8,8,0,0,1-11.32,11.32l-80-80a8,8,0,0,1,0-11.32l80-80a8,8,0,0,1,11.32,11.32L91.31,128Z"/></symbol>
|
||||
<symbol id="ph-caret-right" viewBox="0 0 256 256"><path d="M181.66,133.66l-80,80a8,8,0,0,1-11.32-11.32L164.69,128,90.34,53.66a8,8,0,0,1,11.32-11.32l80,80A8,8,0,0,1,181.66,133.66Z"/></symbol>
|
||||
<symbol id="ph-caret-up" viewBox="0 0 256 256"><path d="M213.66,165.66a8,8,0,0,1-11.32,0L128,91.31,53.66,165.66a8,8,0,0,1-11.32-11.32l80-80a8,8,0,0,1,11.32,0l80,80A8,8,0,0,1,213.66,165.66Z"/></symbol>
|
||||
<symbol id="ph-camera" viewBox="0 0 256 256"><path d="M208,56H180.28L166.65,35.56A8,8,0,0,0,160,32H96a8,8,0,0,0-6.65,3.56L75.71,56H48A24,24,0,0,0,24,80V192a24,24,0,0,0,24,24H208a24,24,0,0,0,24-24V80A24,24,0,0,0,208,56Zm8,136a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V80a8,8,0,0,1,8-8H80a8,8,0,0,0,6.66-3.56L100.28,48h55.43l13.63,20.44A8,8,0,0,0,176,72h32a8,8,0,0,1,8,8ZM128,88a44,44,0,1,0,44,44A44.05,44.05,0,0,0,128,88Zm0,72a28,28,0,1,1,28-28A28,28,0,0,1,128,160Z"/></symbol>
|
||||
<symbol id="ph-cell-signal-high" viewBox="0 0 256 256"><path d="M168,72V200a8,8,0,0,1-16,0V72a8,8,0,0,1,16,0Zm-48,32a8,8,0,0,0-8,8v88a8,8,0,0,0,16,0V112A8,8,0,0,0,120,104ZM80,144a8,8,0,0,0-8,8v48a8,8,0,0,0,16,0V152A8,8,0,0,0,80,144ZM40,184a8,8,0,0,0-8,8v8a8,8,0,0,0,16,0v-8A8,8,0,0,0,40,184Z"/></symbol>
|
||||
@@ -30,6 +33,7 @@
|
||||
<symbol id="ph-envelope-simple" viewBox="0 0 256 256"><path d="M224,48H32a8,8,0,0,0-8,8V192a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A8,8,0,0,0,224,48ZM203.43,64,128,133.15,52.57,64ZM216,192H40V74.19l82.59,75.71a8,8,0,0,0,10.82,0L216,74.19V192Z"/></symbol>
|
||||
<symbol id="ph-eye" viewBox="0 0 256 256"><path d="M247.31,124.76c-.35-.79-8.82-19.58-27.65-38.41C194.57,61.26,162.88,48,128,48S61.43,61.26,36.34,86.35C17.51,105.18,9,124,8.69,124.76a8,8,0,0,0,0,6.5c.35.79,8.82,19.57,27.65,38.4C61.43,194.74,93.12,208,128,208s66.57-13.26,91.66-38.34c18.83-18.83,27.3-37.61,27.65-38.4A8,8,0,0,0,247.31,124.76ZM128,192c-30.78,0-57.67-11.19-79.93-33.25A133.47,133.47,0,0,1,25,128,133.33,133.33,0,0,1,48.07,97.25C70.33,75.19,97.22,64,128,64s57.67,11.19,79.93,33.25A133.46,133.46,0,0,1,231.05,128C223.84,141.46,192.43,192,128,192Zm0-112a48,48,0,1,0,48,48A48.05,48.05,0,0,0,128,80Zm0,80a32,32,0,1,1,32-32A32,32,0,0,1,128,160Z"/></symbol>
|
||||
<symbol id="ph-flame" viewBox="0 0 256 256"><path d="M173.79,51.48a221.25,221.25,0,0,0-41.67-34.34,8,8,0,0,0-8.24,0A221.25,221.25,0,0,0,82.21,51.48C54.59,80.48,40,112.47,40,144a88,88,0,0,0,176,0C216,112.47,201.41,80.48,173.79,51.48ZM96,184c0-27.67,22.53-47.28,32-54.3,9.48,7,32,26.63,32,54.3a32,32,0,0,1-64,0Zm77.27,15.93A47.8,47.8,0,0,0,176,184c0-44-42.09-69.79-43.88-70.86a8,8,0,0,0-8.24,0C122.09,114.21,80,140,80,184a47.8,47.8,0,0,0,2.73,15.93A71.88,71.88,0,0,1,56,144c0-34.41,20.4-63.15,37.52-81.19A216.21,216.21,0,0,1,128,33.54a215.77,215.77,0,0,1,34.48,29.27C193.49,95.5,200,125,200,144A71.88,71.88,0,0,1,173.27,199.93Z"/></symbol>
|
||||
<symbol id="ph-flag" viewBox="0 0 256 256"><path d="M42.76,50A8,8,0,0,0,40,56V224a8,8,0,0,0,16,0V179.77c26.79-21.16,49.87-9.75,76.45,3.41,16.4,8.11,34.06,16.85,53,16.85,13.93,0,28.54-4.75,43.82-18a8,8,0,0,0,2.76-6V56A8,8,0,0,0,218.76,50c-28,24.23-51.72,12.49-79.21-1.12C111.07,34.76,78.78,18.79,42.76,50ZM216,172.25c-26.79,21.16-49.87,9.74-76.45-3.41-25-12.35-52.81-26.13-83.55-8.4V59.79c26.79-21.16,49.87-9.75,76.45,3.4,25,12.35,52.82,26.13,83.55,8.4Z"/></symbol>
|
||||
<symbol id="ph-gear" viewBox="0 0 256 256"><path d="M128,80a48,48,0,1,0,48,48A48.05,48.05,0,0,0,128,80Zm0,80a32,32,0,1,1,32-32A32,32,0,0,1,128,160Zm88-29.84q.06-2.16,0-4.32l14.92-18.64a8,8,0,0,0,1.48-7.06,107.21,107.21,0,0,0-10.88-26.25,8,8,0,0,0-6-3.93l-23.72-2.64q-1.48-1.56-3-3L186,40.54a8,8,0,0,0-3.94-6,107.71,107.71,0,0,0-26.25-10.87,8,8,0,0,0-7.06,1.49L130.16,40Q128,40,125.84,40L107.2,25.11a8,8,0,0,0-7.06-1.48A107.6,107.6,0,0,0,73.89,34.51a8,8,0,0,0-3.93,6L67.32,64.27q-1.56,1.49-3,3L40.54,70a8,8,0,0,0-6,3.94,107.71,107.71,0,0,0-10.87,26.25,8,8,0,0,0,1.49,7.06L40,125.84Q40,128,40,130.16L25.11,148.8a8,8,0,0,0-1.48,7.06,107.21,107.21,0,0,0,10.88,26.25,8,8,0,0,0,6,3.93l23.72,2.64q1.49,1.56,3,3L70,215.46a8,8,0,0,0,3.94,6,107.71,107.71,0,0,0,26.25,10.87,8,8,0,0,0,7.06-1.49L125.84,216q2.16.06,4.32,0l18.64,14.92a8,8,0,0,0,7.06,1.48,107.21,107.21,0,0,0,26.25-10.88,8,8,0,0,0,3.93-6l2.64-23.72q1.56-1.48,3-3L215.46,186a8,8,0,0,0,6-3.94,107.71,107.71,0,0,0,10.87-26.25,8,8,0,0,0-1.49-7.06Zm-16.1-6.5a73.93,73.93,0,0,1,0,8.68,8,8,0,0,0,1.74,5.48l14.19,17.73a91.57,91.57,0,0,1-6.23,15L187,173.11a8,8,0,0,0-5.1,2.64,74.11,74.11,0,0,1-6.14,6.14,8,8,0,0,0-2.64,5.1l-2.51,22.58a91.32,91.32,0,0,1-15,6.23l-17.74-14.19a8,8,0,0,0-5-1.75h-.48a73.93,73.93,0,0,1-8.68,0,8,8,0,0,0-5.48,1.74L100.45,215.8a91.57,91.57,0,0,1-15-6.23L82.89,187a8,8,0,0,0-2.64-5.1,74.11,74.11,0,0,1-6.14-6.14,8,8,0,0,0-5.1-2.64L46.43,170.6a91.32,91.32,0,0,1-6.23-15l14.19-17.74a8,8,0,0,0,1.74-5.48,73.93,73.93,0,0,1,0-8.68,8,8,0,0,0-1.74-5.48L40.2,100.45a91.57,91.57,0,0,1,6.23-15L69,82.89a8,8,0,0,0,5.1-2.64,74.11,74.11,0,0,1,6.14-6.14A8,8,0,0,0,82.89,69L85.4,46.43a91.32,91.32,0,0,1,15-6.23l17.74,14.19a8,8,0,0,0,5.48,1.74,73.93,73.93,0,0,1,8.68,0,8,8,0,0,0,5.48-1.74L155.55,40.2a91.57,91.57,0,0,1,15,6.23L173.11,69a8,8,0,0,0,2.64,5.1,74.11,74.11,0,0,1,6.14,6.14,8,8,0,0,0,5.1,2.64l22.58,2.51a91.32,91.32,0,0,1,6.23,15l-14.19,17.74A8,8,0,0,0,199.87,123.66Z"/></symbol>
|
||||
<symbol id="ph-globe" viewBox="0 0 256 256"><path d="M128,24h0A104,104,0,1,0,232,128,104.12,104.12,0,0,0,128,24Zm88,104a87.61,87.61,0,0,1-3.33,24H174.16a157.44,157.44,0,0,0,0-48h38.51A87.61,87.61,0,0,1,216,128ZM102,168H154a115.11,115.11,0,0,1-26,45A115.27,115.27,0,0,1,102,168Zm-3.9-16a140.84,140.84,0,0,1,0-48h59.88a140.84,140.84,0,0,1,0,48ZM40,128a87.61,87.61,0,0,1,3.33-24H81.84a157.44,157.44,0,0,0,0,48H43.33A87.61,87.61,0,0,1,40,128ZM154,88H102a115.11,115.11,0,0,1,26-45A115.27,115.27,0,0,1,154,88Zm52.33,0H170.71a135.28,135.28,0,0,0-22.3-45.6A88.29,88.29,0,0,1,206.37,88ZM107.59,42.4A135.28,135.28,0,0,0,85.29,88H49.63A88.29,88.29,0,0,1,107.59,42.4ZM49.63,168H85.29a135.28,135.28,0,0,0,22.3,45.6A88.29,88.29,0,0,1,49.63,168Zm98.78,45.6a135.28,135.28,0,0,0,22.3-45.6h35.66A88.29,88.29,0,0,1,148.41,213.6Z"/></symbol>
|
||||
<symbol id="ph-graph" viewBox="0 0 256 256"><path d="M200,152a31.84,31.84,0,0,0-19.53,6.68l-23.11-18A31.65,31.65,0,0,0,160,128c0-.74,0-1.48-.08-2.21l13.23-4.41A32,32,0,1,0,168,104c0,.74,0,1.48.08,2.21l-13.23,4.41A32,32,0,0,0,128,96a32.59,32.59,0,0,0-5.27.44L115.89,81A32,32,0,1,0,96,88a32.59,32.59,0,0,0,5.27-.44l6.84,15.4a31.92,31.92,0,0,0-8.57,39.64L73.83,165.44a32.06,32.06,0,1,0,10.63,12l25.71-22.84a31.91,31.91,0,0,0,37.36-1.24l23.11,18A31.65,31.65,0,0,0,168,184a32,32,0,1,0,32-32Zm0-64a16,16,0,1,1-16,16A16,16,0,0,1,200,88ZM80,56A16,16,0,1,1,96,72,16,16,0,0,1,80,56ZM56,208a16,16,0,1,1,16-16A16,16,0,0,1,56,208Zm56-80a16,16,0,1,1,16,16A16,16,0,0,1,112,128Zm88,72a16,16,0,1,1,16-16A16,16,0,0,1,200,200Z"/></symbol>
|
||||
@@ -58,7 +62,9 @@
|
||||
<symbol id="ph-palette" viewBox="0 0 256 256"><path d="M200.77,53.89A103.27,103.27,0,0,0,128,24h-1.07A104,104,0,0,0,24,128c0,43,26.58,79.06,69.36,94.17A32,32,0,0,0,136,192a16,16,0,0,1,16-16h46.21a31.81,31.81,0,0,0,31.2-24.88,104.43,104.43,0,0,0,2.59-24A103.28,103.28,0,0,0,200.77,53.89Zm13,93.71A15.89,15.89,0,0,1,198.21,160H152a32,32,0,0,0-32,32,16,16,0,0,1-21.31,15.07C62.49,194.3,40,164,40,128a88,88,0,0,1,87.09-88h.9a88.35,88.35,0,0,1,88,87.25A88.86,88.86,0,0,1,213.81,147.6ZM140,76a12,12,0,1,1-12-12A12,12,0,0,1,140,76ZM96,100A12,12,0,1,1,84,88,12,12,0,0,1,96,100Zm0,56a12,12,0,1,1-12-12A12,12,0,0,1,96,156Zm88-56a12,12,0,1,1-12-12A12,12,0,0,1,184,100Z"/></symbol>
|
||||
<symbol id="ph-paper-plane-tilt" viewBox="0 0 256 256"><path d="M227.32,28.68a16,16,0,0,0-15.66-4.08l-.15,0L19.57,82.84a16,16,0,0,0-2.49,29.8L102,154l41.3,84.87A15.86,15.86,0,0,0,157.74,248q.69,0,1.38-.06a15.88,15.88,0,0,0,14-11.51l58.2-191.94c0-.05,0-.1,0-.15A16,16,0,0,0,227.32,28.68ZM157.83,231.85l-.05.14,0-.07-40.06-82.3,48-48a8,8,0,0,0-11.31-11.31l-48,48L24.08,98.25l-.07,0,.14,0L216,40Z"/></symbol>
|
||||
<symbol id="ph-path" viewBox="0 0 256 256"><path d="M200,168a32.06,32.06,0,0,0-31,24H72a32,32,0,0,1,0-64h96a40,40,0,0,0,0-80H72a8,8,0,0,0,0,16h96a24,24,0,0,1,0,48H72a48,48,0,0,0,0,96h97a32,32,0,1,0,31-40Zm0,48a16,16,0,1,1,16-16A16,16,0,0,1,200,216Z"/></symbol>
|
||||
<symbol id="ph-pause" viewBox="0 0 256 256"><path d="M200,32H160a16,16,0,0,0-16,16V208a16,16,0,0,0,16,16h40a16,16,0,0,0,16-16V48A16,16,0,0,0,200,32Zm0,176H160V48h40ZM96,32H56A16,16,0,0,0,40,48V208a16,16,0,0,0,16,16H96a16,16,0,0,0,16-16V48A16,16,0,0,0,96,32Zm0,176H56V48H96Z"/></symbol>
|
||||
<symbol id="ph-piano-keys" viewBox="0 0 256 256"><path d="M208,32H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM80,48h24v88H80Zm32,104a8,8,0,0,0,8-8V48h16v96a8,8,0,0,0,8,8h8v56H104V152Zm40-16V48h24v88ZM48,48H64v96a8,8,0,0,0,8,8H88v56H48ZM208,208H168V152h16a8,8,0,0,0,8-8V48h16V208Z"/></symbol>
|
||||
<symbol id="ph-play" viewBox="0 0 256 256"><path d="M232.4,114.49,88.32,26.35a16,16,0,0,0-16.2-.3A15.86,15.86,0,0,0,64,39.87V216.13A15.94,15.94,0,0,0,80,232a16.07,16.07,0,0,0,8.36-2.35L232.4,141.51a15.81,15.81,0,0,0,0-27ZM80,215.94V40l143.83,88Z"/></symbol>
|
||||
<symbol id="ph-plant" viewBox="0 0 256 256"><path d="M247.63,47.89a8,8,0,0,0-7.52-7.52c-51.76-3-93.32,12.74-111.18,42.22-11.8,19.49-11.78,43.16-.16,65.74a71.34,71.34,0,0,0-14.17,27L98.33,159c7.82-16.33,7.52-33.35-1-47.49-13.2-21.79-43.67-33.47-81.5-31.25a8,8,0,0,0-7.52,7.52c-2.23,37.83,9.46,68.3,31.25,81.5A45.82,45.82,0,0,0,63.44,176,54.58,54.58,0,0,0,87,170.33l25,25V224a8,8,0,0,0,16,0V194.51a55.61,55.61,0,0,1,12.27-35,73.91,73.91,0,0,0,33.31,8.4,60.9,60.9,0,0,0,31.83-8.86C234.89,141.21,250.67,99.65,247.63,47.89ZM47.81,155.6C32.47,146.31,23.79,124.32,24,96c28.32-.24,50.31,8.47,59.6,23.81,4.85,8,5.64,17.33,2.46,26.94L61.65,122.34a8,8,0,0,0-11.31,11.31l24.41,24.41C65.14,161.24,55.82,160.45,47.81,155.6Zm149.31-10.22c-13.4,8.11-29.15,8.73-45.15,2l53.69-53.7a8,8,0,0,0-11.31-11.31L140.65,136c-6.76-16-6.15-31.76,2-45.15,13.94-23,47-35.82,89.33-34.83C232.94,98.34,220.14,131.44,197.12,145.38Z"/></symbol>
|
||||
<symbol id="ph-prohibit" viewBox="0 0 256 256"><path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm88,104a87.56,87.56,0,0,1-20.41,56.28L71.72,60.4A88,88,0,0,1,216,128ZM40,128A87.56,87.56,0,0,1,60.41,71.72L184.28,195.6A88,88,0,0,1,40,128Z"/></symbol>
|
||||
<symbol id="ph-pulse" viewBox="0 0 256 256"><path d="M240,128a8,8,0,0,1-8,8H204.94l-37.78,75.58A8,8,0,0,1,160,216h-.4a8,8,0,0,1-7.08-5.14L95.35,60.76,63.28,131.31A8,8,0,0,1,56,136H24a8,8,0,0,1,0-16H50.85L88.72,36.69a8,8,0,0,1,14.76.46l57.51,151,31.85-63.71A8,8,0,0,1,200,120h32A8,8,0,0,1,240,128Z"/></symbol>
|
||||
|
||||
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 40 KiB |
+5
-3
@@ -170,7 +170,7 @@
|
||||
<div id="map-wrap" style="position:relative;width:100%;height:100%;display:flex;">
|
||||
<div id="leaflet-map" style="flex:1 1 0%;height:100%;"></div>
|
||||
<div class="map-side-pane" id="mapSidePane">
|
||||
<div class="pane-toggle" id="mapPaneToggle" title="Path Inspector">◀</div>
|
||||
<div class="pane-toggle" id="mapPaneToggle" title="Path Inspector"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-caret-left"/></svg></div>
|
||||
<div class="pane-content">
|
||||
<h3 style="margin:0 0 8px 0;font-size:14px;">Path Inspector</h3>
|
||||
<p style="font-size:11px;color:var(--text-muted);margin:0 0 8px 0;">Hex prefixes (1-3 bytes), comma or space separated.</p>
|
||||
@@ -1760,7 +1760,9 @@
|
||||
|
||||
toggle.addEventListener('click', function () {
|
||||
pane.classList.toggle('expanded');
|
||||
toggle.textContent = pane.classList.contains('expanded') ? '▶' : '◀';
|
||||
// #1648 M4: Phosphor sprite glyph for pane toggle (was ▶/◀). // EMOJI-OK: comment
|
||||
toggle.innerHTML = '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#' +
|
||||
(pane.classList.contains('expanded') ? 'ph-caret-right' : 'ph-caret-left') + '"/></svg>';
|
||||
// Invalidate map size after transition.
|
||||
setTimeout(function () { if (map) map.invalidateSize(); }, 220);
|
||||
});
|
||||
@@ -1777,7 +1779,7 @@
|
||||
var prefixParam = params.get('prefixes');
|
||||
if (prefixParam && input) {
|
||||
pane.classList.add('expanded');
|
||||
toggle.textContent = '▶';
|
||||
toggle.innerHTML = '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-caret-right"/></svg>';
|
||||
input.value = prefixParam;
|
||||
setTimeout(function () { if (map) map.invalidateSize(); }, 220);
|
||||
mapPiSubmit(prefixParam);
|
||||
|
||||
+5
-3
@@ -1107,7 +1107,9 @@
|
||||
packetsPaused = !packetsPaused;
|
||||
const pauseBtn = document.getElementById('pktPauseBtn');
|
||||
if (pauseBtn) {
|
||||
pauseBtn.textContent = packetsPaused ? '▶' : '⏸';
|
||||
// #1648 M4: Phosphor sprite glyph for play/pause (was ▶/⏸). // EMOJI-OK: comment
|
||||
pauseBtn.innerHTML = '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#' +
|
||||
(packetsPaused ? 'ph-play' : 'ph-pause') + '"/></svg>';
|
||||
pauseBtn.title = packetsPaused ? 'Resume live updates' : 'Pause live updates';
|
||||
pauseBtn.classList.toggle('active', packetsPaused);
|
||||
}
|
||||
@@ -1155,7 +1157,7 @@
|
||||
pauseBuffer.push(...msgs);
|
||||
if (pauseBuffer.length > 2000) pauseBuffer = pauseBuffer.slice(-2000);
|
||||
const btn = document.getElementById('pktPauseBtn');
|
||||
if (btn) btn.textContent = '▶ ' + pauseBuffer.length;
|
||||
if (btn) btn.innerHTML = '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-play"/></svg> ' + pauseBuffer.length;
|
||||
return;
|
||||
}
|
||||
const newPkts = msgs
|
||||
@@ -3112,7 +3114,7 @@
|
||||
<button class="copy-link-btn" data-packet-hash="${pkt.hash || ''}" data-packet-id="${pkt.id}" title="Copy link to this packet"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-link"/></svg> Copy Link</button>
|
||||
${pathHops.length ? `<button class="detail-map-link" id="viewRouteBtn"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-map-trifold"/></svg> View route on map</button>` : ''}
|
||||
${pkt.hash ? `<a href="#/traces/${pkt.hash}" class="detail-map-link" style="text-decoration:none"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-magnifying-glass"/></svg> Trace</a>` : ''}
|
||||
<button class="replay-live-btn" title="Replay this packet on the live map">▶ Replay</button>
|
||||
<button class="replay-live-btn" title="Replay this packet on the live map"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-play"/></svg> Replay</button>
|
||||
</div>
|
||||
|
||||
${(hasRawHex || Object.keys(decoded).length) ? `<details class="detail-technical"${(typeof window !== 'undefined' && window.innerWidth > 480) ? ' open' : ''}>
|
||||
|
||||
+23
-13
@@ -58,8 +58,9 @@
|
||||
|
||||
/**
|
||||
* Build the role-aware marker SVG for a hop. Origin and destination get a
|
||||
* larger outline + a glyph (▶ / ⚑) layered on the standard role shape so
|
||||
* the role information remains visible.
|
||||
* larger outline + a Phosphor sprite glyph (play/flag) layered on the
|
||||
* standard role shape so the role information remains visible.
|
||||
* #1648 M4: prior glyphs were inline <text> chars (\u25B6 / \u2691). // EMOJI-OK: comment
|
||||
*/
|
||||
function buildHopSVG(p, opts) {
|
||||
var size = opts.size || 22;
|
||||
@@ -78,13 +79,18 @@
|
||||
var ringDash = opts.unresolved ? '4 3' : 'none';
|
||||
var ringFill = opts.unresolved ? 'rgba(150,150,150,0.15)' : 'none';
|
||||
|
||||
// Phosphor sprite <use> overlaid on the role marker. Sized to ~55% of
|
||||
// outer ring, centered on the marker. fill="#0f172a" preserves the
|
||||
// dark-on-light glyph contrast of the prior <text> implementation.
|
||||
var glyph = '';
|
||||
if (opts.isOrigin) {
|
||||
glyph = '<text x="' + (outerSize / 2) + '" y="' + (outerSize / 2 + 4) +
|
||||
'" text-anchor="middle" font-size="11" font-weight="700" fill="#0f172a" aria-hidden="true">\u25B6</text>';
|
||||
} else if (opts.isDest) {
|
||||
glyph = '<text x="' + (outerSize / 2) + '" y="' + (outerSize / 2 + 4) +
|
||||
'" text-anchor="middle" font-size="12" font-weight="700" fill="#0f172a" aria-hidden="true">\u2691</text>';
|
||||
if (opts.isOrigin || opts.isDest) {
|
||||
var gid = opts.isOrigin ? 'ph-play' : 'ph-flag';
|
||||
var gSize = Math.round(outerSize * 0.55);
|
||||
var gOff = (outerSize - gSize) / 2;
|
||||
glyph = '<svg x="' + gOff + '" y="' + gOff + '" width="' + gSize +
|
||||
'" height="' + gSize + '" viewBox="0 0 256 256" fill="#0f172a"' +
|
||||
' aria-hidden="true"><use href="/icons/phosphor-sprite.svg#' + gid +
|
||||
'"/></svg>';
|
||||
}
|
||||
|
||||
// Strip outer <svg> from inner SVG, re-wrap with outer ring + glyph
|
||||
@@ -103,11 +109,15 @@
|
||||
}
|
||||
|
||||
function buildBadge(idx, total, opts) {
|
||||
var txt;
|
||||
if (opts.isOrigin) txt = '\u25B6'; // ▶
|
||||
else if (opts.isDest) txt = '\u2691'; // ⚑
|
||||
else txt = String(idx); // intermediate hop number
|
||||
return '<span class="mc-route-seq-badge" aria-hidden="true">' + txt + '</span>';
|
||||
// Intermediate hops render the hop number; origin/destination render a
|
||||
// Phosphor sprite glyph (play/flag) in place of the prior \u25B6 / \u2691. // EMOJI-OK: comment
|
||||
if (opts.isOrigin || opts.isDest) {
|
||||
var gid = opts.isOrigin ? 'ph-play' : 'ph-flag';
|
||||
return '<span class="mc-route-seq-badge" aria-hidden="true">' +
|
||||
'<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#' +
|
||||
gid + '"/></svg></span>';
|
||||
}
|
||||
return '<span class="mc-route-seq-badge" aria-hidden="true">' + String(idx) + '</span>';
|
||||
}
|
||||
|
||||
function buildPopupHtml(p, hopNum, total) {
|
||||
|
||||
@@ -115,13 +115,14 @@ body.mc-route-active .leaflet-overlay-pane svg path:not(.mc-rt-edge) { display:
|
||||
font-weight: 600;
|
||||
}
|
||||
.mc-rt-paths-header::-webkit-details-marker { display: none; }
|
||||
.mc-rt-paths-header::before {
|
||||
content: '▾';
|
||||
/* #1648 M4: chevron is now an inline Phosphor sprite (ph-caret-down) injected
|
||||
by route-view.js. CSS only handles the rotation when the <details> is
|
||||
closed. Replaces prior ::before content with a Misc-Symbols char. */
|
||||
.mc-rt-paths-chevron {
|
||||
margin-right: 4px;
|
||||
font-size: 9px;
|
||||
transition: transform 120ms;
|
||||
}
|
||||
.mc-rt-paths:not([open]) .mc-rt-paths-header::before { transform: rotate(-90deg); }
|
||||
.mc-rt-paths:not([open]) .mc-rt-paths-chevron { transform: rotate(-90deg); }
|
||||
.mc-rt-path-clear {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border, #444);
|
||||
|
||||
@@ -355,6 +355,7 @@
|
||||
'</li>';
|
||||
}).join('');
|
||||
pathPicker = '<details class="mc-rt-paths" open><summary class="mc-rt-paths-header">' +
|
||||
'<svg class="ph-icon mc-rt-paths-chevron" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-caret-down"/></svg>' +
|
||||
uniquePathsCount + ' unique paths · click to isolate' +
|
||||
'<button type="button" class="mc-rt-path-clear" aria-label="Show all paths">All</button>' +
|
||||
'</summary><ul class="mc-rt-path-list">' + pickerRows + '</ul></details>';
|
||||
@@ -448,7 +449,7 @@
|
||||
sidebar.innerHTML =
|
||||
// Desktop: resize handle on the right edge + collapse button.
|
||||
'<div class="mc-rt-resize-handle" role="separator" aria-label="Resize route panel" aria-orientation="vertical" tabindex="0"></div>' +
|
||||
'<button type="button" class="mc-rt-collapse-btn" aria-label="Collapse route panel" title="Collapse route panel">◀</button>' +
|
||||
'<button type="button" class="mc-rt-collapse-btn" aria-label="Collapse route panel" title="Collapse route panel"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-caret-left"/></svg></button>' +
|
||||
'<div class="mc-rt-collapsed-label" aria-hidden="true">ROUTE</div>' +
|
||||
// Mobile: bottom-sheet header (summary + chevron). No drag-grip —
|
||||
// conflicted with browser pull-to-refresh and CoreScope's own pull-to-
|
||||
@@ -466,7 +467,10 @@
|
||||
var collapsed = sidebar.classList.toggle('mc-rt-collapsed');
|
||||
collapseBtn.setAttribute('aria-label', collapsed ? 'Expand route panel' : 'Collapse route panel');
|
||||
collapseBtn.setAttribute('title', collapsed ? 'Expand route panel' : 'Collapse route panel');
|
||||
collapseBtn.textContent = collapsed ? '▶' : '◀';
|
||||
// #1648 M4: swap Phosphor sprite glyph (caret-right when collapsed,
|
||||
// caret-left when expanded). Replaces prior ▶/◀ Misc-Symbols chars. // EMOJI-OK: comment
|
||||
collapseBtn.innerHTML = '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#' +
|
||||
(collapsed ? 'ph-caret-right' : 'ph-caret-left') + '"/></svg>';
|
||||
setTimeout(function () { if (mapRef && mapRef.invalidateSize) mapRef.invalidateSize(); }, 280);
|
||||
});
|
||||
}
|
||||
@@ -478,7 +482,7 @@
|
||||
sidebar.classList.remove('mc-rt-collapsed');
|
||||
if (collapseBtn) {
|
||||
collapseBtn.setAttribute('aria-label', 'Collapse route panel');
|
||||
collapseBtn.textContent = '◀';
|
||||
collapseBtn.innerHTML = '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-caret-left"/></svg>';
|
||||
}
|
||||
setTimeout(function () { if (mapRef && mapRef.invalidateSize) mapRef.invalidateSize(); }, 280);
|
||||
}
|
||||
|
||||
@@ -109,12 +109,21 @@ async function runViewport(browser, width, height, label) {
|
||||
await step(label + ': sequence-number badge present beside each marker (not in label text)', async () => {
|
||||
const data = await page.evaluate(() => {
|
||||
const badges = Array.from(document.querySelectorAll('.mc-route-seq-badge'));
|
||||
return badges.map(b => b.textContent.trim());
|
||||
return badges.map(b => ({
|
||||
text: b.textContent.trim(),
|
||||
// #1648 M4: origin/dest badges now contain a Phosphor sprite
|
||||
// (<use href="…#ph-play"/> or "#ph-flag") instead of a glyph char.
|
||||
spriteId: (b.querySelector('use') || {}).getAttribute &&
|
||||
(b.querySelector('use').getAttribute('href') || '').replace(/^.*#/, ''),
|
||||
}));
|
||||
});
|
||||
assert(data.length >= 5, 'expected >=5 sequence badges, got ' + data.length);
|
||||
// Badges should be numeric or numbered glyphs.
|
||||
// Badges should be numeric, a numbered glyph, OR a Phosphor sprite ref
|
||||
// (ph-play for origin, ph-flag for destination).
|
||||
for (const b of data) {
|
||||
assert(/^[\d①②③④⑤⑥⑦⑧⑨⑩▶⚑]+$/.test(b), 'badge "' + b + '" not numeric/glyph');
|
||||
if (b.text && /^[\d①②③④⑤⑥⑦⑧⑨⑩▶⚑]+$/.test(b.text)) continue;
|
||||
if (b.spriteId && /^ph-(play|flag)$/.test(b.spriteId)) continue;
|
||||
assert(false, 'badge "' + JSON.stringify(b) + '" not numeric/glyph/sprite');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/env node
|
||||
/* Issue #1648 — M4: emoji → Phosphor sprite migration (static scan).
|
||||
*
|
||||
* M4 covers map & route overlays:
|
||||
* map.js, area-map.html, analytics.js (route-jump residual),
|
||||
* packets.js (route/replay residual), route-view.js,
|
||||
* route-view-utils.js, route-view.css, route-render.js
|
||||
*
|
||||
* Asserts (per file):
|
||||
* 1. Zero UI-iconography codepoints (emoji + the Misc-Symbols set used
|
||||
* historically as icons, including pane-toggle carets ▶/◀, dropdown
|
||||
* caret ▾, ✕ close, ⚑ destination flag) outside an allowlist
|
||||
* (// EMOJI-OK lines, or comments mentioning prior glyphs).
|
||||
* 2. At least N <use href="…#ph-… references per file (sanity floor).
|
||||
*
|
||||
* Anti-tautology: this test FAILS pre-implementation by construction.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const assert = require('assert');
|
||||
|
||||
const ROOT = path.resolve(__dirname, 'public');
|
||||
|
||||
const M4_FILES = [
|
||||
'map.js',
|
||||
'area-map.html',
|
||||
'analytics.js',
|
||||
'packets.js',
|
||||
'route-view.js',
|
||||
'route-view-utils.js',
|
||||
'route-view.css',
|
||||
'route-render.js',
|
||||
];
|
||||
|
||||
// Floors: existing M2/M3 swaps already in some of these files contribute.
|
||||
// M4 adds at least a handful per file (carets, replay, close, flag, etc.).
|
||||
const MIN_USE_REFS = {
|
||||
'map.js': 18,
|
||||
'area-map.html': 1,
|
||||
'analytics.js': 60,
|
||||
'packets.js': 8,
|
||||
'route-view.js': 5,
|
||||
'route-view-utils.js': 6,
|
||||
'route-view.css': 0, // CSS uses content with <svg> not feasible; rely on JS
|
||||
// route-render.js uses dynamic href concat (ph-play/ph-flag chosen at runtime);
|
||||
// sprite refs are functional but not a literal-substring grep target. Floor 0.
|
||||
'route-render.js': 0,
|
||||
};
|
||||
|
||||
const EMOJI = /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}]/u;
|
||||
// Misc-Symbols treated as iconography across all M4 surfaces.
|
||||
const MISC_ICON_BASE = /[◆●■▲★☆○✓✗⚠✉✕⚑]/u;
|
||||
// Per-file extra chars: pane/collapse carets ▶◀ and route-CSS chevron ▾.
|
||||
// (packets.js dropdown chevrons ▾/▴ are M5 table-chrome; not in M4 scope.)
|
||||
const EXTRA_ICONS = {
|
||||
'map.js': /[▶◀▾]/u,
|
||||
'analytics.js': /[▶◀]/u,
|
||||
'packets.js': /[▶◀]/u,
|
||||
'route-view.js': /[▶◀▾]/u,
|
||||
'route-view-utils.js': /[▶◀▾]/u,
|
||||
'route-view.css': /[▶◀▾]/u,
|
||||
'route-render.js': /[▶◀▾]/u,
|
||||
};
|
||||
|
||||
function scanFile(rel) {
|
||||
const abs = path.join(ROOT, rel);
|
||||
const txt = fs.readFileSync(abs, 'utf8');
|
||||
const lines = txt.split('\n');
|
||||
const hits = [];
|
||||
const extra = EXTRA_ICONS[rel];
|
||||
lines.forEach((line, idx) => {
|
||||
if (line.includes('EMOJI-OK')) return;
|
||||
if (EMOJI.test(line) || MISC_ICON_BASE.test(line) || (extra && extra.test(line))) {
|
||||
hits.push({ file: rel, line: idx + 1, text: line.trim().slice(0, 200) });
|
||||
}
|
||||
});
|
||||
return hits;
|
||||
}
|
||||
|
||||
function countUseRefs(rel) {
|
||||
const abs = path.join(ROOT, rel);
|
||||
const txt = fs.readFileSync(abs, 'utf8');
|
||||
return (txt.match(/<use href="\/icons\/phosphor-sprite\.svg#ph-/g) || []).length;
|
||||
}
|
||||
|
||||
function assertSpriteHasM4Icons() {
|
||||
const sp = path.join(ROOT, 'icons', 'phosphor-sprite.svg');
|
||||
const txt = fs.readFileSync(sp, 'utf8');
|
||||
const need = [
|
||||
'ph-caret-left', 'ph-caret-right', 'ph-play',
|
||||
'ph-flag', 'ph-arrow-u-up-left', 'ph-pause',
|
||||
];
|
||||
const missing = need.filter(id => !txt.includes(`id="${id}"`));
|
||||
if (missing.length) throw new Error(`sprite missing M4 symbols: ${missing.join(', ')}`);
|
||||
}
|
||||
|
||||
// Route-render.js builds its sprite href with runtime concat. Verify the
|
||||
// sprite IDs and the sprite path prefix both appear in source.
|
||||
function assertRouteRenderSpriteRefs() {
|
||||
const txt = fs.readFileSync(path.join(ROOT, 'route-render.js'), 'utf8');
|
||||
for (const id of ['ph-play', 'ph-flag']) {
|
||||
if (!txt.includes("'" + id + "'") && !txt.includes('"' + id + '"')) {
|
||||
throw new Error(`route-render.js must reference sprite id ${id}`);
|
||||
}
|
||||
}
|
||||
if (!txt.includes('/icons/phosphor-sprite.svg#')) {
|
||||
throw new Error('route-render.js must reference the Phosphor sprite path');
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
let failed = 0;
|
||||
console.log('— Issue #1648 M4 — emoji/misc-icon scan');
|
||||
|
||||
try {
|
||||
assertSpriteHasM4Icons();
|
||||
console.log(' ✓ sprite has required M4 symbols');
|
||||
} catch (e) {
|
||||
console.error(` ✗ ${e.message}`);
|
||||
failed++;
|
||||
}
|
||||
|
||||
try {
|
||||
assertRouteRenderSpriteRefs();
|
||||
console.log(' ✓ route-render.js threads ph-play / ph-flag sprite refs');
|
||||
} catch (e) {
|
||||
console.error(` ✗ ${e.message}`);
|
||||
failed++;
|
||||
}
|
||||
|
||||
for (const rel of M4_FILES) {
|
||||
const hits = scanFile(rel);
|
||||
if (hits.length === 0) {
|
||||
console.log(` ✓ ${rel} clean`);
|
||||
} else {
|
||||
console.error(` ✗ ${rel} has ${hits.length} emoji/misc-icon hit(s):`);
|
||||
for (const h of hits.slice(0, 30)) console.error(` ${h.file}:${h.line} ${h.text}`);
|
||||
if (hits.length > 30) console.error(` … (+${hits.length - 30} more)`);
|
||||
failed++;
|
||||
}
|
||||
const useRefs = countUseRefs(rel);
|
||||
const min = MIN_USE_REFS[rel] || 0;
|
||||
if (useRefs < min) {
|
||||
console.error(` ✗ ${rel} has only ${useRefs} <use href="…#ph-…"> refs (expected ≥${min})`);
|
||||
failed++;
|
||||
} else {
|
||||
console.log(` ✓ ${rel} has ${useRefs} Phosphor <use> refs (≥${min})`);
|
||||
}
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
console.error(`\nFAIL: ${failed} M4 check(s) failed`);
|
||||
process.exit(1);
|
||||
}
|
||||
// Hard asserts for CI
|
||||
for (const rel of M4_FILES) {
|
||||
const hits = scanFile(rel);
|
||||
assert.strictEqual(hits.length, 0,
|
||||
`${rel} must contain zero emoji/misc-icon iconography (got ${hits.length} hit(s))`);
|
||||
const useRefs = countUseRefs(rel);
|
||||
const min = MIN_USE_REFS[rel] || 0;
|
||||
assert.ok(useRefs >= min,
|
||||
`${rel} must have ≥${min} <use href="…#ph-…"> refs (got ${useRefs})`);
|
||||
}
|
||||
console.log('\nPASS: all M4 surfaces icon-free and Phosphor-swapped');
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,223 @@
|
||||
#!/usr/bin/env node
|
||||
/* Issue #1648 — M4: emoji → Phosphor sprite migration (E2E behavioral).
|
||||
*
|
||||
* Asserts (in a real Chromium against a running server):
|
||||
* (a) /map — Path Inspector pane toggle renders a Phosphor sprite (no
|
||||
* Misc-Symbols caret), and overall map page is icon-free.
|
||||
* (b) /map?packet=<hash>&obs=<id> (multi-path CHAN fixture) — route
|
||||
* overlay sidebar collapse button renders Phosphor caret.
|
||||
* (c) /packets — replay button renders Phosphor play sprite (no ▶ char).
|
||||
* (d) /analytics — distance map-jump buttons render ph-map-trifold and
|
||||
* the Network Overview chevron renders a sprite.
|
||||
* (e) /area-map standalone — clear/undo buttons render sprites.
|
||||
* (f) NO .notdef anywhere — every <use> resolves to a defined symbol id.
|
||||
*
|
||||
* CI gating: CHROMIUM_REQUIRE=1 makes Chromium-launch failure a HARD FAIL.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const { chromium } = require('playwright');
|
||||
const assert = require('assert');
|
||||
|
||||
const BASE = process.env.BASE_URL || 'http://localhost:13581';
|
||||
// Forbidden Misc-Symbols icon chars in rendered M4 surfaces:
|
||||
const M4_FORBIDDEN_RE = /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}◆●■▲★☆○✓✗⚠✉✕▶◀▾⚑]/u;
|
||||
|
||||
let passes = 0, failures = 0;
|
||||
function pass(msg) { console.log(` ✓ ${msg}`); passes++; }
|
||||
function fail(msg) { console.error(` ✗ ${msg}`); failures++; }
|
||||
|
||||
async function gotoSpa(page, route) {
|
||||
await page.goto(`${BASE}/#${route}`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForFunction(() => !!document.querySelector('#app'), null, { timeout: 8000 }).catch(() => {});
|
||||
await page.waitForTimeout(700);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const requireChromium = process.env.CHROMIUM_REQUIRE === '1';
|
||||
let browser;
|
||||
try {
|
||||
browser = await chromium.launch({
|
||||
headless: true,
|
||||
executablePath: process.env.CHROMIUM_PATH || undefined,
|
||||
args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
|
||||
});
|
||||
} catch (err) {
|
||||
if (requireChromium) {
|
||||
console.error(`test-issue-1648-m4-icons-e2e.js: HARD FAIL — Chromium unavailable: ${err.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.warn(`SKIP — Chromium unavailable: ${err.message}`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
|
||||
// (a) /map — pane toggle + general sprite presence
|
||||
await gotoSpa(page, '/map');
|
||||
const mapState = await page.evaluate(() => {
|
||||
const toggle = document.getElementById('mapPaneToggle');
|
||||
return {
|
||||
paneToggleText: toggle ? toggle.textContent : null,
|
||||
paneToggleSprites: toggle ? toggle.querySelectorAll('svg.ph-icon use').length : 0,
|
||||
allSprites: document.querySelectorAll('svg.ph-icon use').length,
|
||||
bodyText: (document.getElementById('app') || document.body).textContent || '',
|
||||
};
|
||||
});
|
||||
if (mapState.paneToggleSprites === 0) fail('(a) /map: pane toggle has no Phosphor sprite');
|
||||
else pass(`(a) /map: pane toggle has ${mapState.paneToggleSprites} sprite(s)`);
|
||||
if (mapState.paneToggleText && /[▶◀]/.test(mapState.paneToggleText)) {
|
||||
fail(`(a) /map: pane toggle still contains ▶/◀ text (got ${JSON.stringify(mapState.paneToggleText)})`);
|
||||
} else {
|
||||
pass('(a) /map: pane toggle text has no Misc-Symbols carets');
|
||||
}
|
||||
if (mapState.allSprites < 3) fail(`(a) /map: only ${mapState.allSprites} sprite refs total (expected ≥3)`);
|
||||
else pass(`(a) /map: ${mapState.allSprites} sprite refs on page`);
|
||||
|
||||
// (b) /map?packet=<hash>&obs=<id> — multi-path CHAN fixture renders
|
||||
// route overlay sidebar with a collapse button. We don't require the
|
||||
// fixture to exist in staging — only assert the sprite scheme if the
|
||||
// sidebar shows up.
|
||||
await gotoSpa(page, '/map?packet=305b678c9394b964&obs=10591318');
|
||||
await page.waitForTimeout(1200);
|
||||
const route = await page.evaluate(() => {
|
||||
const sidebar = document.querySelector('.mc-rt-sidebar, [class*="mc-rt-"]');
|
||||
const collapseBtn = document.querySelector('.mc-rt-collapse-btn');
|
||||
return {
|
||||
sidebarPresent: !!sidebar,
|
||||
collapseSprites: collapseBtn ? collapseBtn.querySelectorAll('svg.ph-icon use').length : 0,
|
||||
collapseText: collapseBtn ? collapseBtn.textContent : '',
|
||||
mapSprites: document.querySelectorAll('svg.ph-icon use').length,
|
||||
};
|
||||
});
|
||||
if (route.sidebarPresent && route.collapseSprites === 0) {
|
||||
fail('(b) /map?packet=…: route collapse button has no sprite');
|
||||
} else if (route.sidebarPresent) {
|
||||
pass(`(b) /map?packet=…: route collapse button has ${route.collapseSprites} sprite(s)`);
|
||||
} else {
|
||||
console.warn(' ⚠ (b) /map?packet=…: route sidebar not present (fixture may be missing in this env)');
|
||||
}
|
||||
if (route.collapseText && /[▶◀]/.test(route.collapseText)) {
|
||||
fail('(b) /map?packet=…: collapse btn still has ▶/◀ char');
|
||||
}
|
||||
|
||||
// (c) /packets — replay button renders Phosphor play sprite
|
||||
await gotoSpa(page, '/packets');
|
||||
await page.waitForTimeout(1500);
|
||||
// Click first packet row to render detail with replay button (if any).
|
||||
await page.evaluate(() => {
|
||||
const row = document.querySelector('table tbody tr, .pkt-row, .packet-row');
|
||||
if (row) row.click();
|
||||
});
|
||||
await page.waitForTimeout(800);
|
||||
const packets = await page.evaluate(() => {
|
||||
const replay = document.querySelector('.replay-live-btn');
|
||||
const viewRoute = document.querySelector('#viewRouteBtn, .detail-map-link');
|
||||
return {
|
||||
replaySprite: replay ? replay.querySelectorAll('svg.ph-icon use').length : null,
|
||||
replayText: replay ? replay.textContent : null,
|
||||
viewRouteSprites: viewRoute ? viewRoute.querySelectorAll('svg.ph-icon use').length : null,
|
||||
pageSprites: document.querySelectorAll('svg.ph-icon use').length,
|
||||
};
|
||||
});
|
||||
if (packets.replaySprite === 0) {
|
||||
fail('(c) /packets: replay button has no sprite');
|
||||
} else if (packets.replaySprite > 0) {
|
||||
pass(`(c) /packets: replay button has ${packets.replaySprite} sprite(s)`);
|
||||
} else {
|
||||
console.warn(' ⚠ (c) /packets: replay button not present (no packet detail open)');
|
||||
}
|
||||
if (packets.replayText && /[▶◀⏸]/.test(packets.replayText)) {
|
||||
fail(`(c) /packets: replay button still has play/pause char (${JSON.stringify(packets.replayText)})`);
|
||||
}
|
||||
if (packets.viewRouteSprites === 0) fail('(c) /packets: View-route button missing sprite');
|
||||
else if (packets.viewRouteSprites > 0) pass('(c) /packets: View-route button has sprite');
|
||||
if (packets.pageSprites < 5) fail(`(c) /packets: only ${packets.pageSprites} sprite refs (expected ≥5)`);
|
||||
else pass(`(c) /packets: ${packets.pageSprites} sprite refs on page`);
|
||||
|
||||
// (d) /analytics — Network Overview chevron + distance map-jump buttons
|
||||
await gotoSpa(page, '/analytics?tab=prefix-tool');
|
||||
await page.waitForTimeout(1500);
|
||||
const analytics = await page.evaluate(() => {
|
||||
const chev = document.getElementById('ptOverviewChevron');
|
||||
return {
|
||||
chevText: chev ? chev.textContent : null,
|
||||
chevSprites: chev ? chev.querySelectorAll('svg.ph-icon use').length : 0,
|
||||
pageSprites: document.querySelectorAll('svg.ph-icon use').length,
|
||||
};
|
||||
});
|
||||
if (analytics.chevSprites === 0) {
|
||||
fail('(d) /analytics: Network Overview chevron has no sprite');
|
||||
} else {
|
||||
pass(`(d) /analytics: Network Overview chevron has ${analytics.chevSprites} sprite(s)`);
|
||||
}
|
||||
if (analytics.chevText && /[▶◀]/.test(analytics.chevText)) {
|
||||
fail('(d) /analytics: chevron still contains ▶ text');
|
||||
}
|
||||
if (analytics.pageSprites < 10) fail(`(d) /analytics: only ${analytics.pageSprites} sprite refs`);
|
||||
else pass(`(d) /analytics: ${analytics.pageSprites} sprite refs on page`);
|
||||
|
||||
// Also check distance tab if reachable
|
||||
await gotoSpa(page, '/analytics?tab=distance');
|
||||
await page.waitForTimeout(2000);
|
||||
const dist = await page.evaluate(() => {
|
||||
const btn = document.querySelector('.dist-map-hop, .dist-map-path');
|
||||
return {
|
||||
mapBtnSprites: btn ? btn.querySelectorAll('svg.ph-icon use').length : null,
|
||||
};
|
||||
});
|
||||
if (dist.mapBtnSprites === 0) fail('(d) /analytics distance: map-jump button missing sprite');
|
||||
else if (dist.mapBtnSprites > 0) pass('(d) /analytics distance: map-jump button has sprite');
|
||||
else console.warn(' ⚠ (d) /analytics distance: no map-jump button rendered (empty dataset)');
|
||||
|
||||
// (e) /area-map (standalone HTML)
|
||||
await page.goto(`${BASE}/area-map.html`, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(500);
|
||||
const area = await page.evaluate(() => {
|
||||
const clear = document.getElementById('btn-clear-draw');
|
||||
const undo = document.getElementById('btn-undo');
|
||||
return {
|
||||
clearSprites: clear ? clear.querySelectorAll('svg.ph-icon use').length : 0,
|
||||
clearText: clear ? clear.textContent : '',
|
||||
undoSprites: undo ? undo.querySelectorAll('svg.ph-icon use').length : 0,
|
||||
};
|
||||
});
|
||||
if (area.clearSprites === 0) fail('(e) /area-map: clear button has no sprite');
|
||||
else pass(`(e) /area-map: clear button has ${area.clearSprites} sprite(s)`);
|
||||
if (area.undoSprites === 0) fail('(e) /area-map: undo button has no sprite');
|
||||
else pass(`(e) /area-map: undo button has ${area.undoSprites} sprite(s)`);
|
||||
if (/[✕↩]/.test(area.clearText)) fail('(e) /area-map: clear button still has ✕ char');
|
||||
|
||||
// (f) No unresolved sprite refs anywhere we've visited
|
||||
await gotoSpa(page, '/map');
|
||||
const undef = await page.evaluate(async () => {
|
||||
const resp = await fetch('/icons/phosphor-sprite.svg').catch(() => null);
|
||||
if (!resp || !resp.ok) return { error: 'sprite fetch failed' };
|
||||
const text = await resp.text();
|
||||
const ids = new Set();
|
||||
for (const m of text.matchAll(/id="(ph-[a-z-]+)"/g)) ids.add(m[1]);
|
||||
const uses = Array.from(document.querySelectorAll('svg.ph-icon use'));
|
||||
const missing = [];
|
||||
for (const u of uses) {
|
||||
const href = u.getAttribute('href') || u.getAttribute('xlink:href') || '';
|
||||
const m = href.match(/#(ph-[a-z-]+)/);
|
||||
if (!m) { missing.push(href); continue; }
|
||||
if (!ids.has(m[1])) missing.push(m[1]);
|
||||
}
|
||||
return { count: uses.length, ids: ids.size, missing };
|
||||
});
|
||||
if (undef.error) fail(`(f) sprite fetch: ${undef.error}`);
|
||||
else if (undef.missing && undef.missing.length) fail(`(f) ${undef.missing.length} sprite ref(s) unresolved: ${undef.missing.slice(0,5).join(', ')}`);
|
||||
else pass(`(f) all ${undef.count} sprite refs resolve to one of ${undef.ids} defined symbols`);
|
||||
|
||||
await browser.close();
|
||||
console.log(`\ntest-issue-1648-m4-icons-e2e.js: ${passes} passed, ${failures} failed`);
|
||||
assert.strictEqual(failures, 0, `${failures} M4 icon-render assertions failed`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('test-issue-1648-m4-icons-e2e.js: FAIL —', err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user