Files
rhinelab-ui/scripts/check-archive-diagonal.mjs
T

209 lines
7.1 KiB
JavaScript

import assert from "node:assert/strict";
import { mkdir, writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import { pathToFileURL } from "node:url";
const { chromium } = await import(
process.env.PLAYWRIGHT_MODULE
? pathToFileURL(resolve(process.env.PLAYWRIGHT_MODULE)).href
: "playwright"
);
const browser = await chromium.launch({
channel: "chrome",
headless: true,
args: ["--use-angle=d3d11", "--enable-gpu", "--ignore-gpu-blocklist"],
});
const report = [];
await mkdir(".tools/array-input", { recursive: true });
const stats = (page) => page.evaluate(() => rhine.stats());
try {
for (const [width, height, mobile] of [
[1920, 1080, false],
[390, 844, true],
[844, 390, true],
].filter(
([w, h]) =>
!process.env.REVIEW_CASES ||
process.env.REVIEW_CASES.split(",").includes(`${w}x${h}`),
)) {
const context = await browser.newContext({
viewport: { width, height },
hasTouch: mobile,
isMobile: mobile,
});
const page = await context.newPage(),
errors = [];
page.on("pageerror", (e) => errors.push(e.message));
await page.goto(
`${process.env.REVIEW_URL || "http://127.0.0.1:5204"}/?scene=archive`,
);
await page.waitForFunction(
() => window.rhine?.stats().extraction >= 0.399,
null,
{ timeout: 60000 },
);
await page.locator('[data-action="next"]').click();
await page.waitForTimeout(2200);
const cdp = mobile ? await context.newCDPSession(page) : null;
const landscape = mobile && width > height;
const x = width * (landscape ? 0.4 : 0.65),
y = height * (landscape ? 0.52 : 0.3);
const down = async () =>
mobile
? cdp.send("Input.dispatchTouchEvent", {
type: "touchStart",
touchPoints: [{ x, y, id: 1 }],
})
: (await page.mouse.move(x, y), page.mouse.down());
const move = async (px, py) =>
mobile
? cdp.send("Input.dispatchTouchEvent", {
type: "touchMove",
touchPoints: [{ x: px, y: py, id: 1 }],
})
: page.mouse.move(px, py);
const up = async () =>
mobile
? cdp.send("Input.dispatchTouchEvent", {
type: "touchEnd",
touchPoints: [],
})
: page.mouse.up();
const results = [];
for (const axis of ["lane", "row"])
for (const sign of [1, -1]) {
const before = await stats(page);
const vector = before.dragProjection[axis];
assert.ok(
Math.abs(vector.x) > 1 && Math.abs(vector.y) > 1,
"The actual camera produces diagonal axes",
);
const amount = (axis === "lane" ? 0.8 : 1.8) * sign;
await down();
const captured = await stats(page);
assert.ok(
captured.holdingArchive,
"Gesture starts in the canvas, clear of the document controls",
);
// Use the camera at pointer-down; mouse parallax can slightly change it.
const direction = captured.dragProjection[axis];
await move(x + direction.x * amount, y + direction.y * amount);
await page.waitForTimeout(160);
const during = await stats(page);
assert.equal(
during.dragMapping,
"free",
`${width}x${height}: ${axis} uses the camera projection`,
);
assert.ok(during.dragTrack);
const track = axis === "lane" ? "columnCamera" : "rail",
spacing = axis === "lane" ? 5.2 : -0.62;
assert.ok(
Math.abs((during[track] - captured[track]) / spacing - amount) < 0.04,
"Projected travel follows the requested physical distance",
);
assert.equal(
during.selectedCell[axis],
before.selectedCell[axis] + Math.round(amount),
);
if (axis === "row")
assert.equal(
during.selectedCell.lane,
before.selectedCell.lane,
"Depth drag keeps its column",
);
await up();
await page.waitForFunction(() => !rhine.stats().archiveMomentum, null, {
timeout: 12000,
});
results.push({ axis, sign, direction, mapping: during.dragMapping });
}
// One held gesture moves freely in screen space and turns without relocking.
await down();
const anchor = await stats(page),
basis = anchor.dragProjection;
const paths = [
[45, 0],
[45, 50],
[-35, 50],
[-35, -30],
[0, 0],
];
for (const [dx, dy] of paths) {
await move(x + dx, y + dy);
await page.waitForTimeout(100);
const state = await stats(page);
const lane = (state.columnCamera - anchor.columnCamera) / 5.2;
const row = (state.rail - anchor.rail) / -0.62;
assert.ok(
Math.abs(lane * basis.lane.x + row * basis.row.x - dx) < 2,
"Screen X follows the pointer",
);
assert.ok(
Math.abs(lane * basis.lane.y + row * basis.row.y - dy) < 2,
"Screen Y follows the pointer",
);
assert.equal(state.dragMapping, "free");
}
await up();
await page.waitForFunction(() => !rhine.stats().archiveMomentum);
results.push({ screenPath: paths, checks: "free turns passed" });
if (!mobile) {
await down();
const releaseBasis = (await stats(page)).dragProjection;
for (let i = 1; i <= 4; i++) {
await move(
x + ((releaseBasis.lane.x * 0.4 + releaseBasis.row.x * 1.8) * i) / 4,
y + ((releaseBasis.lane.y * 0.4 + releaseBasis.row.y * 1.8) * i) / 4,
);
await page.waitForTimeout(8);
}
await up();
const release = (await stats(page)).archiveMomentum;
assert.equal(release?.phase, "coasting");
assert.ok(release.velocity.lane > 0 && release.velocity.row > 0);
await page.waitForTimeout(250);
const coast = (await stats(page)).archiveMomentum;
assert.equal(coast?.phase, "coasting");
assert.ok(
coast.value.lane > release.value.lane &&
coast.value.row > release.value.row,
);
assert.ok(
Math.abs(
coast.velocity.lane / coast.velocity.row -
release.velocity.lane / release.velocity.row,
) < 0.001,
);
await down();
const caught = await stats(page);
assert.equal(caught.archiveMomentum, null);
await page.waitForTimeout(120);
const held = await stats(page);
assert.equal(held.columnCamera, caught.columnCamera);
assert.equal(held.rail, caught.rail);
await up();
await page.waitForTimeout(1500);
results.push({
checks: "free coast and two-track catch passed",
release,
});
}
await page.screenshot({
path: resolve(`.tools/array-input/diagonal-${width}x${height}.png`),
});
assert.deepEqual(errors, []);
report.push({ width, height, mobile, results, checks: "passed" });
console.log(
`${width}x${height}: projected axes, free screen paths and turns passed`,
);
await context.close();
}
} finally {
await mkdir(".tools/array-input", { recursive: true });
await writeFile(
".tools/array-input/diagonal.json",
JSON.stringify(report, null, 2),
);
await browser.close();
}