Files
ECC/tests/lib/control-plane-view-ui.test.js
T
pasmudandCommandCodeBot 3052732f28 fix(control-pane): add non-color risk cues to the control-plane view
The control-plane projection encoded risk with hue alone. The clear
green #3fb950 and the resolution red #ff7b72 differ by only 1.01:1 in
relative luminance, so a red-green color-blind operator saw the same
grey for "clear" and "steer now".

Risk now carries a second, non-color channel, matching the pattern
merged for the sibling airspace view in #3193:

- markers are drawn as a circle, square, or triangle per risk level
- the legend shows the matching glyph instead of color-only dots
- the Lanes panel prints the level name next to the percentage
- the canvas gets role="img" and a live aria-label describing counts

The clear green also moves to #2ea043 so it stays separable from the
resolution red by luminance (1.34:1) while keeping 5.73:1 against the
#0b0e14 background.

Adds tests/lib/control-plane-view-ui-a11y.test.js covering the roles,
the shapes, the legend, and the contrast ratios. The existing test
harness needed a setAttribute stub for the new aria-label update.

Co-Authored-By: CommandCodeBot <noreply@commandcode.ai>
2026-09-26 22:52:34 +00:00

61 lines
2.7 KiB
JavaScript

'use strict';
const assert = require('assert');
const vm = require('vm');
const { renderControlPlaneViewHtml } = require('../../scripts/lib/control-pane/control-plane-view-ui');
const { renderProximityVizHtml } = require('../../scripts/lib/control-pane/proximity-viz');
const controlPlaneHtml = renderControlPlaneViewHtml();
assert.ok(controlPlaneHtml.includes('grid-template-rows: minmax(0, 1fr)'));
assert.ok(controlPlaneHtml.includes('#stage { position: relative; height: 100%; min-height: 0;'));
const proximityHtml = renderProximityVizHtml();
assert.ok(proximityHtml.includes('grid-template-rows: minmax(0, 1fr)'));
assert.ok(proximityHtml.includes('#stage { position: relative; height: 100%; min-height: 0;'));
async function renderResponse(ok, data) {
const elements = new Map();
const context = new Proxy({}, { get: () => () => {} });
function element() {
return { textContent: '', style: {}, appendChild() {}, setAttribute() {}, getContext: () => context,
clientWidth: 640, clientHeight: 480,
parentElement: { getBoundingClientRect: () => ({ width: 640, height: 480 }) } };
}
const document = {
getElementById(id) { if (!elements.has(id)) elements.set(id, element()); return elements.get(id); },
createElement: element
};
const html = renderControlPlaneViewHtml();
const start = html.indexOf('<script>');
const end = html.indexOf('</script>', start);
assert.ok(start >= 0 && end > start, 'fixed renderer template must contain its inline script');
const code = html.slice(start + '<script>'.length, end);
vm.runInNewContext(code, {
document, window: { addEventListener() {}, devicePixelRatio: 1 }, setInterval() {},
fetch: async () => ({ ok, json: async () => data })
});
await new Promise(resolve => setImmediate(resolve));
return elements;
}
let passed = 0;
(async () => {
const failed = await renderResponse(false, { ok: false, error: 'snapshot unavailable' });
assert.strictEqual(failed.get('status').textContent, 'offline', 'HTTP errors must not display a healthy empty view');
passed += 1;
const malformed = await renderResponse(true, { schemaVersion: 'wrong' });
assert.strictEqual(malformed.get('status').textContent, 'offline', 'invalid schemas must be rejected');
passed += 1;
const valid = await renderResponse(true, {
schemaVersion: 'ecc.control-plane.view.v1', tasks: [], lanes: [], pairs: [], events: [],
projection: { agents: [] }, thresholds: { ta: 0.35, ra: 0.7 }, counts: {}
});
assert.ok(valid.get('status').textContent.includes('0 tasks'));
passed += 1;
console.log(`Results: Passed: ${passed}, Failed: 0`);
})().catch(error => {
console.error(error.message);
console.log(`Results: Passed: ${passed}, Failed: 1`);
process.exitCode = 1;
});