miaoda-game-devkit 0.2.14 → 0.2.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,9 +4,11 @@
4
4
 
5
5
  该开发工具包提供:
6
6
 
7
- - React 游戏的生产/手动时钟、Strict Mode 安全 Session ownership、JSDOM
7
+ - React 游戏的生产/手动时钟、Strict Mode 安全 Controller ownership、JSDOM
8
8
  Vitest 配置和自动收集运行错误的 Testing Library setup,分别从 `miaoda-game-devkit/react`、
9
9
  `miaoda-game-devkit/react/testing` 和 `miaoda-game-devkit/react/vitest-config` 导入;
10
+ - React 的 `playthroughTest` 与项目级 reporter 自动验证生产入口、真实 DOM 输入、
11
+ 有界推进和结果断言;聚焦运行只提示未检查,完整运行缺少有效流程时失败;
10
12
 
11
13
  - React Vitest 在项目声明 Phaser 3 时使用其 browser bundle,并提供仅供模块能力探测的
12
14
  最小 Canvas 2D 边界,使 Phaser 3 代码不会阻断普通 JSDOM 测试;Storage 使用 JSDOM
@@ -62,10 +64,11 @@ Phaser 4 模板对应使用 `miaoda-phaser-game-lint`。命令类型不通过依
62
64
  模板即使预装 Phaser 3 及相关插件,也不会解析 Phaser 或运行 Phaser 4 检查。旧的
63
65
  `miaoda-game-lint` 不再发布,避免在同时包含 React 与 Phaser 依赖的项目中产生歧义。
64
66
 
65
- 开发工具包自行维护工具版本,并从自身依赖中解析可执行文件,使用方不需要重复声明
67
+ 开发工具包自行维护并精确固定 Vitest 等工具版本,从自身依赖中解析可执行文件,使用方不需要重复声明
66
68
  Biome、Oxlint 或 tsgo。仓库中保留稳定的命令入口,因此 `dist/` 尚未生成时,pnpm
67
69
  也能正确建立命令链接;工作区开发状态下由 Nx 在模板检查前构建开发工具包,发布包则
68
- 直接携带运行时 `dist/`。开发工具包内部的 `*.test.*` 与构建后的 `*.contract.mjs` 都不会
70
+ 直接携带运行时 `dist/`。打包消费者契约会验证固定 Vitest 版本以及 reporter 的通过、聚焦和
71
+ 缺失流程行为。开发工具包内部的 `*.test.*` 与构建后的 `*.contract.mjs` 都不会
69
72
  进入 npm 包;这些契约只由 devkit 自己的测试目标执行。
70
73
 
71
74
  monorepo 中的模板可以放置只包含 `extends: ["miaoda-game-devkit/biome"]` 的薄配置,
@@ -139,7 +142,9 @@ DOM 到 Phaser 的输入、命中测试和游戏状态变化,不验证 Canvas/
139
142
 
140
143
  ## 发布顺序
141
144
 
142
- 1. 在开发工具包中运行类型检查和测试。
145
+ 1. 在开发工具包中运行类型检查和测试。`pnpm test` 会额外生成经过
146
+ `clean-publish` 清理的 tarball,在隔离项目中按发布目录结构解包,并以
147
+ `NODE_ENV=production` 运行真实 React DOM 输入测试。
143
148
  2. 使用 `pnpm run release:dry-run` 检查将要发布的文件和清理后的 `package.json`。
144
149
  3. 根据版本类型运行 `pnpm run release:patch`、`release:minor` 或 `release:major`。
145
150
  发布脚本会先测试和构建,再通过 `clean-publish` 从临时目录发布;发布包会移除
@@ -115,13 +115,14 @@ function checkVitestConfig(target) {
115
115
  }
