mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-06-05 21:02:54 +00:00
91d90d48fb
Red commit: 482ffe69e6 (CI: pending)
## What
Drops `max-width: 4ch` from `.mc-cluster .mc-pill` in
`public/style.css`. Keeps `overflow: hidden` + `text-overflow: ellipsis`
as belt-only graceful degradation.
## Why
#1362 added `max-width: 4ch` as defense-in-depth for the `999+` JS cap.
But `4ch` is applied to the BOX including the `1px 3px` padding, so
effective text width is ~2.5ch — enough for `R6` but not `R60`. Result:
post-merge regression on staging where multi-digit cluster pills render
`R…` instead of `R60`/`C30`.
The JS cap in `public/map.js` already clamps counts to `999+` (max 5
chars: `R999+`). That's the load-bearing safety. The CSS `max-width` was
overcaution and went too aggressive. Option A from the issue: drop the
cap entirely, keep ellipsis as graceful-degrade if JS ever fails.
## TDD red→green
- RED: `test-issue-1364-pill-no-clamp.js` asserts `.mc-pill` CSS does
NOT contain `max-width: 4ch` (regression guard) and DOES contain
`overflow: hidden` + `text-overflow: ellipsis` (graceful degradation).
Fails on the unchanged CSS.
- GREEN: deletes the `max-width: 4ch;` line from `.mc-pill`. Test
passes.
Wired into `.github/workflows/deploy.yml` alongside the #1360 test.
## Visual verification
Open `/map` zoomed-out on staging. Cluster pills must render full counts
(`R60`, `C30`, `R250`, capped `R999+`) — no `R…` ellipsis. No horizontal
scrollbar even on synthetic 4-digit injection.
Fixes #1364
---------
Co-authored-by: openclaw-bot <bot@openclaw.local>
94 lines
4.2 KiB
JavaScript
94 lines
4.2 KiB
JavaScript
/**
|
|
* #1360 — regression(map): #1357 cluster role pills lost the count number.
|
|
*
|
|
* Pill body must contain BOTH the role letter (WCAG carrier from #1356)
|
|
* AND the per-role count (the data sighted operators need at a glance).
|
|
*
|
|
* Pure-string assertions over public/map.js (mirrors #1356 test pattern).
|
|
*/
|
|
'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 mapSrc = fs.readFileSync(path.join(__dirname, 'public', 'map.js'), 'utf8');
|
|
|
|
console.log('\n=== #1360: pill body emits letter + count (not letter alone) ===');
|
|
|
|
// A. Source must concatenate letter and n (the count) into the pill body.
|
|
// Acceptable shapes: `letter + n`, `letter + String(n)`, `(letter + n)`.
|
|
const concatRe = /letter\s*\+\s*(?:String\()?\s*n\b/;
|
|
assert(concatRe.test(mapSrc),
|
|
'map.js concatenates letter + n (or letter + String(n)) for pill body');
|
|
|
|
// B. The pill body must NOT be bare `letter` followed immediately by '</span>'.
|
|
// i.e. reject `... + letter + '</span>'` with nothing in between.
|
|
const bareLetterRe = /\+\s*letter\s*\+\s*['"]<\/span>/;
|
|
assert(!bareLetterRe.test(mapSrc),
|
|
'pill body is no longer just letter (no `+ letter + "</span>"` pattern)');
|
|
|
|
// C. Simulate makeClusterIcon by exercising __meshcoreMapInternals if loadable
|
|
// in Node — fallback: pattern-check the rendered HTML template.
|
|
// map.js is browser-oriented (Leaflet IIFE) so we string-test the template.
|
|
// Build a synthetic expected pill body: a letter from R/C/M/S/O + digits.
|
|
// The assertion below validates the rendered shape via regex over the
|
|
// template's emitted output pattern.
|
|
const pillTemplateRe = /<span class="mc-pill[\s\S]{0,400}letter\s*\+\s*(?:String\()?\s*n/;
|
|
assert(pillTemplateRe.test(mapSrc),
|
|
'pill HTML template body interpolates letter + n inside the span');
|
|
|
|
// D. Letter is still the first character of the pill body (preserves #1356
|
|
// WCAG carrier ordering — assistive scanning sees the role letter first).
|
|
// The concatenation must be `letter + n`, not `n + letter`.
|
|
const reverseRe = /\bn\s*\+\s*letter\b/;
|
|
assert(!reverseRe.test(mapSrc),
|
|
'letter precedes count in concatenation (letter + n, not n + letter)');
|
|
|
|
// E. Acceptance criterion from the issue: pill body matches /^[RCMSO]\d+$/
|
|
// for non-zero counts. Verify ROLE_LETTERS maps to the expected set.
|
|
const roleLettersRe = /ROLE_LETTERS\s*=\s*\{([\s\S]*?)\}/;
|
|
const rlMatch = mapSrc.match(roleLettersRe);
|
|
assert(rlMatch, 'ROLE_LETTERS map is defined in map.js');
|
|
if (rlMatch) {
|
|
const letters = (rlMatch[1].match(/'[A-Z]'/g) || []).map(function (s) { return s[1]; });
|
|
const expected = ['R', 'C', 'M', 'S', 'O'];
|
|
const haveAll = expected.every(function (l) { return letters.indexOf(l) !== -1; });
|
|
assert(haveAll,
|
|
'ROLE_LETTERS includes R, C, M, S, O so pill body matches /^[RCMSO]\\d+$/');
|
|
}
|
|
|
|
// === #1360 follow-up: 4+ digit count overflow guard ===
|
|
console.log('\n=== #1360 follow-up: pill width bounded for 4+ digit counts ===');
|
|
|
|
// F. JS cap: makeClusterIcon must clamp counts > 999 to "999+" so pill body
|
|
// becomes e.g. "R999+" instead of "R1234" / "R10000".
|
|
const jsCapRe = /n\s*>\s*999[\s\S]{0,80}['"]999\+['"]/;
|
|
assert(jsCapRe.test(mapSrc),
|
|
'makeClusterIcon caps counts > 999 to "999+" (n > 999 → "999+")');
|
|
|
|
// G. CSS guard: .mc-pill rule must include max-width AND text-overflow:ellipsis
|
|
// as defense-in-depth in case a render slips past the JS cap.
|
|
const cssSrc = fs.readFileSync(path.join(__dirname, 'public', 'style.css'), 'utf8');
|
|
const pillRuleRe = /\.mc-cluster\s+\.mc-pill\s*\{([\s\S]*?)\}/;
|
|
const pillMatch = cssSrc.match(pillRuleRe);
|
|
assert(pillMatch, '.mc-cluster .mc-pill rule found in style.css');
|
|
if (pillMatch) {
|
|
const body = pillMatch[1];
|
|
// #1364: dropped `max-width` — it over-clamped multi-digit counts.
|
|
// Graceful-degrade ellipsis assertion stays.
|
|
assert(/text-overflow\s*:\s*ellipsis/.test(body),
|
|
'.mc-pill declares text-overflow: ellipsis (graceful clip)');
|
|
}
|
|
|
|
console.log('\n=== Summary ===');
|
|
console.log(' Passed: ' + passed);
|
|
console.log(' Failed: ' + failed);
|
|
console.log('\n#1360 ' + (failed === 0 ? 'PASS' : 'FAIL'));
|
|
process.exit(failed === 0 ? 0 : 1);
|