Files
healer-man/scripts/playtest-mouse-camera.cjs
2026-09-16 20:58:10 -04:00

181 lines
7.5 KiB
JavaScript

const assert = require("node:assert/strict");
const crypto = require("node:crypto");
const fs = require("node:fs");
const path = require("node:path");
const { chromium } = require("C:/Users/phenom/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright");
const base = process.env.MOUSE_CAMERA_TEST_URL || "http://127.0.0.1:5176";
const directory = path.resolve("playtest-artifacts/mouse-camera");
fs.mkdirSync(directory, { recursive: true });
(async () => {
const browser = await chromium.launch({ channel: "msedge", headless: true });
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
page.setDefaultTimeout(120_000);
const errors = [];
page.on("pageerror", (error) => errors.push(error.message));
const canvasHash = async () => {
const canvas = page.locator("canvas");
await canvas.evaluate((element) => {
const hidden = [];
let current = element;
while (current.parentElement) {
for (const sibling of current.parentElement.children) {
if (sibling === current || !(sibling instanceof HTMLElement)) continue;
hidden.push([sibling, sibling.style.visibility]);
sibling.style.visibility = "hidden";
}
current = current.parentElement;
}
window.__restoreCanvasCapture = () => {
for (const [node, visibility] of hidden) node.style.visibility = visibility;
delete window.__restoreCanvasCapture;
};
});
try {
return crypto.createHash("sha256").update(await canvas.screenshot()).digest("hex");
} finally {
await page.evaluate(() => window.__restoreCanvasCapture?.());
}
};
const gameState = () => page.evaluate(async () => {
const storeEntry = performance.getEntriesByType("resource")
.findLast((entry) => new URL(entry.name).pathname === "/src/game/store.ts");
if (!storeEntry) throw new Error("game store module was not loaded");
const { useGameStore } = await import(storeEntry.name);
const state = useGameStore.getState();
return {
cameraLookActive: state.cameraLookActive,
cameraYaw: state.cameraYaw,
pointerLocked: document.pointerLockElement !== null,
selection: window.getSelection()?.toString() ?? "",
};
});
try {
await page.goto(base, { waitUntil: "domcontentloaded" });
await page.getByRole("button", { name: /Play offline/ }).click();
await page.getByRole("button", { name: /Create character/ }).click();
await page.getByRole("button", { name: /Customize & name/ }).click();
await page.getByLabel("Character name").fill("Mouselooker");
await page.getByRole("button", { name: /Create character/ }).click();
await page.getByRole("button", { name: /Continue|Enter world/ }).first().click();
await page.locator(".main-menu-screen").waitFor();
await page.getByRole("button", { name: /^Dungeons/ }).click();
await page.getByRole("button", { name: /Choose role/ }).click();
await page.locator(".dungeon-role-option").first().click();
await page.locator(".game-shell").waitFor();
await page.waitForFunction(async () => {
const storeEntry = performance.getEntriesByType("resource")
.findLast((entry) => new URL(entry.name).pathname === "/src/game/store.ts");
if (!storeEntry) return false;
const { useGameStore } = await import(storeEntry.name);
return ["ready", "proxy", "error"].includes(useGameStore.getState().assetStatus);
});
await page.locator(".loading-screen").waitFor({ state: "hidden" });
await page.waitForTimeout(1_000);
const selectionBefore = await page.evaluate(() => {
const fixture = document.createElement("p");
fixture.textContent = "Existing browser selection";
fixture.style.userSelect = "text";
fixture.style.position = "fixed";
fixture.style.left = "-10000px";
document.body.append(fixture);
const range = document.createRange();
range.selectNodeContents(fixture);
const selection = window.getSelection();
if (!selection) throw new Error("browser selection API is unavailable");
selection.removeAllRanges();
selection.addRange(range);
return selection.toString();
});
assert.equal(selectionBefore, "Existing browser selection");
const scene = page.locator(".scene-shell");
const bounds = await scene.boundingBox();
assert(bounds, "3D scene is visible");
const cdp = await page.context().newCDPSession(page);
const start = { x: bounds.x + bounds.width * 0.55, y: bounds.y + bounds.height * 0.55 };
const before = await gameState();
const frameBefore = await canvasHash();
assert.equal(before.cameraLookActive, false);
await page.mouse.move(start.x, start.y);
await page.mouse.down({ button: "right" });
await page.waitForFunction(async () => {
const storeEntry = performance.getEntriesByType("resource")
.findLast((entry) => new URL(entry.name).pathname === "/src/game/store.ts");
if (!storeEntry) return false;
const { useGameStore } = await import(storeEntry.name);
return useGameStore.getState().cameraLookActive;
});
const captured = await gameState();
assert.equal(captured.pointerLocked, false);
assert.equal(captured.selection, "");
await cdp.send("Input.dispatchMouseEvent", {
type: "mouseMoved",
x: start.x + 160,
y: start.y - 55,
button: "right",
buttons: 2,
pointerType: "mouse",
});
await page.waitForTimeout(350);
const moved = await gameState();
assert.notEqual(moved.cameraYaw, before.cameraYaw);
assert.notEqual(
await canvasHash(),
frameBefore,
"regular dungeon camera input redraws the WebGL scene independently from its HUD and nameplates",
);
await page.screenshot({ path: path.join(directory, "right-drag-camera.png") });
await page.mouse.up({ button: "right" });
await page.waitForFunction(async () => {
const storeEntry = performance.getEntriesByType("resource")
.findLast((entry) => new URL(entry.name).pathname === "/src/game/store.ts");
if (!storeEntry) return false;
const { useGameStore } = await import(storeEntry.name);
return !useGameStore.getState().cameraLookActive;
});
const released = await gameState();
await page.mouse.move(start.x - 120, start.y + 70, { steps: 8 });
await page.waitForTimeout(350);
const afterReleaseMove = await gameState();
assert.equal(afterReleaseMove.cameraYaw, released.cameraYaw);
assert.equal(afterReleaseMove.pointerLocked, false);
assert.equal(
await page.locator(".game-shell").evaluate((element) => getComputedStyle(element).userSelect),
"none",
);
assert.deepEqual(errors, []);
const result = {
passed: true,
yawBefore: before.cameraYaw,
yawDuringDrag: moved.cameraYaw,
yawAfterRelease: afterReleaseMove.cameraYaw,
pointerLocked: afterReleaseMove.pointerLocked,
selectionCleared: captured.selection === "",
errors,
};
fs.writeFileSync(path.join(directory, "results.json"), `${JSON.stringify(result, null, 2)}\n`);
console.log(JSON.stringify(result, null, 2));
console.log("Mouse camera playtest passed");
} catch (error) {
await page.mouse.up({ button: "right" }).catch(() => {});
await page.screenshot({ path: path.join(directory, "failure.png") });
console.log((await page.locator("body").innerText()).slice(-2_000));
console.log("Browser errors:", errors);
throw error;
} finally {
await browser.close();
}
})().catch((error) => {
console.error(error);
process.exitCode = 1;
});