33d5d55c33
Per Codex's Phase 2 audit notes. Moved the drag-snap (lineFrom) and find-match (matchWord) logic into $lib/wordsearch.js and added vitest coverage: - lineFrom always yields a straight, in-bounds path — a non-straight drag snaps, never returns bent; single cell and edge-clamping covered. - matchWord matches forward + reversed selections, is a harmless no-op on an already-found word (so completion/best-time can't double-record), and returns null for non-words / too-short selections. Restore behaviour audited: finish() (which records best-time) only runs when the final word is found mid-play; on refresh, restore() repopulates found cells + time and the derived status flips to done WITHOUT calling finish(), so best-time never re-records. First JS test runner for the frontend (npm test → vitest run). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
55 lines
2.2 KiB
JavaScript
55 lines
2.2 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
|
import { lineFrom, matchWord } from './wordsearch.js';
|
|
|
|
const colinear = (cells) => {
|
|
if (cells.length < 2) return true;
|
|
const [r0, c0] = cells[0], [r1, c1] = cells[1];
|
|
const sr = Math.sign(r1 - r0), sc = Math.sign(c1 - c0);
|
|
return cells.every(([r, c], i) => r === r0 + sr * i && c === c0 + sc * i);
|
|
};
|
|
|
|
describe('lineFrom', () => {
|
|
it('builds straight horizontal / vertical / diagonal runs', () => {
|
|
expect(lineFrom([2, 1], [2, 4], 10)).toEqual([[2, 1], [2, 2], [2, 3], [2, 4]]);
|
|
expect(lineFrom([1, 3], [4, 3], 10)).toEqual([[1, 3], [2, 3], [3, 3], [4, 3]]);
|
|
expect(lineFrom([0, 0], [3, 3], 10)).toEqual([[0, 0], [1, 1], [2, 2], [3, 3]]);
|
|
});
|
|
|
|
it('snaps a non-straight drag onto a straight line (never bent)', () => {
|
|
// knight-ish move (dr=1, dc=3) → should snap, and stay colinear
|
|
const cells = lineFrom([5, 2], [6, 8], 10);
|
|
expect(colinear(cells)).toBe(true);
|
|
// a near-diagonal still resolves to a straight path
|
|
expect(colinear(lineFrom([0, 0], [4, 5], 10))).toBe(true);
|
|
});
|
|
|
|
it('clamps to the grid and handles a single cell', () => {
|
|
expect(lineFrom([0, 0], [0, 0], 10)).toEqual([[0, 0]]);
|
|
const cells = lineFrom([8, 8], [20, 8], 10); // runs off the bottom edge
|
|
expect(cells.every(([r, c]) => r >= 0 && r < 10 && c >= 0 && c < 10)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('matchWord', () => {
|
|
const grid = ['CALM', 'XXXX', 'XXXX', 'XXXX']; // CALM across row 0
|
|
const words = ['CALM', 'PEACE'];
|
|
|
|
it('matches a forward word', () => {
|
|
expect(matchWord([[0, 0], [0, 1], [0, 2], [0, 3]], grid, words, [])).toBe('CALM');
|
|
});
|
|
|
|
it('matches a reversed selection to the listed word', () => {
|
|
expect(matchWord([[0, 3], [0, 2], [0, 1], [0, 0]], grid, words, [])).toBe('CALM');
|
|
});
|
|
|
|
it('is harmless on an already-found word (no re-record)', () => {
|
|
expect(matchWord([[0, 0], [0, 1], [0, 2], [0, 3]], grid, words, ['CALM'])).toBe(null);
|
|
});
|
|
|
|
it('returns null for a non-word or too-short selection', () => {
|
|
expect(matchWord([[1, 0], [1, 1], [1, 2], [1, 3]], grid, words, [])).toBe(null); // XXXX
|
|
expect(matchWord([[0, 0]], grid, words, [])).toBe(null);
|
|
expect(matchWord([], grid, words, [])).toBe(null);
|
|
});
|
|
});
|