with data-sort-key)', () => {
+ const src = fs.readFileSync(__dirname + '/public/packets.js', 'utf8');
+ assert.ok(src.includes('renderPathSymbolsLegend'),
+ 'packets.js must invoke HopDisplay.renderPathSymbolsLegend()');
+ assert.ok(src.includes('path-symbols-legend-wrapper'),
+ 'packets.js must wrap the legend in .path-symbols-legend-wrapper (sibling, not inside
)');
+ // The legend invocation must NOT be inside a
...
with data-sort-key.
+ // Simple structural check: in any line that contains renderPathSymbolsLegend,
+ // we must NOT see "data-sort-key" on that same line.
+ src.split('\n').forEach((line, i) => {
+ if (line.includes('renderPathSymbolsLegend') && line.includes('data-sort-key')) {
+ throw new Error('packets.js line ' + (i+1) + ' places legend inside a sortable
— will clobber the sort handler: ' + line.trim());
+ }
+ });
+ });
+
+ test('#1504: nodes.js places legend in a sibling wrapper (NOT inside
)', () => {
+ const src = fs.readFileSync(__dirname + '/public/nodes.js', 'utf8');
+ assert.ok(src.includes('path-symbols-legend-wrapper'),
+ 'nodes.js must wrap the legend in .path-symbols-legend-wrapper (sibling, not inside
)');
+ src.split('\n').forEach((line, i) => {
+ // line must not contain BOTH
]/.test(line)) {
+ throw new Error('nodes.js line ' + (i+1) + ' still embeds legend inside
: ' + line.trim());
+ }
+ });
+ });
+
+ test('#1504: style.css gives .path-symbols-legend position:relative so absolutely-positioned panel anchors correctly', () => {
+ const css = fs.readFileSync(__dirname + '/public/style.css', 'utf8');
+ // Find the rule block for .path-symbols-legend (NOT .path-symbols-legend-wrapper)
+ const m = css.match(/\.path-symbols-legend\s*\{[^}]*\}/);
+ assert.ok(m, '.path-symbols-legend rule must exist');
+ assert.ok(/position\s*:\s*relative/.test(m[0]),
+ '.path-symbols-legend must declare position:relative so .path-legend-list (position:absolute) anchors to it, not a random ancestor. Block was: ' + m[0]);
+ // And the panel must still be position:absolute
+ const panel = css.match(/\.path-symbols-legend\s+\.path-legend-list\s*\{[^}]*\}/);
+ assert.ok(panel && /position\s*:\s*absolute/.test(panel[0]),
+ '.path-symbols-legend .path-legend-list must remain position:absolute');
+ });
+
+ test('#1504: legend glyphs match what hop-display.js actually renders (no documented-but-missing glyphs)', () => {
+ const hopSrc = fs.readFileSync(__dirname + '/public/hop-display.js', 'utf8');
+ HD.PATH_SYMBOLS_LEGEND.forEach(entry => {
+ const g = entry.glyph;
+ if (g === 'dashed underline') {
+ // documented as a CSS convention; class hop-ambiguous uses border-bottom: dashed
+ assert.ok(/hop-ambiguous|hop-global-fallback/.test(hopSrc),
+ 'legend mentions "dashed underline" but hop-display.js has no ambiguous/global-fallback class');
+ return;
+ }
+ if (g === '⚠N') {
+ // Real template literal in hop-display.js: ⚠${badgeCount}
+ assert.ok(hopSrc.includes('⚠${badgeCount}') || hopSrc.includes('\u26a0${badgeCount}'),
+ 'legend documents ⚠N but hop-display.js does not emit ⚠${badgeCount}');
+ return;
+ }
+ // Otherwise the literal glyph must appear in the file
+ assert.ok(hopSrc.includes(g),
+ 'legend glyph ' + JSON.stringify(g) + ' (codepoints ' +
+ [...g].map(c => 'U+' + c.codePointAt(0).toString(16).toUpperCase()).join(',') +
+ ') not found in hop-display.js');
+ });
+ });
+
+ test('#1504 regression: clicking in legend does NOT trigger column-sort handler', () => {
+ // Simulate the structural guarantee from the wrapper move: legend must not be a descendant of a sortable
.
+ // table-sort.js binds click on th[data-sort-key]; with the legend in a sibling div, no click on summary
+ // can bubble to a sortable th.
+ const pktSrc = fs.readFileSync(__dirname + '/public/packets.js', 'utf8');
+ // Extract the snippet around the table head and confirm the legend is OUTSIDE ...
+ const theadIdx = pktSrc.indexOf('');
+ const theadEnd = pktSrc.indexOf('', theadIdx);
+ const tbodyEnd = pktSrc.indexOf('
', theadEnd);
+ assert.ok(theadIdx > 0 && theadEnd > theadIdx, 'must find thead boundaries');
+ const insideThead = pktSrc.slice(theadIdx, theadEnd);
+ assert.ok(!insideThead.includes('renderPathSymbolsLegend'),
+ 'renderPathSymbolsLegend must NOT appear inside — would clobber sort handler');
+ // Also: legend must be outside the entire
(sibling)
+ const insideTable = pktSrc.slice(theadIdx, tbodyEnd);
+ assert.ok(!insideTable.includes('renderPathSymbolsLegend'),
+ 'legend must be sibling of