mirror of
https://github.com/Narcooo/inkos.git
synced 2026-08-28 14:57:58 +08:00
test(studio): add Playwright e2e for interactive-film player playthrough
Two tests covering real DOM click paths through StoryPlayer: - good ending path (trust >= 1 via trustup choice, conditional option visible) - bad ending path (trust = 0, conditional option hidden by count 0) Seed fixture writes story-graph.json to test-project/ before browser launch. INKOS_PROJECT_ROOT points to test-project/ so GET /api/v1/project succeeds.
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import { saveStoryGraph, StoryGraphSchema } from "@actalk/inkos-core";
|
||||
import { resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
||||
|
||||
// INKOS_PROJECT_ROOT in dev/e2e is ../../test-project (relative to packages/studio).
|
||||
// From this file (packages/studio/e2e/fixtures/seed-graph.ts) that is 4 levels up,
|
||||
// then down into test-project.
|
||||
export const E2E_PROJECT_ROOT = resolve(__dirname, "../../../..", "test-project");
|
||||
export const E2E_PROJECT_ID = "e2e-player-demo";
|
||||
|
||||
const graph = StoryGraphSchema.parse({
|
||||
schemaVersion: 1,
|
||||
projectId: E2E_PROJECT_ID,
|
||||
title: "E2E 试玩剧本",
|
||||
variables: [{ name: "trust", type: "counter", default: 0, desc: "信任" }],
|
||||
nodes: [
|
||||
{
|
||||
id: "start",
|
||||
title: "开场",
|
||||
type: "start",
|
||||
sceneDesc: "你站在宫门前。",
|
||||
choices: [
|
||||
{
|
||||
id: "trustup",
|
||||
text: "交出证据(信任+1)",
|
||||
targetNodeId: "mid",
|
||||
effects: [{ var: "trust", op: "add", value: 1 }],
|
||||
},
|
||||
{
|
||||
id: "hide",
|
||||
text: "藏起证据",
|
||||
targetNodeId: "mid",
|
||||
effects: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "mid",
|
||||
title: "抉择",
|
||||
type: "branch",
|
||||
sceneDesc: "侍卫盯着你。",
|
||||
choices: [
|
||||
{
|
||||
id: "good",
|
||||
text: "坦白",
|
||||
targetNodeId: "endGood",
|
||||
effects: [],
|
||||
condition: { var: "trust", op: ">=", value: 1 },
|
||||
},
|
||||
{
|
||||
id: "bad",
|
||||
text: "逃跑",
|
||||
targetNodeId: "endBad",
|
||||
effects: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ id: "endGood", title: "真相结局", type: "ending", choices: [] },
|
||||
{ id: "endBad", title: "逃亡结局", type: "ending", choices: [] },
|
||||
],
|
||||
endings: [
|
||||
{
|
||||
id: "g",
|
||||
nodeId: "endGood",
|
||||
title: "真相大白",
|
||||
type: "good",
|
||||
description: "你赢得了信任。",
|
||||
},
|
||||
{
|
||||
id: "b",
|
||||
nodeId: "endBad",
|
||||
title: "亡命天涯",
|
||||
type: "bad",
|
||||
description: "你消失在夜色里。",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export async function seedE2EGraph(): Promise<void> {
|
||||
await saveStoryGraph(E2E_PROJECT_ROOT, E2E_PROJECT_ID, graph);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { seedE2EGraph, E2E_PROJECT_ID } from "./fixtures/seed-graph";
|
||||
|
||||
test.beforeAll(async () => {
|
||||
await seedE2EGraph(); // 测试准备:把已知图写到磁盘
|
||||
});
|
||||
|
||||
test("用户点真实按钮把剧本玩到 good 结局,变量与结局计数正确", async ({ page }) => {
|
||||
// 1. 打开播放器页(真实导航)
|
||||
await page.goto(`/#/play/${E2E_PROJECT_ID}`);
|
||||
|
||||
// 2. 点"开始游玩"
|
||||
await page.getByTestId("player-start").click();
|
||||
|
||||
// 3. 开场:HUD 初始 trust=0
|
||||
await expect(page.getByTestId("player-node-title")).toHaveText("开场");
|
||||
await expect(page.getByTestId("hud-trust")).toHaveText("0");
|
||||
|
||||
// 4. 点"交出证据(信任+1)"——真实选项按钮
|
||||
await page.getByTestId("choice-trustup").click();
|
||||
|
||||
// 5. 到达抉择节点,HUD 变为 trust=1(断言真实渲染状态)
|
||||
await expect(page.getByTestId("player-node-title")).toHaveText("抉择");
|
||||
await expect(page.getByTestId("hud-trust")).toHaveText("1");
|
||||
|
||||
// 6. 因 trust>=1,"坦白"这个有条件选项应可见并可点
|
||||
await expect(page.getByTestId("choice-good")).toBeVisible();
|
||||
await page.getByTestId("choice-good").click();
|
||||
|
||||
// 7. 到达 good 结局,类型/标题/已解锁计数正确
|
||||
await expect(page.getByTestId("player-ending")).toBeVisible();
|
||||
await expect(page.getByTestId("player-ending-type")).toHaveText("good");
|
||||
await expect(page.getByTestId("player-ending-title")).toHaveText("真相大白");
|
||||
await expect(page.getByTestId("player-unlocked")).toContainText("1 / 2");
|
||||
});
|
||||
|
||||
test("条件选项在 trust 不足时隐藏(藏起证据路径走向 bad 结局)", async ({ page }) => {
|
||||
await page.goto(`/#/play/${E2E_PROJECT_ID}`);
|
||||
await page.getByTestId("player-start").click();
|
||||
|
||||
// 选"藏起证据"——trust 保持 0
|
||||
await page.getByTestId("choice-hide").click();
|
||||
await expect(page.getByTestId("hud-trust")).toHaveText("0");
|
||||
|
||||
// trust<1,"坦白"选项应不可见,只能"逃跑"
|
||||
await expect(page.getByTestId("choice-good")).toHaveCount(0);
|
||||
await page.getByTestId("choice-bad").click();
|
||||
|
||||
await expect(page.getByTestId("player-ending-type")).toHaveText("bad");
|
||||
});
|
||||
@@ -27,6 +27,7 @@
|
||||
"build:server": "tsc -p tsconfig.server.json",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run",
|
||||
"test:e2e": "playwright test",
|
||||
"typecheck": "tsc --noEmit && tsc -p tsconfig.server.json --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -57,6 +58,7 @@
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.61.0",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { defineConfig } from "@playwright/test";
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./e2e",
|
||||
timeout: 60_000,
|
||||
use: {
|
||||
baseURL: "http://localhost:4567",
|
||||
headless: true,
|
||||
screenshot: "only-on-failure",
|
||||
},
|
||||
// Reuse already-running dev server; start one if none is running.
|
||||
// INKOS_PROJECT_ROOT points to test-project/ which has an inkos.json,
|
||||
// so the API /api/v1/project endpoint returns valid JSON and the React
|
||||
// app can reach the ready state.
|
||||
webServer: {
|
||||
command: "INKOS_STUDIO_PORT=4569 INKOS_PROJECT_ROOT=../../test-project tsx watch --clear-screen=false src/api/index.ts & vite --host --port 4567 ; kill %1 2>/dev/null",
|
||||
url: "http://localhost:4567",
|
||||
reuseExistingServer: true,
|
||||
timeout: 120_000,
|
||||
cwd: ".",
|
||||
},
|
||||
});
|
||||
Generated
+38
@@ -179,6 +179,9 @@ importers:
|
||||
specifier: ^5.0.12
|
||||
version: 5.0.12(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))
|
||||
devDependencies:
|
||||
'@playwright/test':
|
||||
specifier: ^1.61.0
|
||||
version: 1.61.0
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.0.0
|
||||
version: 4.2.1(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0))
|
||||
@@ -1050,6 +1053,11 @@ packages:
|
||||
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
'@playwright/test@1.61.0':
|
||||
resolution: {integrity: sha512-cKA5B6lpFEMyMGjxF54QihfYpB4FkEGH+qZhtArDEG+wezQAJY8Pq6C7T1SjWz+FFzt3TbyoXBQYk/0292TdJA==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
'@protobufjs/aspromise@1.1.2':
|
||||
resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==}
|
||||
|
||||
@@ -2903,6 +2911,11 @@ packages:
|
||||
resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==}
|
||||
engines: {node: '>=14.14'}
|
||||
|
||||
fsevents@2.3.2:
|
||||
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
@@ -3983,6 +3996,16 @@ packages:
|
||||
pkg-types@1.3.1:
|
||||
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
|
||||
|
||||
playwright-core@1.61.0:
|
||||
resolution: {integrity: sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
playwright@1.61.0:
|
||||
resolution: {integrity: sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
points-on-curve@0.2.0:
|
||||
resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==}
|
||||
|
||||
@@ -5974,6 +5997,10 @@ snapshots:
|
||||
|
||||
'@opentelemetry/api@1.9.0': {}
|
||||
|
||||
'@playwright/test@1.61.0':
|
||||
dependencies:
|
||||
playwright: 1.61.0
|
||||
|
||||
'@protobufjs/aspromise@1.1.2': {}
|
||||
|
||||
'@protobufjs/base64@1.1.2': {}
|
||||
@@ -7932,6 +7959,9 @@ snapshots:
|
||||
jsonfile: 6.2.0
|
||||
universalify: 2.0.1
|
||||
|
||||
fsevents@2.3.2:
|
||||
optional: true
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
@@ -9263,6 +9293,14 @@ snapshots:
|
||||
mlly: 1.8.2
|
||||
pathe: 2.0.3
|
||||
|
||||
playwright-core@1.61.0: {}
|
||||
|
||||
playwright@1.61.0:
|
||||
dependencies:
|
||||
playwright-core: 1.61.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
|
||||
points-on-curve@0.2.0: {}
|
||||
|
||||
points-on-path@0.2.1:
|
||||
|
||||
Reference in New Issue
Block a user