116
116
  async function runAllChecks(target) {
117
117
  const targetChecks = target === "react" ? [checkVitestConfig("react")] : [checkPhaser4Dependency(), checkPhaserViteConfig(), checkVitestConfig("phaser")];
118
+ const lintRoots = target === "react" && (0, import_node_fs.existsSync)((0, import_node_path.join)(projectRoot, "tests")) ? ["src", "tests"] : ["src"];
118
119
  const [tsgo, biome] = await Promise.all([
119
120
  run("tsgo", "@typescript/native-preview", "tsgo", ["-p", "tsconfig.json"]),
120
121
  run("biome", "@biomejs/biome", "biome", [
121
122
  "lint",
122
123
  "--config-path",
123
124
  biomeConfig,
124
- "src"
125
+ ...lintRoots
125
126
  ])
126
127
  ]);
127
128
  const tailwindOutput = (0, import_node_path.join)((0, import_node_os.tmpdir)(), `miaoda-game-devkit-${process.pid}.css`);
@@ -135,7 +136,7 @@ async function runAllChecks(target) {
135
136
  const oxlint = await run("oxlint", "oxlint", "oxlint", [
136
137
  "-c",
137
138
  (0, import_node_path.join)(devkitRoot, "oxlint-config.json"),
138
- "src"
139
+ ...lintRoots
139
140
  ]);
140
141
  return [...targetChecks, tsgo, biome, tailwind, oxlint];
141
142
  }
@@ -115,13 +115,14 @@ function checkVitestConfig(target) {
115
115
  }
116
116
  async function runAllChecks(target) {
117
117
  const targetChecks = target === "react" ? [checkVitestConfig("react")] : [checkPhaser4Dependency(), checkPhaserViteConfig(), checkVitestConfig("phaser")];
118
+ const lintRoots = target === "react" && (0, import_node_fs.existsSync)((0, import_node_path.join)(projectRoot, "tests")) ? ["src", "tests"] : ["src"];
118
119
  const [tsgo, biome] = await Promise.all([
119
120
  run("tsgo", "@typescript/native-preview", "tsgo", ["-p", "tsconfig.json"]),
120
121
  run("biome", "@biomejs/biome", "biome", [
121
122
  "lint",
122
123
  "--config-path",
123
124
  biomeConfig,
124
- "src"
125
+ ...lintRoots
125
126
  ])
126
127
  ]);
127
128
  const tailwindOutput = (0, import_node_path.join)((0, import_node_os.tmpdir)(), `miaoda-game-devkit-${process.pid}.css`);
@@ -135,7 +136,7 @@ async function runAllChecks(target) {
135
136
  const oxlint = await run("oxlint", "oxlint", "oxlint", [
136
137
  "-c",
137
138
  (0, import_node_path.join)(devkitRoot, "oxlint-config.json"),
138
- "src"
139
+ ...lintRoots
139
140
  ]);
140
141
  return [...targetChecks, tsgo, biome, tailwind, oxlint];
141
142
  }
@@ -1,13 +1,13 @@
1
1
  export { G as GameClock, a as GameFrameCallback, b as browserGameClock } from '../game-clock-suUidZdT.mjs';
2
2
 
3
- interface DisposableGameSession {
3
+ interface DisposableGameController {
4
4
  destroy(): void;
5
5
  }
6
6
  /**
7
- * 创建由当前组件拥有的 Session,并在真实卸载后销毁。
8
- * 微任务检查避免 React Strict Mode effect 探测误销毁仍在使用的 Session。
9
- * createSession 必须只创建内存状态;监听器、定时器等外部资源应在订阅或操作时启用。
7
+ * Creates a Controller owned by the current component and destroys it after a
8
+ * real unmount. The microtask check ignores React Strict Mode effect probing.
9
+ * Construction must not start external work; controls or subscriptions may do so.
10
10
  */
11
- declare function useOwnedGameSession<TSession extends DisposableGameSession>(createSession: () => TSession): TSession;
11
+ declare function useOwnedGameController<TController extends DisposableGameController>(createController: () => TController): TController;
12
12
 
13
- export { type DisposableGameSession, useOwnedGameSession };
13
+ export { type DisposableGameController, useOwnedGameController };
@@ -1,13 +1,13 @@
1
1
  export { G as GameClock, a as GameFrameCallback, b as browserGameClock } from '../game-clock-suUidZdT.js';
2
2
 
3
- interface DisposableGameSession {
3
+ interface DisposableGameController {
4
4
  destroy(): void;
5
5
  }
6
6
  /**
7
- * 创建由当前组件拥有的 Session,并在真实卸载后销毁。
8
- * 微任务检查避免 React Strict Mode effect 探测误销毁仍在使用的 Session。
9
- * createSession 必须只创建内存状态;监听器、定时器等外部资源应在订阅或操作时启用。
7
+ * Creates a Controller owned by the current component and destroys it after a
8
+ * real unmount. The microtask check ignores React Strict Mode effect probing.
9
+ * Construction must not start external work; controls or subscriptions may do so.
10
10
  */
11
- declare function useOwnedGameSession<TSession extends DisposableGameSession>(createSession: () => TSession): TSession;
11
+ declare function useOwnedGameController<TController extends DisposableGameController>(createController: () => TController): TController;
12
12
 
13
- export { type DisposableGameSession, useOwnedGameSession };
13
+ export { type DisposableGameController, useOwnedGameController };
@@ -21,7 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var react_exports = {};
22
22
  __export(react_exports, {
23
23
  browserGameClock: () => browserGameClock,
24
- useOwnedGameSession: () => useOwnedGameSession
24
+ useOwnedGameController: () => useOwnedGameController
25
25
  });
26
26
  module.exports = __toCommonJS(react_exports);
27
27
 
@@ -34,24 +34,24 @@ var browserGameClock = {
34
34
  clearTimeout: (id) => window.clearTimeout(id)
35
35
  };
36
36
 
37
- // src/react/use-owned-game-session.ts
37
+ // src/react/use-owned-game-controller.ts
38
38
  var import_react = require("react");
39
- function useOwnedGameSession(createSession) {
40
- const [session] = (0, import_react.useState)(createSession);
39
+ function useOwnedGameController(createController) {
40
+ const [controller] = (0, import_react.useState)(createController);
41
41
  const mounted = (0, import_react.useRef)(false);
42
42
  (0, import_react.useEffect)(() => {
43
43
  mounted.current = true;
44
44
  return () => {
45
45
  mounted.current = false;
46
46
  queueMicrotask(() => {
47
- if (!mounted.current) session.destroy();
47
+ if (!mounted.current) controller.destroy();
48
48
  });
49
49
  };
50
- }, [session]);
51
- return session;
50
+ }, [controller]);
51
+ return controller;
52
52
  }
53
53
  // Annotate the CommonJS export names for ESM import in node:
54
54
  0 && (module.exports = {
55
55
  browserGameClock,
56
- useOwnedGameSession
56
+ useOwnedGameController
57
57
  });
@@ -7,23 +7,23 @@ var browserGameClock = {
7
7
  clearTimeout: (id) => window.clearTimeout(id)
8
8
  };
9
9
 
10
- // src/react/use-owned-game-session.ts
10
+ // src/react/use-owned-game-controller.ts
11
11
  import { useEffect, useRef, useState } from "react";
12
- function useOwnedGameSession(createSession) {
13
- const [session] = useState(createSession);
12
+ function useOwnedGameController(createController) {
13
+ const [controller] = useState(createController);
14
14
  const mounted = useRef(false);
15
15
  useEffect(() => {
16
16
  mounted.current = true;
17
17
  return () => {
18
18
  mounted.current = false;
19
19
  queueMicrotask(() => {
20
- if (!mounted.current) session.destroy();
20
+ if (!mounted.current) controller.destroy();
21
21
  });
22
22
  };
23
- }, [session]);
24
- return session;
23
+ }, [controller]);
24
+ return controller;
25
25
  }
26
26
  export {
27
27
  browserGameClock,
28
- useOwnedGameSession
28
+ useOwnedGameController
29
29
  };
@@ -1,4 +1,6 @@
1
1
  import { G as GameClock, a as GameFrameCallback } from '../game-clock-suUidZdT.mjs';
2
+ import { RenderResult } from '@testing-library/react';
3
+ import { ReactNode } from 'react';
2
4
 
3
5
  /** 测试用确定性时钟;应用测试推进它,不直接伪造游戏状态。 */
4
6
  declare class ManualGameClock implements GameClock {
@@ -18,4 +20,86 @@ declare class ManualGameClock implements GameClock {
18
20
  pendingTimerCount(): number;
19
21
  }
20
22
 
21
- export { ManualGameClock };
23
+ /** 有界推进的通用配置,不绑定任何特定 Core、phase 或时钟实现。 */
24
+ interface StepUntilOptions {
25
+ /** 条件仍不成立时允许执行的最大生产步数,默认 120。 */
26
+ maxSteps?: number;
27
+ /** 每轮推进一单位游戏自有时间或工作;即时回合制结果可以省略。 */
28
+ step?: (step: number) => void | Promise<void>;
29
+ }
30
+
31
+ /** 单条 React 主流程测试留下的可序列化运行期证据。 */
32
+ interface ReactPlaythroughEvidence {
33
+ /** 捕获阶段观察到的键盘、指针或触摸类 DOM 输入总数。 */
34
+ domInputEvents: number;
35
+ /** 在步数上限内成功满足条件的 stepUntil 次数。 */
36
+ boundedRuns: number;
37
+ /** 最后一次 stepUntil 成功后新增的 Vitest 断言数。 */
38
+ assertionsAfterOutcome: number;
39
+ /** helper 是否已经完成全部证据校验。 */
40
+ verified: boolean;
41
+ }
42
+ /** 通过 Vitest task metadata 从 worker 传递给主线程 reporter 的数据。 */
43
+ interface ReactPlaythroughMetadata {
44
+ /** metadata 结构版本,用于拒绝无法识别的旧数据。 */
45
+ version: 1;
46
+ /** 静态、非交互项目跳过主流程验证时必须提供的理由。 */
47
+ waiverReason?: string;
48
+ /** 测试执行期间持续更新的客观证据。 */
49
+ evidence: ReactPlaythroughEvidence;
50
+ }
51
+
52
+ /** 主流程回调唯一需要学习的测试工具。 */
53
+ interface ReactPlaythroughArguments {
54
+ /** Testing Library 对生产游戏入口的渲染结果。 */
55
+ view: RenderResult;
56
+ /**
57
+ * 在硬性步数上限内等待权威结果。
58
+ * 返回后必须再断言该结果,进入中间 active/running 状态不算完成。
59
+ */
60
+ stepUntil(condition: () => boolean, options?: StepUntilOptions): Promise<number>;
61
+ }
62
+ /** 使用方编写的生产玩法步骤;既支持同步游戏,也支持异步 DOM 输入。 */
63
+ type ReactPlaythroughRun = (arguments_: ReactPlaythroughArguments) => void | Promise<void>;
64
+ /** 低配置主流程测试函数及其结构化豁免入口。 */
65
+ interface ReactPlaythroughTest {
66
+ /** 渲染生产入口并声明一条最低可玩主流程。 */
67
+ (element: ReactNode, run: ReactPlaythroughRun): void;
68
+ /** 仅允许带充分理由地豁免确实没有玩家输入的项目。 */
69
+ skip(reason: string, element: ReactNode, run: ReactPlaythroughRun): void;
70
+ }
71
+ /** reporter 从单个 Vitest TestCase 提取出的稳定审计输入。 */
72
+ interface ReactPlaythroughAuditInput {
73
+ /** 包含 describe 层级的完整测试名,仅用于诊断输出。 */
74
+ name: string;
75
+ /** Vitest 报告的最终执行状态。 */
76
+ state: "passed" | "failed" | "skipped" | "pending";
77
+ /** 只有 playthroughTest 声明的测试才具备该字段。 */
78
+ metadata?: ReactPlaythroughMetadata;
79
+ }
80
+ /** 整次 Vitest 运行的最低可玩性审计结果。 */
81
+ interface ReactPlaythroughAuditResult {
82
+ /** 是否满足有效主流程或有效豁免。 */
83
+ passed: boolean;
84
+ /** 是否依靠显式非交互理由通过,而非执行了主流程。 */
85
+ waived: boolean;
86
+ /** 可直接指导模型补测的稳定问题列表。 */
87
+ issues: string[];
88
+ }
89
+ /** React 游戏测试的最低可玩主流程入口。 */
90
+ declare const playthroughTest: ReactPlaythroughTest;
91
+ /**
92
+ * 供 Vitest reporter 与确定性契约测试共用的纯审计函数。
93
+ *
94
+ * 它只验证客观流程证据,不能理解任意游戏的胜负语义;具体结果仍由
95
+ * 使用方在 stepUntil 返回后通过权威状态或玩家可见结果断言。
96
+ */
97
+ declare function auditReactPlaythroughRun(tests: ReactPlaythroughAuditInput[]): ReactPlaythroughAuditResult;
98
+ declare module "vitest" {
99
+ interface TaskMeta {
100
+ /** 供 ReactPlaythroughReporter 汇总的可序列化主流程证据。 */
101
+ reactPlaythrough?: ReactPlaythroughMetadata;
102
+ }
103
+ }
104
+
105
+ export { ManualGameClock, type ReactPlaythroughArguments, type ReactPlaythroughAuditInput, type ReactPlaythroughAuditResult, type ReactPlaythroughEvidence, type ReactPlaythroughMetadata, type ReactPlaythroughTest, type StepUntilOptions, auditReactPlaythroughRun, playthroughTest };
@@ -1,4 +1,6 @@
1
1
  import { G as GameClock, a as GameFrameCallback } from '../game-clock-suUidZdT.js';
2
+ import { RenderResult } from '@testing-library/react';
3
+ import { ReactNode } from 'react';
2
4
 
3
5
  /** 测试用确定性时钟;应用测试推进它,不直接伪造游戏状态。 */
4
6
  declare class ManualGameClock implements GameClock {
@@ -18,4 +20,86 @@ declare class ManualGameClock implements GameClock {
18
20
  pendingTimerCount(): number;
19
21
  }
20
22
 
21
- export { ManualGameClock };
23
+ /** 有界推进的通用配置,不绑定任何特定 Core、phase 或时钟实现。 */
24
+ interface StepUntilOptions {
25
+ /** 条件仍不成立时允许执行的最大生产步数,默认 120。 */
26
+ maxSteps?: number;
27
+ /** 每轮推进一单位游戏自有时间或工作;即时回合制结果可以省略。 */
28
+ step?: (step: number) => void | Promise<void>;
29
+ }
30
+
31
+ /** 单条 React 主流程测试留下的可序列化运行期证据。 */
32
+ interface ReactPlaythroughEvidence {
33
+ /** 捕获阶段观察到的键盘、指针或触摸类 DOM 输入总数。 */
34
+ domInputEvents: number;
35
+ /** 在步数上限内成功满足条件的 stepUntil 次数。 */
36
+ boundedRuns: number;
37
+ /** 最后一次 stepUntil 成功后新增的 Vitest 断言数。 */
38
+ assertionsAfterOutcome: number;
39
+ /** helper 是否已经完成全部证据校验。 */
40
+ verified: boolean;
41
+ }
42
+ /** 通过 Vitest task metadata 从 worker 传递给主线程 reporter 的数据。 */
43
+ interface ReactPlaythroughMetadata {
44
+ /** metadata 结构版本,用于拒绝无法识别的旧数据。 */
45
+ version: 1;
46
+ /** 静态、非交互项目跳过主流程验证时必须提供的理由。 */
47
+ waiverReason?: string;
48
+ /** 测试执行期间持续更新的客观证据。 */
49
+ evidence: ReactPlaythroughEvidence;
50
+ }
51
+
52
+ /** 主流程回调唯一需要学习的测试工具。 */
53
+ interface ReactPlaythroughArguments {
54
+ /** Testing Library 对生产游戏入口的渲染结果。 */
55
+ view: RenderResult;
56
+ /**
57
+ * 在硬性步数上限内等待权威结果。
58
+ * 返回后必须再断言该结果,进入中间 active/running 状态不算完成。
59
+ */
60
+ stepUntil(condition: () => boolean, options?: StepUntilOptions): Promise<number>;
61
+ }
62
+ /** 使用方编写的生产玩法步骤;既支持同步游戏,也支持异步 DOM 输入。 */
63
+ type ReactPlaythroughRun = (arguments_: ReactPlaythroughArguments) => void | Promise<void>;
64
+ /** 低配置主流程测试函数及其结构化豁免入口。 */
65
+ interface ReactPlaythroughTest {
66
+ /** 渲染生产入口并声明一条最低可玩主流程。 */
67
+ (element: ReactNode, run: ReactPlaythroughRun): void;
68
+ /** 仅允许带充分理由地豁免确实没有玩家输入的项目。 */
69
+ skip(reason: string, element: ReactNode, run: ReactPlaythroughRun): void;
70
+ }
71
+ /** reporter 从单个 Vitest TestCase 提取出的稳定审计输入。 */
72
+ interface ReactPlaythroughAuditInput {
73
+ /** 包含 describe 层级的完整测试名,仅用于诊断输出。 */
74
+ name: string;
75
+ /** Vitest 报告的最终执行状态。 */
76
+ state: "passed" | "failed" | "skipped" | "pending";
77
+ /** 只有 playthroughTest 声明的测试才具备该字段。 */
78
+ metadata?: ReactPlaythroughMetadata;
79
+ }
80
+ /** 整次 Vitest 运行的最低可玩性审计结果。 */
81
+ interface ReactPlaythroughAuditResult {
82
+ /** 是否满足有效主流程或有效豁免。 */
83
+ passed: boolean;
84
+ /** 是否依靠显式非交互理由通过,而非执行了主流程。 */
85
+ waived: boolean;
86
+ /** 可直接指导模型补测的稳定问题列表。 */
87
+ issues: string[];
88
+ }
89
+ /** React 游戏测试的最低可玩主流程入口。 */
90
+ declare const playthroughTest: ReactPlaythroughTest;
91
+ /**
92
+ * 供 Vitest reporter 与确定性契约测试共用的纯审计函数。
93
+ *
94
+ * 它只验证客观流程证据,不能理解任意游戏的胜负语义;具体结果仍由
95
+ * 使用方在 stepUntil 返回后通过权威状态或玩家可见结果断言。
96
+ */
97
+ declare function auditReactPlaythroughRun(tests: ReactPlaythroughAuditInput[]): ReactPlaythroughAuditResult;
98
+ declare module "vitest" {
99
+ interface TaskMeta {
100
+ /** 供 ReactPlaythroughReporter 汇总的可序列化主流程证据。 */
101
+ reactPlaythrough?: ReactPlaythroughMetadata;
102
+ }
103
+ }
104
+
105
+ export { ManualGameClock, type ReactPlaythroughArguments, type ReactPlaythroughAuditInput, type ReactPlaythroughAuditResult, type ReactPlaythroughEvidence, type ReactPlaythroughMetadata, type ReactPlaythroughTest, type StepUntilOptions, auditReactPlaythroughRun, playthroughTest };
@@ -17,12 +17,16 @@ var __copyProps = (to, from, except, desc) => {
17
17
  };
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
 
20
- // src/react/manual-game-clock.ts
21
- var manual_game_clock_exports = {};
22
- __export(manual_game_clock_exports, {
23
- ManualGameClock: () => ManualGameClock
20
+ // src/react/testing.ts
21
+ var testing_exports = {};
22
+ __export(testing_exports, {
23
+ ManualGameClock: () => ManualGameClock,
24
+ auditReactPlaythroughRun: () => auditReactPlaythroughRun,
25
+ playthroughTest: () => playthroughTest
24
26
  });
25
- module.exports = __toCommonJS(manual_game_clock_exports);
27
+ module.exports = __toCommonJS(testing_exports);
28
+
29
+ // src/react/manual-game-clock.ts
26
30
  var ManualGameClock = class {
27
31
  time = 0;
28
32
  nextId = 1;
@@ -76,7 +80,170 @@ var ManualGameClock = class {
76
80
  return this.timers.size;
77
81
  }
78
82
  };
83
+
84
+ // src/react/react-playthrough.ts
85
+ var import_react2 = require("@testing-library/react");
86
+ var import_vitest = require("vitest");
87
+
88
+ // src/react/react-playthrough-core.ts
89
+ var import_react = require("@testing-library/react");
90
+ function normalizePlaythroughWaiverReason(waiverReason) {
91
+ if (waiverReason === void 0) return void 0;
92
+ const reason = waiverReason.trim();
93
+ if (reason.length < 20) {
94
+ throw new Error(
95
+ "playthroughTest.skip reason must contain at least 20 characters."
96
+ );
97
+ }
98
+ return reason;
99
+ }
100
+ async function runBoundedUntil(condition, options = {}) {
101
+ const maxSteps = options.maxSteps ?? 120;
102
+ if (!Number.isSafeInteger(maxSteps) || maxSteps < 0 || maxSteps > 1e4) {
103
+ throw new RangeError(
104
+ "stepUntil maxSteps must be a safe integer between 0 and 10000."
105
+ );
106
+ }
107
+ for (let step = 0; step <= maxSteps; step += 1) {
108
+ if (condition()) return step;
109
+ if (step < maxSteps) {
110
+ await (0, import_react.act)(async () => {
111
+ await options.step?.(step + 1);
112
+ });
113
+ }
114
+ }
115
+ throw new Error(
116
+ `Playthrough outcome was not reached within ${maxSteps} steps.`
117
+ );
118
+ }
119
+
120
+ // src/react/react-playthrough.ts
121
+ var INPUT_EVENTS = [
122
+ "click",
123
+ "keydown",
124
+ "keyup",
125
+ "pointerdown",
126
+ "pointerup",
127
+ "touchstart",
128
+ "touchend"
129
+ ];
130
+ function createMetadata(waiverReason) {
131
+ return {
132
+ version: 1,
133
+ waiverReason,
134
+ evidence: {
135
+ domInputEvents: 0,
136
+ boundedRuns: 0,
137
+ assertionsAfterOutcome: 0,
138
+ verified: false
139
+ }
140
+ };
141
+ }
142
+ function definePlaythrough(element, run, waiverReason) {
143
+ const reason = normalizePlaythroughWaiverReason(waiverReason);
144
+ const metadata = createMetadata(reason);
145
+ (0, import_vitest.test)("production game completes a bounded playthrough", {
146
+ skip: Boolean(reason),
147
+ meta: { reactPlaythrough: metadata }
148
+ }, async () => {
149
+ const evidence = metadata.evidence;
150
+ let assertionsAtOutcome;
151
+ const recordInput = () => {
152
+ evidence.domInputEvents += 1;
153
+ };
154
+ for (const event of INPUT_EVENTS) {
155
+ document.addEventListener(event, recordInput, true);
156
+ }
157
+ try {
158
+ const view = (0, import_react2.render)(element);
159
+ if (view.container.childNodes.length === 0) {
160
+ throw new Error(
161
+ "playthroughTest must render the production game entry."
162
+ );
163
+ }
164
+ await run({
165
+ view,
166
+ async stepUntil(condition, options = {}) {
167
+ const steps = await runBoundedUntil(condition, options);
168
+ if (evidence.domInputEvents === 0) {
169
+ throw new Error(
170
+ "stepUntil reached an outcome before any production DOM input."
171
+ );
172
+ }
173
+ evidence.boundedRuns += 1;
174
+ assertionsAtOutcome = import_vitest.expect.getState().assertionCalls;
175
+ return steps;
176
+ }
177
+ });
178
+ const assertionCalls = import_vitest.expect.getState().assertionCalls;
179
+ evidence.assertionsAfterOutcome = assertionsAtOutcome === void 0 ? 0 : assertionCalls - assertionsAtOutcome;
180
+ if (evidence.domInputEvents === 0) {
181
+ throw new Error(
182
+ "playthroughTest did not observe production DOM input."
183
+ );
184
+ }
185
+ if (evidence.boundedRuns === 0) {
186
+ throw new Error("playthroughTest must complete one bounded stepUntil.");
187
+ }
188
+ if (evidence.assertionsAfterOutcome === 0) {
189
+ throw new Error(
190
+ "Assert an authoritative game outcome after stepUntil returns."
191
+ );
192
+ }
193
+ evidence.verified = true;
194
+ } finally {
195
+ for (const event of INPUT_EVENTS) {
196
+ document.removeEventListener(event, recordInput, true);
197
+ }
198
+ }
199
+ });
200
+ }
201
+ var playthroughTest = Object.assign(
202
+ (element, run) => definePlaythrough(element, run),
203
+ {
204
+ skip: (reason, element, run) => definePlaythrough(element, run, reason)
205
+ }
206
+ );
207
+ function auditReactPlaythroughRun(tests) {
208
+ const declared = tests.filter((candidate) => candidate.metadata);
209
+ const valid = declared.filter(({ state, metadata }) => {
210
+ const evidence = metadata?.evidence;
211
+ return state === "passed" && evidence?.verified === true && evidence.domInputEvents > 0 && evidence.boundedRuns > 0 && evidence.assertionsAfterOutcome > 0;
212
+ });
213
+ const waivers = declared.filter(
214
+ ({ state, metadata }) => state === "skipped" && (metadata?.waiverReason?.trim().length ?? 0) >= 20
215
+ );
216
+ const issues = [];
217
+ if (declared.length === 0) {
218
+ issues.push(
219
+ "\u7F3A\u5C11\u751F\u4EA7\u6E38\u620F\u53EF\u73A9\u6027\u9A8C\u8BC1\uFF1A\u4F7F\u7528 playthroughTest \u6E32\u67D3 <App />\uFF0C\u901A\u8FC7\u771F\u5B9E DOM \u8F93\u5165\u5B8C\u6210\u4E00\u4E2A\u6709\u6B65\u6570\u4E0A\u9650\u7684\u5173\u952E\u6D41\u7A0B\u3002"
220
+ );
221
+ } else {
222
+ for (const candidate of declared) {
223
+ const isValid = valid.includes(candidate);
224
+ const isWaived = waivers.includes(candidate);
225
+ if (isValid || isWaived) continue;
226
+ if (candidate.state === "skipped") {
227
+ issues.push(
228
+ `\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u88AB\u8DF3\u8FC7\uFF0C\u4F46\u6CA1\u6709\u81F3\u5C11 20 \u4E2A\u5B57\u7B26\u7684\u660E\u786E\u7406\u7531\u3002`
229
+ );
230
+ } else if (candidate.state !== "passed") {
231
+ issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7684\u72B6\u6001\u4E3A ${candidate.state}\u3002`);
232
+ } else {
233
+ issues.push(
234
+ `\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u6CA1\u6709\u7559\u4E0B\u5B8C\u6574\u7684\u8F93\u5165\u3001\u6709\u9650\u63A8\u8FDB\u548C\u7ED3\u679C\u65AD\u8A00\u8BC1\u636E\u3002`
235
+ );
236
+ }
237
+ }
238
+ }
239
+ if (issues.length > 0) return { passed: false, waived: false, issues };
240
+ if (valid.length > 0) return { passed: true, waived: false, issues: [] };
241
+ if (waivers.length > 0) return { passed: true, waived: true, issues: [] };
242
+ return { passed: false, waived: false, issues };
243
+ }
79
244
  // Annotate the CommonJS export names for ESM import in node:
80
245
  0 && (module.exports = {
81
- ManualGameClock
246
+ ManualGameClock,
247
+ auditReactPlaythroughRun,
248
+ playthroughTest
82
249
  });