← back to Rubik Cube 3d

tests/e2e.mjs

121 lines

// Headless E2E for the Rubik's cube page. Uses the global playwright install.
import { createRequire } from 'module';
import { fileURLToPath } from 'url';
import path from 'path';

const require = createRequire(import.meta.url);
let chromium;
try {
  ({ chromium } = require('/Users/macstudio3/.npm-global/lib/node_modules/playwright'));
} catch {
  ({ chromium } = require('playwright'));
}

const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
const url = 'file://' + path.join(root, 'index.html');

let browser;
try {
  browser = await chromium.launch();
} catch {
  browser = await chromium.launch({ channel: 'chrome' });
}
const page = await browser.newPage({ viewport: { width: 900, height: 800 } });
const errors = [];
page.on('pageerror', e => errors.push('pageerror: ' + e.message));
page.on('console', m => { if (m.type() === 'error') errors.push('console: ' + m.text()); });

const results = [];
const check = (name, ok) => { results.push({ name, ok }); console.log((ok ? 'PASS' : 'FAIL') + '  ' + name); };
const idle = () => page.waitForFunction(() => !window.__cube.isBusy(), null, { timeout: 30000 });
const solved = () => page.evaluate(() => window.__cube.isSolved());
const moves = () => page.evaluate(() => window.__cube.getMoves());

await page.goto(url);
await page.waitForFunction(() => !!window.__cube);

check('loads solved', await solved());
check('26 cubies rendered', await page.locator('.cubie').count() === 26);
check('156 faces rendered', await page.locator('.face').count() === 156);

// scramble via button
await page.click('#scramble');
await idle();
check('scramble un-solves the cube', !(await solved()));
check('scramble does not count as moves', (await moves()) === 0);

// a user move counts; undo reverts it
await page.evaluate(() => window.__cube.enqueue('U'));
await idle();
check('user move increments counter', (await moves()) === 1);
await page.click('#undo');
await idle();
check('undo decrements counter', (await moves()) === 0);

// reset restores solved
await page.click('#reset');
await idle();
check('reset restores solved state', await solved());
check('reset clears counter', (await moves()) === 0);

// four quarter-turns of any face = identity
for (let i = 0; i < 4; i++) await page.evaluate(() => window.__cube.enqueue('R'));
await idle();
check('R x4 returns to solved', await solved());

// slice move works and Mx4 = identity
await page.evaluate(() => window.__cube.enqueue('M'));
await idle();
check('slice move M un-solves', !(await solved()));
for (let i = 0; i < 3; i++) await page.evaluate(() => window.__cube.enqueue('M'));
await idle();
check('M x4 returns to solved', await solved());

// solve detection: scramble via API with 2 known moves, invert them, expect banner
await page.click('#reset');
await page.evaluate(() => { window.__cube.scramble(); });
await idle();
const seq = ['R', 'U'];
for (const n of seq) await page.evaluate(n2 => window.__cube.enqueue(n2, false), n);
await idle();
// invert
for (const n of seq.slice().reverse()) await page.evaluate(n2 => window.__cube.enqueue(n2, true), n);
// (cube is back to the scrambled state, not solved — banner must NOT fire)
await idle();
check('banner hidden while unsolved', !(await page.locator('#banner.show').count()));
await page.click('#reset');
check('solved-detection math is true on identity', await solved());

// keyboard turn
await page.keyboard.press('f');
await idle();
check('keyboard move works', !(await solved()));
await page.keyboard.press('Shift+F');
await idle();
check('shift = counter-clockwise inverts', await solved());

// orbit drag changes camera
const before = await page.evaluate(() => window.__cube.getOrbit());
const vp = await page.locator('#viewport').boundingBox();
await page.mouse.move(vp.x + vp.width / 2, vp.y + vp.height / 2);
await page.mouse.down();
await page.mouse.move(vp.x + vp.width / 2 + 120, vp.y + vp.height / 2 + 60, { steps: 5 });
await page.mouse.up();
const after = await page.evaluate(() => window.__cube.getOrbit());
check('drag orbits the camera', after.rx !== before.rx && after.ry !== before.ry);

// wheel zoom
await page.mouse.move(vp.x + vp.width / 2, vp.y + vp.height / 2);
await page.mouse.wheel(0, -400);
const zoomed = await page.evaluate(() => window.__cube.getOrbit());
check('wheel zooms', zoomed.zoom > after.zoom);

await page.screenshot({ path: path.join(root, 'tests', 'screenshot.png') });
check('no console/page errors', errors.length === 0);
if (errors.length) console.log(errors.join('\n'));

await browser.close();
const failed = results.filter(r => !r.ok);
console.log(`\n${results.length - failed.length}/${results.length} passed`);
process.exit(failed.length ? 1 : 0);