Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ jobs:
node test-area-filter.js
node test-issue-1293-marker-shapes.js
node test-issue-1356-map-a11y.js
node test-issue-1360-pill-letter-count.js

- name: Verify proto syntax
run: |
Expand Down
5 changes: 4 additions & 1 deletion public/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -1517,11 +1517,14 @@
if (pillsShown < 4) {
var bg = ROLE_BG_VAR[role] || 'var(--mc-role-companion)';
var letter = ROLE_LETTERS[role] || '?';
// #1360 follow-up: cap 4+ digit counts as "999+" to bound pill width.
// Defense-in-depth: .mc-pill CSS also enforces max-width + ellipsis.
if (n > 999) n = '999+';
pillsHtml += '<span class="mc-pill role-' + role + '" ' +
'role="img" aria-label="' + n + ' ' + role + (n === 1 ? '' : 's') + '" ' +
'style="background:' + bg + ';color:#1a1a1a" ' +
'title="' + n + ' ' + role + (n === 1 ? '' : 's') + '">' +
letter + '</span>';
letter + n + '</span>';
pillsShown += 1;
}
}
Expand Down
9 changes: 8 additions & 1 deletion public/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -3387,6 +3387,10 @@ th.sort-active { color: var(--accent, #60a5fa); }
.mc-cluster .mc-pill {
display: inline-block; min-width: 12px; padding: 1px 3px;
border-radius: 3px;
/* #1360 follow-up: defense-in-depth cap for pathological 4+ digit counts.
JS caps the rendered text at "999+" (max 4 chars); this bounds visual
width if a stray render slips through. */
max-width: 4ch; overflow: hidden; text-overflow: ellipsis;
/* Audit: bump 9px → 10px, monospace, dark text on every Wong hue.
#1a1a1a on all 5 Wong hues passes SC 1.4.3 small-text (≥4.5:1).
Sized in rem (0.625rem = 10px @ default 16px root) so user
Expand All @@ -3395,7 +3399,10 @@ th.sort-active { color: var(--accent, #60a5fa); }
letter-spacing: 0;
color: #1a1a1a; text-align: center; text-shadow: none;
border: 1px solid rgba(0,0,0,0.25);
overflow: visible; /* SC 1.4.12 — user letter-spacing override must not clip */
/* #1360: overflow:hidden + text-overflow:ellipsis above bound the pill
when counts approach the 4-char cap ("999+"). Acceptable tradeoff vs.
SC 1.4.12 letter-spacing clipping: text content is the role letter +
<=4 digits, far short of needing aggressive letter-spacing overrides. */
}

/* V3 — multi-byte hash labels: neutral fill + colored 3px left border */
Expand Down
93 changes: 93 additions & 0 deletions test-issue-1360-pill-letter-count.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* #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];
assert(/max-width\s*:/.test(body),
'.mc-pill declares max-width (bounds pill width for overflow)');
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);