Initial commit: RHINE LAB · ANALYSIS OS:三维界面与动效实验(本仓库不含个人归档索引数据)
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
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 output = resolve(".tools/array-input");
|
||||
await mkdir(output, { recursive: true });
|
||||
const report = [];
|
||||
const stats = (page) => page.evaluate(() => window.rhine.stats());
|
||||
const settle = (page) => page.waitForTimeout(2200);
|
||||
try {
|
||||
for (const mobile of [false, true].filter(
|
||||
(mobile) =>
|
||||
!process.env.REVIEW_CASES ||
|
||||
process.env.REVIEW_CASES === (mobile ? "mobile" : "desktop"),
|
||||
)) {
|
||||
const width = mobile ? 390 : 1920,
|
||||
height = mobile ? 844 : 1080;
|
||||
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().ready && !document.querySelector("#loading"),
|
||||
null,
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() => window.rhine.stats().extraction >= 0.399,
|
||||
null,
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await settle(page);
|
||||
const start = await stats(page);
|
||||
const x = width * 0.55,
|
||||
y = height * 0.3;
|
||||
const laneStep = Math.max(100, Math.min(280, width * 0.24));
|
||||
const rowStep = Math.max(72, Math.min(150, height * 0.14));
|
||||
const cdp = mobile ? await context.newCDPSession(page) : null;
|
||||
let origin, projection, inspecting, pointer;
|
||||
const downRaw = async (x, y) =>
|
||||
mobile
|
||||
? cdp.send("Input.dispatchTouchEvent", {
|
||||
type: "touchStart",
|
||||
touchPoints: [{ x, y, id: 1 }],
|
||||
})
|
||||
: (await page.mouse.move(x, y), page.mouse.down());
|
||||
const moveRaw = async (x, y) =>
|
||||
mobile
|
||||
? cdp.send("Input.dispatchTouchEvent", {
|
||||
type: "touchMove",
|
||||
touchPoints: [{ x, y, id: 1 }],
|
||||
})
|
||||
: page.mouse.move(x, y);
|
||||
const down = async (px, py) => {
|
||||
await downRaw(px, py);
|
||||
const state = await stats(page);
|
||||
origin = { x: px, y: py };
|
||||
pointer = origin;
|
||||
projection = state.dragProjection;
|
||||
inspecting = state.canInspect;
|
||||
};
|
||||
// Existing behavioral cases use logical column/file distances; project them.
|
||||
const move = async (px, py) => {
|
||||
if (inspecting) return moveRaw(px, py);
|
||||
const lane = (origin.x - px) / laneStep,
|
||||
row = (origin.y - py) / rowStep;
|
||||
pointer = {
|
||||
x: origin.x + lane * projection.lane.x + row * projection.row.x,
|
||||
y: origin.y + lane * projection.lane.y + row * projection.row.y,
|
||||
};
|
||||
return moveRaw(pointer.x, pointer.y);
|
||||
};
|
||||
const up = async () =>
|
||||
mobile
|
||||
? cdp.send("Input.dispatchTouchEvent", {
|
||||
type: "touchEnd",
|
||||
touchPoints: [],
|
||||
})
|
||||
: page.mouse.up();
|
||||
await down(x, y);
|
||||
await move(x - laneStep * 0.3, y + 2);
|
||||
await page.waitForTimeout(150);
|
||||
const partial = await stats(page);
|
||||
assert.ok(partial.dragTrack);
|
||||
assert.equal(partial.selectedCell.lane, start.selectedCell.lane);
|
||||
assert.ok(
|
||||
partial.columnCamera > start.columnCamera + 1,
|
||||
"Array follows before selecting the next cell",
|
||||
);
|
||||
await move(x - laneStep * 0.8, y + 3);
|
||||
await page.waitForTimeout(150);
|
||||
assert.equal(
|
||||
(await stats(page)).selectedCell.lane,
|
||||
start.selectedCell.lane + 1,
|
||||
"Selection changes before release",
|
||||
);
|
||||
await page.screenshot({
|
||||
path: resolve(output, `${mobile ? "touch" : "mouse"}-drag.png`),
|
||||
});
|
||||
await up();
|
||||
await settle(page);
|
||||
const lane = await stats(page);
|
||||
assert.equal(lane.selectedCell.lane, start.selectedCell.lane + 1);
|
||||
assert.equal(lane.dragTrack, null);
|
||||
assert.ok(
|
||||
Math.abs(lane.columnCamera - (lane.selectedCell.lane - 2) * 5.2) < 0.03,
|
||||
);
|
||||
await down(x, y);
|
||||
await move(x + 2, y - rowStep * 0.8);
|
||||
await page.waitForTimeout(150);
|
||||
assert.equal(
|
||||
(await stats(page)).selectedCell.row,
|
||||
lane.selectedCell.row + 1,
|
||||
);
|
||||
await move(x + 4, y + rowStep * 0.8);
|
||||
await page.waitForTimeout(150);
|
||||
const reverse = await stats(page);
|
||||
assert.equal(reverse.selectedCell.lane, lane.selectedCell.lane);
|
||||
assert.equal(
|
||||
reverse.selectedCell.row,
|
||||
lane.selectedCell.row - 1,
|
||||
"Reversing mid-drag switches to previous file",
|
||||
);
|
||||
await up();
|
||||
await settle(page);
|
||||
if (!mobile) {
|
||||
// Find a visible cover using actual scene hit-testing, then hold the pointer still.
|
||||
let hit = false;
|
||||
for (const point of [
|
||||
[960, 450],
|
||||
[800, 440],
|
||||
[1100, 400],
|
||||
[600, 500],
|
||||
[1300, 400],
|
||||
]) {
|
||||
await page.mouse.move(...point);
|
||||
await page.waitForTimeout(50);
|
||||
if ((await stats(page)).hoverCell) {
|
||||
hit = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert.ok(hit, "A visible card can be hovered");
|
||||
await page.waitForTimeout(450);
|
||||
const hovered = await stats(page);
|
||||
assert.ok(
|
||||
Object.values(hovered.hoverLifts).some((v) => v > 0.27),
|
||||
"Hover raises the card",
|
||||
);
|
||||
assert.equal(
|
||||
hovered.extraction,
|
||||
0.4,
|
||||
"Hover does not change extraction or camera progress",
|
||||
);
|
||||
await page.screenshot({ path: resolve(output, "mouse-hover.png") });
|
||||
await page.mouse.down();
|
||||
await page.mouse.up();
|
||||
assert.deepEqual(
|
||||
(await stats(page)).selectedCell,
|
||||
hovered.hoverCell,
|
||||
"Click still selects the hovered physical cell",
|
||||
);
|
||||
await page.mouse.move(5, 5);
|
||||
await page.waitForTimeout(700);
|
||||
assert.equal((await stats(page)).hoverCell, null);
|
||||
assert.deepEqual((await stats(page)).hoverLifts, {});
|
||||
await page.mouse.move(x, y);
|
||||
const beforeWheel = await stats(page);
|
||||
await page.mouse.wheel(0, 100);
|
||||
await page.waitForTimeout(100);
|
||||
assert.equal(
|
||||
(await stats(page)).selectedCell.row,
|
||||
beforeWheel.selectedCell.row + 1,
|
||||
);
|
||||
await page.mouse.wheel(0, -100);
|
||||
await settle(page);
|
||||
assert.equal(
|
||||
(await stats(page)).selectedCell.row,
|
||||
beforeWheel.selectedCell.row,
|
||||
);
|
||||
await page.locator('[data-action="settings"]').click();
|
||||
await page.waitForTimeout(350);
|
||||
const modal = await stats(page);
|
||||
await page.mouse.move(x, y);
|
||||
await page.mouse.wheel(0, 400);
|
||||
await page.waitForTimeout(200);
|
||||
assert.deepEqual(
|
||||
(await stats(page)).selectedCell,
|
||||
modal.selectedCell,
|
||||
"Modal blocks array wheel navigation",
|
||||
);
|
||||
await page.locator('[data-action="close-modal"]').click();
|
||||
await page.waitForFunction(
|
||||
() => !document.querySelector(".modal-backdrop"),
|
||||
);
|
||||
}
|
||||
// Run across a complete row cycle, checking physical direction at the seam.
|
||||
const loop = await stats(page);
|
||||
for (let i = 0; i < 8; i++)
|
||||
await page.locator('[data-action="next"]').click();
|
||||
assert.equal((await stats(page)).selected, loop.selected);
|
||||
assert.equal(
|
||||
(await stats(page)).selectedCell.row,
|
||||
loop.selectedCell.row + 8,
|
||||
);
|
||||
await settle(page);
|
||||
const interrupted = await stats(page);
|
||||
await down(x, y);
|
||||
await move(x - laneStep * 0.3, y);
|
||||
await page.waitForTimeout(100);
|
||||
if (mobile) {
|
||||
await cdp.send("Input.dispatchTouchEvent", {
|
||||
type: "touchStart",
|
||||
touchPoints: [
|
||||
{ ...pointer, id: 1 },
|
||||
{ x: x + 30, y: y + 30, id: 2 },
|
||||
],
|
||||
});
|
||||
} else {
|
||||
await page.evaluate(() =>
|
||||
document.querySelector("#three-scene canvas").releasePointerCapture(1),
|
||||
);
|
||||
}
|
||||
await up();
|
||||
await settle(page);
|
||||
assert.equal(
|
||||
(await stats(page)).dragTrack,
|
||||
null,
|
||||
"Interrupted drag clears capture state",
|
||||
);
|
||||
assert.deepEqual(
|
||||
(await stats(page)).selectedCell,
|
||||
interrupted.selectedCell,
|
||||
"Interrupted partial drag does not select",
|
||||
);
|
||||
// A subsequent gesture must work after cancellation / multi-touch.
|
||||
await down(x, y);
|
||||
await move(x, y - rowStep * 0.8);
|
||||
await page.waitForTimeout(150);
|
||||
await up();
|
||||
await settle(page);
|
||||
assert.equal(
|
||||
(await stats(page)).selectedCell.row,
|
||||
interrupted.selectedCell.row + 1,
|
||||
);
|
||||
await page.locator(".read-file").click();
|
||||
await page.waitForFunction(() => window.rhine.stats().canInspect, null, {
|
||||
timeout: 30000,
|
||||
});
|
||||
const detail = await stats(page);
|
||||
await down(x, y);
|
||||
await move(x + 60, y + 5);
|
||||
await up();
|
||||
await page.waitForTimeout(500);
|
||||
assert.deepEqual(
|
||||
(await stats(page)).selectedCell,
|
||||
detail.selectedCell,
|
||||
"Detail drag only rotates",
|
||||
);
|
||||
assert.ok((await stats(page)).rotation > 0.05);
|
||||
assert.deepEqual(errors, []);
|
||||
report.push({
|
||||
mobile,
|
||||
start: start.selectedCell,
|
||||
after: reverse.selectedCell,
|
||||
checks: "passed",
|
||||
});
|
||||
console.log(`${mobile ? "Touch" : "Mouse"} input passed`);
|
||||
await context.close();
|
||||
}
|
||||
} finally {
|
||||
await writeFile(
|
||||
resolve(output, "report.json"),
|
||||
JSON.stringify(report, null, 2),
|
||||
);
|
||||
await browser.close();
|
||||
}
|
||||
Reference in New Issue
Block a user