@quake2ts/test-utils 0.0.1

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.
Files changed (65) hide show
  1. package/README.md +454 -0
  2. package/dist/index.cjs +5432 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +2150 -0
  5. package/dist/index.d.ts +2150 -0
  6. package/dist/index.js +5165 -0
  7. package/dist/index.js.map +1 -0
  8. package/package.json +82 -0
  9. package/src/client/helpers/hud.ts +114 -0
  10. package/src/client/helpers/prediction.ts +136 -0
  11. package/src/client/helpers/view.ts +201 -0
  12. package/src/client/mocks/console.ts +75 -0
  13. package/src/client/mocks/download.ts +48 -0
  14. package/src/client/mocks/input.ts +246 -0
  15. package/src/client/mocks/network.ts +148 -0
  16. package/src/client/mocks/state.ts +148 -0
  17. package/src/e2e/network.ts +47 -0
  18. package/src/e2e/playwright.ts +90 -0
  19. package/src/e2e/visual.ts +172 -0
  20. package/src/engine/helpers/pipeline-test-template.ts +113 -0
  21. package/src/engine/helpers/webgpu-rendering.ts +251 -0
  22. package/src/engine/mocks/assets.ts +129 -0
  23. package/src/engine/mocks/audio.ts +152 -0
  24. package/src/engine/mocks/buffers.ts +88 -0
  25. package/src/engine/mocks/lighting.ts +64 -0
  26. package/src/engine/mocks/particles.ts +76 -0
  27. package/src/engine/mocks/renderer.ts +218 -0
  28. package/src/engine/mocks/webgl.ts +267 -0
  29. package/src/engine/mocks/webgpu.ts +262 -0
  30. package/src/engine/rendering.ts +103 -0
  31. package/src/game/factories.ts +204 -0
  32. package/src/game/helpers/physics.ts +171 -0
  33. package/src/game/helpers/save.ts +232 -0
  34. package/src/game/helpers.ts +310 -0
  35. package/src/game/mocks/ai.ts +67 -0
  36. package/src/game/mocks/combat.ts +61 -0
  37. package/src/game/mocks/items.ts +166 -0
  38. package/src/game/mocks.ts +105 -0
  39. package/src/index.ts +93 -0
  40. package/src/server/helpers/bandwidth.ts +127 -0
  41. package/src/server/helpers/multiplayer.ts +158 -0
  42. package/src/server/helpers/snapshot.ts +241 -0
  43. package/src/server/mockNetDriver.ts +106 -0
  44. package/src/server/mockTransport.ts +50 -0
  45. package/src/server/mocks/commands.ts +93 -0
  46. package/src/server/mocks/connection.ts +139 -0
  47. package/src/server/mocks/master.ts +97 -0
  48. package/src/server/mocks/physics.ts +32 -0
  49. package/src/server/mocks/state.ts +162 -0
  50. package/src/server/mocks/transport.ts +161 -0
  51. package/src/setup/audio.ts +118 -0
  52. package/src/setup/browser.ts +249 -0
  53. package/src/setup/canvas.ts +142 -0
  54. package/src/setup/node.ts +21 -0
  55. package/src/setup/storage.ts +60 -0
  56. package/src/setup/timing.ts +142 -0
  57. package/src/setup/webgl.ts +8 -0
  58. package/src/setup/webgpu.ts +113 -0
  59. package/src/shared/bsp.ts +145 -0
  60. package/src/shared/collision.ts +64 -0
  61. package/src/shared/factories.ts +88 -0
  62. package/src/shared/math.ts +65 -0
  63. package/src/shared/mocks.ts +243 -0
  64. package/src/shared/pak-loader.ts +45 -0
  65. package/src/visual/snapshots.ts +292 -0
