Extract + unit-test the padding-aware cell geometry (Codex nice-to-have)

Pulled the pointer→cell math out of cellAt() into a pure cellFromPoint(rect, x,
y, n) in $lib/wordsearch.js (only getBoundingClientRect stays in the component),
and covered it with vitest — including the last-column case that was drifting
under the old overflowing layout, plus clamping and a scrolled-origin rect.
11 vitest tests now; real-device testing remains the final validator.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-10 21:43:28 -04:00
parent b909b7e64b
commit 98441fae15
3 changed files with 42 additions and 8 deletions
@@ -1,6 +1,6 @@
<script>
import { getJSON } from '$lib/api.js';
import { lineFrom, matchWord } from '$lib/wordsearch.js';
import { lineFrom, matchWord, cellFromPoint } from '$lib/wordsearch.js';
let { size = 'med', onstatus } = $props();
@@ -86,12 +86,7 @@
function summary() { return { game: 'wordsearch', size, date, status, found: foundWords.length, total: words.length, ms: resultMs }; }
function cellAt(e) {
const rect = gridEl.getBoundingClientRect();
const pad = 7; // grid padding (6) + border (1)
const cw = (rect.width - 2 * pad) / n; // even 1fr columns share the inner width
const c = Math.min(n - 1, Math.max(0, Math.floor((e.clientX - rect.left - pad) / cw)));
const r = Math.min(n - 1, Math.max(0, Math.floor((e.clientY - rect.top - pad) / cw)));
return [r, c];
return cellFromPoint(gridEl.getBoundingClientRect(), e.clientX, e.clientY, n);
}
function down(e) {
+13
View File
@@ -28,6 +28,19 @@ export function lineFrom(start, end, size) {
return cells.length ? cells : [start];
}
/**
* Map a pointer position to a grid cell. Pure geometry (the DOM rect is passed
* in) so it's testable: `rect` is {left, top, width}, cells share the inner width
* (grid width minus padding/border) evenly, and the result is clamped to range.
* @returns {[number,number]} [row, col]
*/
export function cellFromPoint(rect, clientX, clientY, n, pad = 7) {
const cw = (rect.width - 2 * pad) / n;
const c = Math.min(n - 1, Math.max(0, Math.floor((clientX - rect.left - pad) / cw)));
const r = Math.min(n - 1, Math.max(0, Math.floor((clientY - rect.top - pad) / cw)));
return [r, c];
}
/**
* Read the letters along `cells` off `grid` (array of row strings) and return
* the matching not-yet-found word (forward or reversed), or null. Re-selecting
+27 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { lineFrom, matchWord } from './wordsearch.js';
import { lineFrom, matchWord, cellFromPoint } from './wordsearch.js';
const colinear = (cells) => {
if (cells.length < 2) return true;
@@ -30,6 +30,32 @@ describe('lineFrom', () => {
});
});
describe('cellFromPoint', () => {
// pad=7; inner = 462-14 = 448; with n=14 → cw = 32px per column.
const rect = { left: 0, top: 0, width: 462 };
const at = (x, y) => cellFromPoint(rect, x, y, 14);
const colCentre = (col) => 7 + col * 32 + 16; // pad + col*cw + half a cell
it('maps a cell centre to that cell', () => {
expect(at(colCentre(0), colCentre(0))).toEqual([0, 0]);
expect(at(colCentre(7), colCentre(3))).toEqual([3, 7]);
});
it('maps the LAST column correctly (the overflow regression)', () => {
expect(at(colCentre(13), colCentre(13))).toEqual([13, 13]); // not 12, not off-grid
});
it('clamps points outside the grid to the nearest cell', () => {
expect(at(-50, -50)).toEqual([0, 0]);
expect(at(9999, 9999)).toEqual([13, 13]);
});
it('respects a non-zero rect origin (scrolled page)', () => {
const r2 = { left: 100, top: 200, width: 462 };
expect(cellFromPoint(r2, 100 + colCentre(5), 200 + colCentre(2), 14)).toEqual([2, 5]);
});
});
describe('matchWord', () => {
const grid = ['CALM', 'XXXX', 'XXXX', 'XXXX']; // CALM across row 0
const words = ['CALM', 'PEACE'];