209 lines
7.1 KiB
JavaScript
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();
|
|
}
|