@@ -0,0 +1,246 @@
1
+
2
+ /**
3
+ * Client Input System Mocks
4
+ *
5
+ * Provides utilities for simulating user input (keyboard, mouse, pointer lock)
6
+ * in a test environment (JSDOM/Browser).
7
+ */
8
+
9
+ export interface KeyModifiers {
10
+ ctrl?: boolean;
11
+ alt?: boolean;
12
+ shift?: boolean;
13
+ meta?: boolean;
14
+ }
15
+
16
+ /**
17
+ * Factory for creating a mock KeyboardEvent.
18
+ */
19
+ export function createMockKeyboardEvent(key: string, type: 'keydown' | 'keyup' = 'keydown', modifiers: KeyModifiers = {}): KeyboardEvent {
20
+ return new KeyboardEvent(type, {
21
+ key,
22
+ code: key, // Default code to key if not specified (caller can override property if needed)
23
+ ctrlKey: modifiers.ctrl,
24
+ altKey: modifiers.alt,
25
+ shiftKey: modifiers.shift,
26
+ metaKey: modifiers.meta,
27
+ bubbles: true,
28
+ cancelable: true,
29
+ view: window
30
+ });
31
+ }
32
+
33
+ /**
34
+ * Factory for creating a mock MouseEvent with support for movementX/Y.
35
+ */
36
+ export function createMockMouseEvent(type: string, options: MouseEventInit = {}): MouseEvent {
37
+ const event = new MouseEvent(type, {
38
+ bubbles: true,
39
+ cancelable: true,
40
+ view: window,
41
+ ...options
42
+ });
43
+
44
+ // Patch movementX/Y if needed as JSDOM MouseEvent might not initialize them from options
45
+ if (options.movementX !== undefined) {
46
+ Object.defineProperty(event, 'movementX', { value: options.movementX });
47
+ }
48
+ if (options.movementY !== undefined) {
49
+ Object.defineProperty(event, 'movementY', { value: options.movementY });
50
+ }
51
+
52
+ return event;
53
+ }
54
+
55
+ /**
56
+ * Factory for creating a mock WheelEvent.
57
+ */
58
+ export function createMockWheelEvent(deltaX: number = 0, deltaY: number = 0): WheelEvent {
59
+ return new WheelEvent('wheel', {
60
+ deltaX,
61
+ deltaY,
62
+ bubbles: true,
63
+ cancelable: true,
64
+ view: window
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Class implementing a mock for the Pointer Lock API.
70
+ * Handles patching the document and element prototypes to simulate pointer lock.
71
+ */
72
+ export class MockPointerLock {
73
+ private _doc: Document;
74
+
75
+ constructor(doc: Document = document) {
76
+ this._doc = doc;
77
+ this.setup();
78
+ }
79
+
80
+ private setup() {
81
+ // Check if already patched to avoid conflicts
82
+ if ((this._doc as any).__mockPointerLockInstalled) return;
83
+
84
+ let _pointerLockElement: Element | null = null;
85
+ const doc = this._doc;
86
+
87
+ // Patch document.pointerLockElement
88
+ Object.defineProperty(doc, 'pointerLockElement', {
89
+ get: () => _pointerLockElement,
90
+ configurable: true
91
+ });
92
+
93
+ // Patch document.exitPointerLock
94
+ doc.exitPointerLock = () => {
95
+ if (_pointerLockElement) {
96
+ _pointerLockElement = null;
97
+ doc.dispatchEvent(new Event('pointerlockchange'));
98
+ }
99
+ };
100
+
101
+ // Patch HTMLElement.prototype.requestPointerLock
102
+ // Save original if needed, but in JSDOM it might not exist or do nothing
103
+ if (!(global.HTMLElement.prototype as any).__originalRequestPointerLock) {
104
+ (global.HTMLElement.prototype as any).__originalRequestPointerLock = (global.HTMLElement.prototype as any).requestPointerLock;
105
+ }
106
+
107
+ (global.HTMLElement.prototype as any).requestPointerLock = function() {
108
+ _pointerLockElement = this;
109
+ doc.dispatchEvent(new Event('pointerlockchange'));
110
+ };
111
+
112
+ (doc as any).__mockPointerLockInstalled = true;
113
+ }
114
+
115
+ get element(): Element | null {
116
+ return this._doc.pointerLockElement;
117
+ }
118
+
119
+ get locked(): boolean {
120
+ return !!this.element;
121
+ }
122
+
123
+ request(element: HTMLElement) {
124
+ element.requestPointerLock();
125
+ }
126
+
127
+ exit() {
128
+ this._doc.exitPointerLock();
129
+ }
130
+
131
+ isLocked(): boolean {
132
+ return this.locked;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Class for simulating input events.
138
+ */
139
+ export class InputInjector {
140
+ constructor(private doc: Document = document, private win: Window = window) {}
141
+
142
+ keyDown(key: string, code?: string, modifiers?: KeyModifiers) {
143
+ const event = createMockKeyboardEvent(key, 'keydown', modifiers);
144
+ if (code) {
145
+ Object.defineProperty(event, 'code', { value: code });
146
+ }
147
+ this.doc.dispatchEvent(event);
148
+ }
149
+
150
+ keyUp(key: string, code?: string, modifiers?: KeyModifiers) {
151
+ const event = createMockKeyboardEvent(key, 'keyup', modifiers);
152
+ if (code) {
153
+ Object.defineProperty(event, 'code', { value: code });
154
+ }
155
+ this.doc.dispatchEvent(event);
156
+ }
157
+
158
+ mouseMove(movementX: number, movementY: number, clientX = 0, clientY = 0) {
159
+ const event = createMockMouseEvent('mousemove', {
160
+ clientX,
161
+ clientY,
162
+ movementX,
163
+ movementY
164
+ });
165
+ this.dispatchToTarget(event);
166
+ }
167
+
168
+ mouseButton(button: number, state: 'down' | 'up' = 'down') {
169
+ if (state === 'down') {
170
+ this.mouseDown(button);
171
+ } else {
172
+ this.mouseUp(button);
173
+ }
174
+ }
175
+
176
+ mouseDown(button: number = 0) {
177
+ const event = createMockMouseEvent('mousedown', { button });
178
+ this.dispatchToTarget(event);
179
+ }
180
+
181
+ mouseUp(button: number = 0) {
182
+ const event = createMockMouseEvent('mouseup', { button });
183
+ this.dispatchToTarget(event);
184
+ }
185
+
186
+ mouseWheel(deltaY: number) {
187
+ const event = createMockWheelEvent(0, deltaY);
188
+ this.dispatchToTarget(event);
189
+ }
190
+
191
+ // Alias for backward compatibility/ease of use
192
+ wheel(deltaY: number) {
193
+ this.mouseWheel(deltaY);
194
+ }
195
+
196
+ private dispatchToTarget(event: Event) {
197
+ const target = this.doc.pointerLockElement || this.doc;
198
+ target.dispatchEvent(event);
199
+ }
200
+ }
201
+
202
+ /**
203
+ * An implementation of InputSource that listens to DOM events.
204
+ * Useful for tests that want to verify interaction with real/mocked DOM events.
205
+ */
206
+ export class BrowserInputSource {
207
+ constructor(private target: EventTarget = document) {}
208
+
209
+ on(event: string, handler: Function): void {
210
+ if (event === 'keydown' || event === 'keyup') {
211
+ this.target.addEventListener(event, (e: any) => {
212
+ handler(e.code);
213
+ });
214
+ } else if (event === 'mousedown' || event === 'mouseup') {
215
+ this.target.addEventListener(event, (e: any) => {
216
+ handler(e.button);
217
+ });
218
+ } else if (event === 'mousemove') {
219
+ this.target.addEventListener(event, (e: any) => {
220
+ handler(e.movementX, e.movementY);
221
+ });
222
+ }
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Factory for creating a MockPointerLock instance.
228
+ */
229
+ export function createMockPointerLock(element?: HTMLElement): MockPointerLock {
230
+ const mock = new MockPointerLock();
231
+ if (element) {
232
+ mock.request(element);
233
+ }
234
+ return mock;
235
+ }
236
+
237
+ /**
238
+ * Factory for creating an InputInjector.
239
+ */
240
+ export function createInputInjector(target?: EventTarget): InputInjector {
241
+ // If target is provided and is a Document, use it.
242
+ // Otherwise default to global document.
243
+ const doc = (target instanceof Document) ? target : document;
244
+ const win = (doc.defaultView) ? doc.defaultView : window;
245
+ return new InputInjector(doc, win);
246
+ }
@@ -0,0 +1,148 @@
1
+
2
+ import {
3
+ ServerCommand,
4
+ ClientCommand,
5
+ Vec3
6
+ } from '@quake2ts/shared';
7
+ import {
8
+ EntityState,
9
+ FrameData,
10
+ ProtocolPlayerState,
11
+ createEmptyEntityState,
12
+ createEmptyProtocolPlayerState,
13
+ DamageIndicator,
14
+ FogData
15
+ } from '@quake2ts/engine';
16
+
17
+ /**
18
+ * Mocks a server message by just providing the type and data buffer.
19
+ * In a real scenario, this would be constructing a binary packet.
20
+ */
21
+ export interface MockServerMessage {
22
+ type: number;
23
+ data: Uint8Array;
24
+ }
25
+
26
+ export function createMockServerMessage(type: number, data: Uint8Array = new Uint8Array()): MockServerMessage {
27
+ return { type, data };
28
+ }
29
+
30
+ /**
31
+ * Creates a mock Snapshot (FrameData) for testing.
32
+ */
33
+ export function createMockSnapshot(
34
+ serverFrame: number,
35
+ entities: EntityState[] = [],
36
+ playerState?: Partial<ProtocolPlayerState>,
37
+ deltaFrame: number = 0
38
+ ): FrameData {
39
+ return {
40
+ serverFrame,
41
+ deltaFrame,
42
+ surpressCount: 0,
43
+ areaBytes: 0,
44
+ areaBits: new Uint8Array(),
45
+ playerState: {
46
+ ...createEmptyProtocolPlayerState(),
47
+ ...playerState
48
+ },
49
+ packetEntities: {
50
+ delta: false,
51
+ entities
52
+ }
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Creates a mock Delta Frame (FrameData with delta flag).
58
+ */
59
+ export function createMockDeltaFrame(
60
+ serverFrame: number,
61
+ deltaFrame: number,
62
+ entities: EntityState[] = [],
63
+ playerState?: Partial<ProtocolPlayerState>
64
+ ): FrameData {
65
+ return {
66
+ serverFrame,
67
+ deltaFrame,
68
+ surpressCount: 0,
69
+ areaBytes: 0,
70
+ areaBits: new Uint8Array(),
71
+ playerState: {
72
+ ...createEmptyProtocolPlayerState(),
73
+ ...playerState
74
+ },
75
+ packetEntities: {
76
+ delta: true,
77
+ entities
78
+ }
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Simulates network delay for a sequence of messages.
84
+ */
85
+ export function simulateNetworkDelay<T>(messages: T[], delayMs: number): Promise<T[]> {
86
+ return new Promise(resolve => {
87
+ setTimeout(() => {
88
+ resolve(messages);
89
+ }, delayMs);
90
+ });
91
+ }
92
+
93
+ /**
94
+ * Simulates packet loss by randomly filtering messages.
95
+ * @param messages The messages to process
96
+ * @param lossPercent Percentage of packet loss (0-100)
97
+ */
98
+ export function simulatePacketLoss<T>(messages: T[], lossPercent: number): T[] {
99
+ return messages.filter(() => Math.random() * 100 >= lossPercent);
100
+ }
101
+
102
+ /**
103
+ * Factory for creating a mock EntityState.
104
+ */
105
+ export function createMockEntityState(
106
+ number: number,
107
+ modelIndex: number = 0,
108
+ origin: Partial<Vec3> = { x: 0, y: 0, z: 0 },
109
+ overrides?: Partial<EntityState>
110
+ ): EntityState {
111
+ const state = createEmptyEntityState();
112
+ state.number = number;
113
+ state.modelindex = modelIndex;
114
+ state.origin.x = origin.x ?? 0;
115
+ state.origin.y = origin.y ?? 0;
116
+ state.origin.z = origin.z ?? 0;
117
+
118
+ if (overrides) {
119
+ Object.assign(state, overrides);
120
+ }
121
+ return state;
122
+ }
123
+
124
+ export function createMockDamageIndicator(
125
+ damage: number,
126
+ dir: Vec3 = { x: 0, y: 0, z: 0 },
127
+ health = true,
128
+ armor = false,
129
+ power = false
130
+ ): DamageIndicator {
131
+ return {
132
+ damage,
133
+ dir,
134
+ health,
135
+ armor,
136
+ power
137
+ };
138
+ }
139
+
140
+ export function createMockFogData(overrides: Partial<FogData> = {}): FogData {
141
+ return {
142
+ density: 0.1,
143
+ red: 100,
144
+ green: 100,
145
+ blue: 100,
146
+ ...overrides
147
+ };
148
+ }
@@ -0,0 +1,148 @@
1
+ import { EntityState, PmoveTraceResult, Vec3 } from '@quake2ts/shared';
2
+
3
+ // Mock ConfigStringIndex to match shared
4
+ enum ConfigStringIndex {
5
+ Models = 32,
6
+ Sounds = 288,
7
+ Images = 544,
8
+ Players = 544 + 256, // Base for players, usually logic handles offset
9
+ }
10
+
11
+ const MAX_MODELS = 256;
12
+ const MAX_SOUNDS = 256;
13
+ const MAX_IMAGES = 256;
14
+
15
+ // Minimal ClientConfigStrings implementation for testing
16
+ export class MockClientConfigStrings {
17
+ private readonly strings: Map<number, string> = new Map();
18
+ private readonly models: string[] = [];
19
+ private readonly sounds: string[] = [];
20
+ private readonly images: string[] = [];
21
+
22
+ constructor() {}
23
+
24
+ public set(index: number, value: string): void {
25
+ this.strings.set(index, value);
26
+
27
+ if (index >= ConfigStringIndex.Models && index < ConfigStringIndex.Models + MAX_MODELS) {
28
+ this.models[index - ConfigStringIndex.Models] = value;
29
+ } else if (index >= ConfigStringIndex.Sounds && index < ConfigStringIndex.Sounds + MAX_SOUNDS) {
30
+ this.sounds[index - ConfigStringIndex.Sounds] = value;
31
+ } else if (index >= ConfigStringIndex.Images && index < ConfigStringIndex.Images + MAX_IMAGES) {
32
+ this.images[index - ConfigStringIndex.Images] = value;
33
+ }
34
+ }
35
+
36
+ public get(index: number): string | undefined {
37
+ return this.strings.get(index);
38
+ }
39
+
40
+ public getModelName(index: number): string | undefined {
41
+ return this.models[index];
42
+ }
43
+
44
+ public getSoundName(index: number): string | undefined {
45
+ return this.sounds[index];
46
+ }
47
+
48
+ public getImageName(index: number): string | undefined {
49
+ return this.images[index];
50
+ }
51
+
52
+ public getPlayerName(playernum: number): string | undefined {
53
+ // Stub logic
54
+ const info = this.strings.get(ConfigStringIndex.Players + playernum);
55
+ if (!info) return undefined;
56
+ const parts = info.split('\\');
57
+ for (let i = 1; i < parts.length; i += 2) {
58
+ if (parts[i] === 'name') {
59
+ return parts[i + 1];
60
+ }
61
+ }
62
+ return undefined;
63
+ }
64
+
65
+ public clear(): void {
66
+ this.strings.clear();
67
+ this.models.length = 0;
68
+ this.sounds.length = 0;
69
+ this.images.length = 0;
70
+ }
71
+ }
72
+
73
+ // Define ClientState interface locally as it's not exported from client
74
+ export interface ClientStateProvider {
75
+ tickRate: number;
76
+ frameTimeMs: number;
77
+ serverFrame: number;
78
+ serverProtocol: number;
79
+ configStrings: MockClientConfigStrings;
80
+ getClientName(num: number): string;
81
+ getKeyBinding(key: string): string;
82
+ inAutoDemo: boolean;
83
+ }
84
+
85
+ export interface ClientInfo {
86
+ name: string;
87
+ skin: string;
88
+ model: string;
89
+ icon: string;
90
+ }
91
+
92
+ export interface ClientState extends ClientStateProvider {
93
+ playerNum: number;
94
+ serverTime: number;
95
+ parseEntities: number;
96
+ }
97
+
98
+ export interface Frame {
99
+ serverFrame: number;
100
+ deltaFrame: number;
101
+ valid: boolean;
102
+ entities: EntityState[];
103
+ }
104
+
105
+ export interface ConnectionState {
106
+ state: 'disconnected' | 'connecting' | 'connected' | 'active';
107
+ }
108
+
109
+ // -- Factories --
110
+
111
+ export const createMockClientState = (overrides?: Partial<ClientState>): ClientState => {
112
+ const configStrings = new MockClientConfigStrings();
113
+
114
+ return {
115
+ tickRate: 10,
116
+ frameTimeMs: 100,
117
+ serverFrame: 0,
118
+ serverProtocol: 34,
119
+ configStrings,
120
+ playerNum: 0,
121
+ serverTime: 0,
122
+ parseEntities: 0,
123
+ inAutoDemo: false,
124
+ getClientName: (num: number) => `Player${num}`,
125
+ getKeyBinding: (key: string) => '',
126
+ ...overrides
127
+ };
128
+ };
129
+
130
+ export const createMockFrame = (overrides?: Partial<Frame>): Frame => ({
131
+ serverFrame: 0,
132
+ deltaFrame: -1,
133
+ valid: true,
134
+ entities: [],
135
+ ...overrides
136
+ });
137
+
138
+ export const createMockClientInfo = (overrides?: Partial<ClientInfo>): ClientInfo => ({
139
+ name: 'Player',
140
+ skin: 'male/grunt',
141
+ model: 'male',
142
+ icon: 'pics/icon.pcx',
143
+ ...overrides
144
+ });
145
+
146
+ export const createMockConnectionState = (state: ConnectionState['state'] = 'connected'): ConnectionState => ({
147
+ state
148
+ });
@@ -0,0 +1,47 @@
1
+ export type NetworkCondition = 'good' | 'slow' | 'unstable' | 'offline' | 'custom';
2
+
3
+ export interface NetworkSimulator {
4
+ latency: number; // ms
5
+ jitter: number; // ms
6
+ packetLoss: number; // percentage 0-1
7
+ bandwidth: number; // bytes per second
8
+ }
9
+
10
+ /**
11
+ * Simulates network conditions.
12
+ */
13
+ export function simulateNetworkCondition(condition: NetworkCondition): NetworkSimulator {
14
+ switch (condition) {
15
+ case 'good':
16
+ return { latency: 20, jitter: 5, packetLoss: 0, bandwidth: 10 * 1024 * 1024 };
17
+ case 'slow':
18
+ return { latency: 150, jitter: 20, packetLoss: 0.01, bandwidth: 1 * 1024 * 1024 };
19
+ case 'unstable':
20
+ return { latency: 100, jitter: 100, packetLoss: 0.05, bandwidth: 512 * 1024 };
21
+ case 'offline':
22
+ return { latency: 0, jitter: 0, packetLoss: 1, bandwidth: 0 };
23
+ case 'custom':
24
+ default:
25
+ return { latency: 0, jitter: 0, packetLoss: 0, bandwidth: Infinity };
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Creates a custom network condition.
31
+ */
32
+ export function createCustomNetworkCondition(latency: number, jitter: number, packetLoss: number): NetworkSimulator {
33
+ return {
34
+ latency,
35
+ jitter,
36
+ packetLoss,
37
+ bandwidth: Infinity // Default to unlimited unless specified
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Helper to throttle bandwidth (e.g. for Playwright).
43
+ */
44
+ export function throttleBandwidth(bytesPerSecond: number): void {
45
+ // This function would interface with Playwright's CDPSession in an E2E test.
46
+ // It returns void here as a placeholder for the logic to be used within a test context.
47
+ }
@@ -0,0 +1,90 @@
1
+ export interface PlaywrightOptions {
2
+ headless?: boolean;
3
+ viewport?: { width: number; height: number };
4
+ }
5
+
6
+ export interface PlaywrightTestClient {
7
+ page: any; // Type as 'Page' from playwright in real usage, but keeping it generic here to avoid hard dependency on playwright types in this util file if not needed
8
+ browser: any;
9
+ navigate(url: string): Promise<void>;
10
+ waitForGame(): Promise<void>;
11
+ injectInput(type: string, data: any): Promise<void>;
12
+ screenshot(name: string): Promise<Buffer>;
13
+ close(): Promise<void>;
14
+ }
15
+
16
+ /**
17
+ * Creates a Playwright test client.
18
+ * Note: Requires playwright to be installed in the project.
19
+ */
20
+ export async function createPlaywrightTestClient(options: PlaywrightOptions = {}): Promise<PlaywrightTestClient> {
21
+ // Dynamic import to avoid hard dependency if not used
22
+ let playwright;
23
+ try {
24
+ playwright = await import('playwright');
25
+ } catch (e) {
26
+ throw new Error('Playwright is not installed. Please install it to use this utility.');
27
+ }
28
+
29
+ const browser = await playwright.chromium.launch({
30
+ headless: options.headless ?? true,
31
+ });
32
+ const context = await browser.newContext({
33
+ viewport: options.viewport || { width: 1280, height: 720 },
34
+ });
35
+ const page = await context.newPage();
36
+
37
+ return {
38
+ page,
39
+ browser,
40
+ async navigate(url: string) {
41
+ await page.goto(url);
42
+ },
43
+ async waitForGame() {
44
+ await waitForGameReady(page);
45
+ },
46
+ async injectInput(type: string, data: any) {
47
+ // Simulate input injection via evaluate
48
+ await page.evaluate(({ type, data }: any) => {
49
+ // Assumes a global function or event listener exists to receive injected input
50
+ console.log('Injecting input', type, data);
51
+ // (window as any).game.injectInput(type, data);
52
+ }, { type, data });
53
+ },
54
+ async screenshot(name: string) {
55
+ return await page.screenshot({ path: `${name}.png` });
56
+ },
57
+ async close() {
58
+ await browser.close();
59
+ }
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Waits for the game to be ready.
65
+ */
66
+ export async function waitForGameReady(page: any, timeout: number = 10000): Promise<void> {
67
+ await page.waitForFunction(() => {
68
+ // Check for some global game state or canvas presence
69
+ return (window as any).game && (window as any).game.isRunning;
70
+ }, { timeout });
71
+ }
72
+
73
+ export interface GameStateCapture {
74
+ time: number;
75
+ entities: number;
76
+ // Add other state properties
77
+ }
78
+
79
+ /**
80
+ * Captures current game state from the browser.
81
+ */
82
+ export async function captureGameState(page: any): Promise<GameStateCapture> {
83
+ return await page.evaluate(() => {
84
+ const game = (window as any).game;
85
+ return {
86
+ time: game ? game.time : 0,
87
+ entities: game && game.entities ? game.entities.length : 0
88
+ };
89
+ });
90
+ }