Files
vela-calculator/tests/visual.cjs
T
codex-bot 176de6bc1f Calculator v1.6: round-screen keypad with arithmetic
- 19-key layout fitted to round display, centered focus, scrollable list
- full arithmetic: +-*/ %, decimals, +/- , AC, divide-by-zero, chained ops
- scroll-to-center focus with top/bottom spacers, high-contrast keys
- tests: arithmetic + geometry/contrast
2026-09-09 15:09:31 +01:00

55 lines
3.2 KiB
JavaScript

// Visual layout test: circle-screen geometry + contrast, computed from the .ux source.
const fs = require('node:fs');
const assert = require('node:assert/strict');
const ux = fs.readFileSync('src/pages/index/index.ux', 'utf8');
const num = (re) => { const m = ux.match(re); assert.ok(m, 'style missing: ' + re); return Number(m[1]); };
const SCREEN = 480, R = SCREEN / 2;
const dispH = num(/\.display\s*{[^}]*height:\s*(\d+)px/);
const itemH = num(/\.item\s*{[^}]*height:\s*(\d+)px/);
const itemW = num(/\.item\s*{[^}]*width:\s*(\d+)px/);
const keysH = num(/\.keys\s*{[^}]*height:\s*(\d+)px/);
const keysW = num(/\.keys\s*{[^}]*width:\s*(\d+)px/);
const N = 19;
// page is justify-content:center → column total = dispH + keysH, centered vertically
const total = dispH + keysH;
const listTop = (SCREEN - total) / 2 + dispH;
// (1) geometry: check every POSSIBLE viewport position (item i centered → viewport spans [i*itemH, i*itemH+keysH] relative to list top incl pad)
// worst-case visible row = the centered one (at circle center) + rows at viewport top/bottom edges
for (let i = 0; i < N; i++) {
// item i centered: its top y = R - itemH/2 → rows at viewport edges: y = R ± keysH/2
for (const y of [R - keysH / 2, R + keysH / 2]) {
const halfW = Math.sqrt(Math.max(0, R * R - Math.abs(y - R) ** 2));
assert.ok(itemW / 2 <= halfW, `viewport edge row at y=${y} exceeds chord: needs ${itemW / 2}px, has ${halfW.toFixed(1)}px`);
}
}
console.log(`PASS circle containment: ${itemW}px items fit chord for all visible rows (listTop=${listTop})`);
// (2) focus = item centered in viewport, and pressing auto-centers it
assert.match(ux, /e\.scrollY \+ this\.KEYS_H \/ 2 - this\.ITEM_H \/ 2/, 'onScroll must center focus in viewport');
assert.match(ux, /scrollTo\(/, 'press() must auto-scroll to keep focus centered');
console.log('PASS focus = middle of viewport, auto-centered on press');
// (3) full reachability of first AND last item (spacers PAD = keysH/2 - itemH/2)
const pad = keysH / 2 - itemH / 2;
const scrollMax = Math.max(0, N * itemH + 2 * pad - keysH);
// item i is centered when scrollY = pad + i*itemH - (keysH/2 - itemH/2) = i*itemH (with pad = keysH/2-itemH/2)
assert.ok(scrollMax >= (N - 1) * itemH, `last item cannot center: scrollMax ${scrollMax} < ${(N - 1) * itemH}`);
assert.match(ux, /class="pad top"/, 'top spacer present');
assert.match(ux, /class="pad bottom"/, 'bottom spacer present');
console.log(`PASS reachability: with ${pad}px spacers, item 0..${N - 1} all centerable (scrollMax ${scrollMax}px)`);
// (4) contrast (large-text 4.5:1 target)
const lum = (hex) => {
const c = hex.match(/\w\w/g).map(x => parseInt(x, 16) / 255).map(v => v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4));
return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
};
const ratio = (a, b) => (Math.max(lum(a), lum(b)) + 0.05) / (Math.min(lum(a), lum(b)) + 0.05);
assert.ok(ratio('#ffffff', '#3a3a3e') >= 4.5, 'normal key contrast');
assert.ok(ratio('#000000', '#f3a633') >= 4.5, 'focused key contrast');
assert.match(ux, /#3a3a3e/, 'key bg lighter than old #242426');
console.log('PASS contrast: keys ' + ratio('#ffffff', '#3a3a3e').toFixed(1) + ':1, focused ' + ratio('#000000', '#f3a633').toFixed(1) + ':1');
console.log('ALL VISUAL CHECKS PASS');