@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,161 @@
1
+ import { NetworkTransport } from '@quake2ts/server';
2
+ import { NetDriver } from '@quake2ts/shared';
3
+ import { vi } from 'vitest';
4
+
5
+ /**
6
+ * Mock implementation of the NetworkTransport interface for server testing.
7
+ * Allows simulating connections and errors.
8
+ */
9
+ export class MockTransport implements NetworkTransport {
10
+ public onConnectionCallback?: (driver: NetDriver, info?: any) => void;
11
+ public onErrorCallback?: (error: Error) => void;
12
+ public address: string = '127.0.0.1';
13
+ public port: number = 27910;
14
+ public sentMessages: Uint8Array[] = [];
15
+ public receivedMessages: Uint8Array[] = [];
16
+ public listening: boolean = false;
17
+
18
+ public listenSpy = vi.fn().mockImplementation(async (port: number) => {
19
+ this.port = port;
20
+ this.listening = true;
21
+ });
22
+ public closeSpy = vi.fn().mockImplementation(() => {
23
+ this.listening = false;
24
+ });
25
+
26
+ /**
27
+ * Start listening on the specified port.
28
+ */
29
+ async listen(port: number): Promise<void> {
30
+ return this.listenSpy(port);
31
+ }
32
+
33
+ /**
34
+ * Close the transport.
35
+ */
36
+ close() {
37
+ this.closeSpy();
38
+ }
39
+
40
+ /**
41
+ * Register a callback for new connections.
42
+ */
43
+ onConnection(callback: (driver: NetDriver, info?: any) => void) {
44
+ this.onConnectionCallback = callback;
45
+ }
46
+
47
+ /**
48
+ * Register a callback for errors.
49
+ */
50
+ onError(callback: (error: Error) => void) {
51
+ this.onErrorCallback = callback;
52
+ }
53
+
54
+ /**
55
+ * Check if the transport is currently listening.
56
+ */
57
+ public isListening(): boolean {
58
+ return this.listening;
59
+ }
60
+
61
+ /**
62
+ * Helper to simulate a new connection.
63
+ * @param driver The network driver for the connection.
64
+ * @param info Optional connection info.
65
+ */
66
+ public simulateConnection(driver: NetDriver, info?: any) {
67
+ if (this.onConnectionCallback) {
68
+ this.onConnectionCallback(driver, info);
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Helper to simulate an error.
74
+ * @param error The error to simulate.
75
+ */
76
+ public simulateError(error: Error) {
77
+ if (this.onErrorCallback) {
78
+ this.onErrorCallback(error);
79
+ }
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Interface for mock UDP socket.
85
+ * This is a partial mock of Node.js dgram.Socket or similar.
86
+ */
87
+ export interface MockUDPSocket {
88
+ send: (msg: Uint8Array, offset: number, length: number, port: number, address: string, callback?: (error: Error | null, bytes: number) => void) => void;
89
+ on: (event: string, callback: (...args: any[]) => void) => void;
90
+ close: () => void;
91
+ bind: (port: number, address?: string) => void;
92
+ address: () => { address: string; family: string; port: number };
93
+ }
94
+
95
+ /**
96
+ * Creates a mock UDP socket.
97
+ * @param overrides Optional overrides for the socket methods.
98
+ */
99
+ export function createMockUDPSocket(overrides?: Partial<MockUDPSocket>): MockUDPSocket {
100
+ const socket: MockUDPSocket = {
101
+ send: vi.fn(),
102
+ on: vi.fn(),
103
+ close: vi.fn(),
104
+ bind: vi.fn(),
105
+ address: vi.fn().mockReturnValue({ address: '127.0.0.1', family: 'IPv4', port: 0 }),
106
+ ...overrides,
107
+ };
108
+ return socket;
109
+ }
110
+
111
+ /**
112
+ * Interface for network address.
113
+ */
114
+ export interface NetworkAddress {
115
+ ip: string;
116
+ port: number;
117
+ }
118
+
119
+ /**
120
+ * Creates a mock network address.
121
+ * @param ip IP address (default: '127.0.0.1')
122
+ * @param port Port number (default: 27910)
123
+ */
124
+ export function createMockNetworkAddress(ip: string = '127.0.0.1', port: number = 27910): NetworkAddress {
125
+ return { ip, port };
126
+ }
127
+
128
+ /**
129
+ * Creates a configured MockTransport instance.
130
+ * @param address Address to bind to (default: '127.0.0.1')
131
+ * @param port Port to listen on (default: 27910)
132
+ * @param overrides Optional overrides for the transport properties.
133
+ */
134
+ export function createMockTransport(
135
+ address: string = '127.0.0.1',
136
+ port: number = 27910,
137
+ overrides?: Partial<MockTransport>
138
+ ): MockTransport {
139
+ const transport = new MockTransport();
140
+ transport.address = address;
141
+ transport.port = port;
142
+ Object.assign(transport, overrides);
143
+ return transport;
144
+ }
145
+
146
+ /**
147
+ * Creates a mock NetDriver instance.
148
+ * @param overrides Optional overrides for the NetDriver methods.
149
+ */
150
+ export function createMockNetDriver(overrides?: Partial<NetDriver>): NetDriver {
151
+ return {
152
+ connect: vi.fn().mockResolvedValue(undefined),
153
+ disconnect: vi.fn(),
154
+ send: vi.fn(),
155
+ onMessage: vi.fn(),
156
+ onClose: vi.fn(),
157
+ onError: vi.fn(),
158
+ isConnected: vi.fn().mockReturnValue(true),
159
+ ...overrides
160
+ };
161
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Mocks for Web Audio API.
3
+ */
4
+
5
+ /**
6
+ * Sets up a mock AudioContext globally.
7
+ */
8
+ export function setupMockAudioContext(): void {
9
+ class MockAudioContext {
10
+ state = 'suspended';
11
+ destination = {};
12
+ currentTime = 0;
13
+ listener = {
14
+ positionX: { value: 0 },
15
+ positionY: { value: 0 },
16
+ positionZ: { value: 0 },
17
+ forwardX: { value: 0 },
18
+ forwardY: { value: 0 },
19
+ forwardZ: { value: 0 },
20
+ upX: { value: 0 },
21
+ upY: { value: 0 },
22
+ upZ: { value: 0 },
23
+ setOrientation: () => {},
24
+ setPosition: () => {}
25
+ };
26
+
27
+ createGain() {
28
+ return {
29
+ gain: { value: 1, linearRampToValueAtTime: () => {} },
30
+ connect: () => {},
31
+ disconnect: () => {}
32
+ };
33
+ }
34
+
35
+ createBufferSource() {
36
+ return {
37
+ buffer: null,
38
+ loop: false,
39
+ playbackRate: { value: 1 },
40
+ connect: () => {},
41
+ start: () => {},
42
+ stop: () => {},
43
+ disconnect: () => {},
44
+ onended: null
45
+ };
46
+ }
47
+
48
+ createPanner() {
49
+ return {
50
+ panningModel: 'equalpower',
51
+ distanceModel: 'inverse',
52
+ positionX: { value: 0 },
53
+ positionY: { value: 0 },
54
+ positionZ: { value: 0 },
55
+ orientationX: { value: 0 },
56
+ orientationY: { value: 0 },
57
+ orientationZ: { value: 0 },
58
+ coneInnerAngle: 360,
59
+ coneOuterAngle: 360,
60
+ coneOuterGain: 0,
61
+ connect: () => {},
62
+ disconnect: () => {},
63
+ setPosition: () => {},
64
+ setOrientation: () => {}
65
+ }
66
+ }
67
+
68
+ createBuffer(numOfChannels: number, length: number, sampleRate: number) {
69
+ return {
70
+ duration: length / sampleRate,
71
+ length,
72
+ sampleRate,
73
+ numberOfChannels: numOfChannels,
74
+ getChannelData: () => new Float32Array(length)
75
+ }
76
+ }
77
+
78
+ decodeAudioData(data: ArrayBuffer, success?: (buffer: any) => void) {
79
+ const buffer = this.createBuffer(2, 100, 44100);
80
+ if (success) success(buffer);
81
+ return Promise.resolve(buffer);
82
+ }
83
+
84
+ resume() { return Promise.resolve(); }
85
+ suspend() { return Promise.resolve(); }
86
+ close() { return Promise.resolve(); }
87
+ }
88
+
89
+ global.AudioContext = MockAudioContext as any;
90
+ // @ts-ignore
91
+ global.webkitAudioContext = MockAudioContext as any;
92
+ }
93
+
94
+ /**
95
+ * Restores original AudioContext.
96
+ */
97
+ export function teardownMockAudioContext(): void {
98
+ // @ts-ignore
99
+ delete global.AudioContext;
100
+ // @ts-ignore
101
+ delete global.webkitAudioContext;
102
+ }
103
+
104
+ export interface AudioEvent {
105
+ type: string;
106
+ data?: any;
107
+ }
108
+
109
+ /**
110
+ * Captures audio events from a context.
111
+ * Requires the context to be instrumented or mocked to emit events.
112
+ * This helper currently works with the `setupMockAudioContext` mock if extended.
113
+ */
114
+ export function captureAudioEvents(context: AudioContext): AudioEvent[] {
115
+ // Placeholder for capturing events.
116
+ // Real implementation would attach spies to context methods.
117
+ return [];
118
+ }
@@ -0,0 +1,249 @@
1
+ import { JSDOM } from 'jsdom';
2
+ import { Canvas, Image, ImageData } from '@napi-rs/canvas';
3
+ import 'fake-indexeddb/auto';
4
+ import { MockPointerLock } from '../client/mocks/input.js';
5
+ import { createMockWebGL2Context } from './webgl.js';
6
+
7
+ export interface BrowserSetupOptions {
8
+ url?: string;
9
+ pretendToBeVisual?: boolean;
10
+ resources?: "usable";
11
+ enableWebGL2?: boolean;
12
+ enablePointerLock?: boolean;
13
+ }
14
+
15
+ /**
16
+ * Sets up a browser environment for testing using JSDOM and napi-rs/canvas.
17
+ * This should be called in your vitest.setup.ts file.
18
+ */
19
+ export function setupBrowserEnvironment(options: BrowserSetupOptions = {}) {
20
+ const {
21
+ url = 'http://localhost',
22
+ pretendToBeVisual = true,
23
+ resources = undefined,
24
+ enableWebGL2 = false,
25
+ enablePointerLock = false
26
+ } = options;
27
+
28
+ // Create a JSDOM instance
29
+ const dom = new JSDOM('<!DOCTYPE html><html><head></head><body></body></html>', {
30
+ url,
31
+ pretendToBeVisual,
32
+ resources: resources as any
33
+ });
34
+
35
+ // Set up global variables
36
+ global.window = dom.window as any;
37
+ global.document = dom.window.document;
38
+
39
+ // Polyfill global DOM Constructors
40
+ global.Document = dom.window.Document;
41
+ global.Element = dom.window.Element;
42
+ global.Node = dom.window.Node;
43
+
44
+ // Handle navigator assignment safely
45
+ try {
46
+ // @ts-ignore
47
+ global.navigator = dom.window.navigator;
48
+ } catch (e) {
49
+ try {
50
+ Object.defineProperty(global, 'navigator', {
51
+ value: dom.window.navigator,
52
+ writable: true,
53
+ configurable: true
54
+ });
55
+ } catch (e2) {
56
+ console.warn('Could not assign global.navigator, skipping.');
57
+ }
58
+ }
59
+
60
+ global.location = dom.window.location;
61
+ global.HTMLElement = dom.window.HTMLElement;
62
+ global.HTMLCanvasElement = dom.window.HTMLCanvasElement;
63
+
64
+ // Polyfill global Event constructors
65
+ global.Event = dom.window.Event;
66
+ global.CustomEvent = dom.window.CustomEvent;
67
+ global.DragEvent = dom.window.DragEvent as any;
68
+ global.MouseEvent = dom.window.MouseEvent;
69
+ global.KeyboardEvent = dom.window.KeyboardEvent;
70
+ global.FocusEvent = dom.window.FocusEvent;
71
+ global.WheelEvent = dom.window.WheelEvent;
72
+ global.InputEvent = dom.window.InputEvent;
73
+ global.UIEvent = dom.window.UIEvent;
74
+
75
+ // Setup Storage mocks
76
+ try {
77
+ global.localStorage = dom.window.localStorage;
78
+ } catch (e) {
79
+ // Ignore if it fails
80
+ }
81
+
82
+ if (!global.localStorage) {
83
+ const storage = new Map<string, string>();
84
+ global.localStorage = {
85
+ getItem: (key: string) => storage.get(key) || null,
86
+ setItem: (key: string, value: string) => storage.set(key, value),
87
+ removeItem: (key: string) => storage.delete(key),
88
+ clear: () => storage.clear(),
89
+ key: (index: number) => Array.from(storage.keys())[index] || null,
90
+ get length() { return storage.size; }
91
+ } as Storage;
92
+ }
93
+
94
+ // Override document.createElement for canvas elements
95
+ const originalCreateElement = document.createElement.bind(document);
96
+ document.createElement = function (tagName: string, options?: any) {
97
+ if (tagName.toLowerCase() === 'canvas') {
98
+ const napiCanvas = new Canvas(300, 150);
99
+ const domCanvas = originalCreateElement('canvas', options);
100
+
101
+ Object.defineProperty(domCanvas, 'width', {
102
+ get: () => napiCanvas.width,
103
+ set: (value) => { napiCanvas.width = value; },
104
+ enumerable: true,
105
+ configurable: true
106
+ });
107
+
108
+ Object.defineProperty(domCanvas, 'height', {
109
+ get: () => napiCanvas.height,
110
+ set: (value) => { napiCanvas.height = value; },
111
+ enumerable: true,
112
+ configurable: true
113
+ });
114
+
115
+ const originalGetContext = domCanvas.getContext.bind(domCanvas);
116
+ domCanvas.getContext = function(contextId: string, options?: any) {
117
+ if (contextId === '2d') {
118
+ return napiCanvas.getContext('2d', options);
119
+ }
120
+
121
+ if (enableWebGL2 && contextId === 'webgl2') {
122
+ return createMockWebGL2Context(domCanvas as HTMLCanvasElement);
123
+ }
124
+
125
+ if (contextId === 'webgl' || contextId === 'webgl2') {
126
+ return originalGetContext(contextId, options);
127
+ }
128
+ return napiCanvas.getContext(contextId as any, options);
129
+ } as any;
130
+
131
+ (domCanvas as any).__napiCanvas = napiCanvas;
132
+
133
+ return domCanvas;
134
+ }
135
+ return originalCreateElement(tagName, options);
136
+ } as any;
137
+
138
+ if (enableWebGL2) {
139
+ const originalProtoGetContext = global.HTMLCanvasElement.prototype.getContext;
140
+ global.HTMLCanvasElement.prototype.getContext = function (
141
+ contextId: string,
142
+ options?: any
143
+ ): any {
144
+ if (contextId === 'webgl2') {
145
+ return createMockWebGL2Context(this);
146
+ }
147
+ // @ts-ignore
148
+ return originalProtoGetContext.call(this, contextId as any, options);
149
+ };
150
+ }
151
+
152
+ global.Image = Image as any;
153
+ global.ImageData = ImageData as any;
154
+
155
+ if (typeof global.createImageBitmap === 'undefined') {
156
+ global.createImageBitmap = async function (
157
+ image: any,
158
+ _options?: ImageBitmapOptions
159
+ ): Promise<any> {
160
+ if (image && typeof image.width === 'number' && typeof image.height === 'number') {
161
+ const canvas = new Canvas(image.width, image.height);
162
+ const ctx = canvas.getContext('2d');
163
+ if (image.data) {
164
+ ctx.putImageData(image as any, 0, 0);
165
+ }
166
+ return canvas;
167
+ }
168
+ const canvas = new Canvas(100, 100);
169
+ return canvas;
170
+ } as unknown as any;
171
+ }
172
+
173
+ if (typeof global.btoa === 'undefined') {
174
+ global.btoa = function (str: string): string {
175
+ return Buffer.from(str, 'binary').toString('base64');
176
+ };
177
+ }
178
+
179
+ if (typeof global.atob === 'undefined') {
180
+ global.atob = function (str: string): string {
181
+ return Buffer.from(str, 'base64').toString('binary');
182
+ };
183
+ }
184
+
185
+ if (enablePointerLock) {
186
+ new MockPointerLock(global.document);
187
+ }
188
+
189
+ if (typeof global.requestAnimationFrame === 'undefined') {
190
+ let lastTime = 0;
191
+ global.requestAnimationFrame = (callback: FrameRequestCallback) => {
192
+ const currTime = Date.now();
193
+ const timeToCall = Math.max(0, 16 - (currTime - lastTime));
194
+ const id = setTimeout(() => {
195
+ callback(currTime + timeToCall);
196
+ }, timeToCall);
197
+ lastTime = currTime + timeToCall;
198
+ return id as unknown as number;
199
+ };
200
+
201
+ global.cancelAnimationFrame = (id: number) => {
202
+ clearTimeout(id);
203
+ };
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Cleans up the browser environment.
209
+ */
210
+ export function teardownBrowserEnvironment() {
211
+ // @ts-ignore
212
+ delete global.window;
213
+ // @ts-ignore
214
+ delete global.document;
215
+ // @ts-ignore
216
+ delete global.navigator;
217
+ // @ts-ignore
218
+ delete global.localStorage;
219
+ // @ts-ignore
220
+ delete global.location;
221
+ // @ts-ignore
222
+ delete global.HTMLElement;
223
+ // @ts-ignore
224
+ delete global.HTMLCanvasElement;
225
+ // @ts-ignore
226
+ delete global.Image;
227
+ // @ts-ignore
228
+ delete global.ImageData;
229
+ // @ts-ignore
230
+ delete global.createImageBitmap;
231
+ // @ts-ignore
232
+ delete global.Event;
233
+ // @ts-ignore
234
+ delete global.CustomEvent;
235
+ // @ts-ignore
236
+ delete global.DragEvent;
237
+ // @ts-ignore
238
+ delete global.MouseEvent;
239
+ // @ts-ignore
240
+ delete global.KeyboardEvent;
241
+ // @ts-ignore
242
+ delete global.FocusEvent;
243
+ // @ts-ignore
244
+ delete global.WheelEvent;
245
+ // @ts-ignore
246
+ delete global.InputEvent;
247
+ // @ts-ignore
248
+ delete global.UIEvent;
249
+ }
@@ -0,0 +1,142 @@
1
+ import { Canvas, ImageData } from '@napi-rs/canvas';
2
+ import { createMockWebGL2Context } from '../engine/mocks/webgl.js';
3
+
4
+ /**
5
+ * Creates a mock HTMLCanvasElement backed by napi-rs/canvas,
6
+ * with support for both 2D and WebGL2 contexts.
7
+ */
8
+ export function createMockCanvas(width: number = 300, height: number = 150): HTMLCanvasElement {
9
+ // Use a real JSDOM canvas if available in the environment (setupBrowserEnvironment)
10
+ if (typeof document !== 'undefined' && document.createElement) {
11
+ const canvas = document.createElement('canvas');
12
+ canvas.width = width;
13
+ canvas.height = height;
14
+ return canvas;
15
+ }
16
+
17
+ // Fallback for non-JSDOM environments or specialized testing
18
+ const napiCanvas = new Canvas(width, height);
19
+ const canvas = {
20
+ width,
21
+ height,
22
+ getContext: (contextId: string, options?: any) => {
23
+ if (contextId === '2d') {
24
+ return napiCanvas.getContext('2d', options);
25
+ }
26
+ if (contextId === 'webgl2') {
27
+ return createMockWebGL2Context(canvas as any);
28
+ }
29
+ return null;
30
+ },
31
+ toDataURL: () => napiCanvas.toDataURL(),
32
+ toBuffer: (mime: any) => napiCanvas.toBuffer(mime),
33
+ // Add other properties as needed
34
+ } as unknown as HTMLCanvasElement;
35
+
36
+ return canvas;
37
+ }
38
+
39
+ /**
40
+ * Creates a mock CanvasRenderingContext2D.
41
+ */
42
+ export function createMockCanvasContext2D(canvas?: HTMLCanvasElement): CanvasRenderingContext2D {
43
+ const c = canvas || createMockCanvas();
44
+ const ctx = c.getContext('2d');
45
+ if (!ctx) {
46
+ throw new Error('Failed to create 2D context');
47
+ }
48
+ return ctx as CanvasRenderingContext2D;
49
+ }
50
+
51
+ /**
52
+ * Information about a captured draw call.
53
+ */
54
+ export interface DrawCall {
55
+ method: string;
56
+ args: any[];
57
+ }
58
+
59
+ /**
60
+ * Wraps a CanvasRenderingContext2D to capture all method calls.
61
+ */
62
+ export function captureCanvasDrawCalls(context: CanvasRenderingContext2D): DrawCall[] {
63
+ const calls: DrawCall[] = [];
64
+ const proto = Object.getPrototypeOf(context);
65
+
66
+ // Iterate over all properties of the context prototype
67
+ for (const key of Object.getOwnPropertyNames(proto)) {
68
+ const value = (context as any)[key];
69
+ if (typeof value === 'function') {
70
+ // Override function
71
+ (context as any)[key] = function(...args: any[]) {
72
+ calls.push({ method: key, args });
73
+ return value.apply(context, args);
74
+ };
75
+ }
76
+ }
77
+
78
+ return calls;
79
+ }
80
+
81
+ /**
82
+ * Creates a mock ImageData object.
83
+ */
84
+ export function createMockImageData(width: number, height: number, fillColor?: [number, number, number, number]): ImageData {
85
+ // Check if global ImageData is available (polyfilled by setupBrowserEnvironment)
86
+ if (typeof global.ImageData !== 'undefined') {
87
+ const data = new Uint8ClampedArray(width * height * 4);
88
+ if (fillColor) {
89
+ for (let i = 0; i < data.length; i += 4) {
90
+ data[i] = fillColor[0];
91
+ data[i + 1] = fillColor[1];
92
+ data[i + 2] = fillColor[2];
93
+ data[i + 3] = fillColor[3];
94
+ }
95
+ }
96
+ return new global.ImageData(data, width, height) as unknown as ImageData;
97
+ }
98
+
99
+ // Fallback if not globally available, though it should be with @napi-rs/canvas
100
+ const data = new Uint8ClampedArray(width * height * 4);
101
+ if (fillColor) {
102
+ for (let i = 0; i < data.length; i += 4) {
103
+ data[i] = fillColor[0];
104
+ data[i + 1] = fillColor[1];
105
+ data[i + 2] = fillColor[2];
106
+ data[i + 3] = fillColor[3];
107
+ }
108
+ }
109
+ return new ImageData(data, width, height);
110
+ }
111
+
112
+ /**
113
+ * Creates a mock HTMLImageElement.
114
+ */
115
+ export function createMockImage(width: number = 100, height: number = 100, src: string = ''): HTMLImageElement {
116
+ if (typeof document !== 'undefined' && document.createElement) {
117
+ const img = document.createElement('img');
118
+ img.width = width;
119
+ img.height = height;
120
+ if (src) img.src = src;
121
+ return img;
122
+ }
123
+
124
+ // Fallback
125
+ const img = {
126
+ width,
127
+ height,
128
+ src,
129
+ complete: true,
130
+ onload: null,
131
+ onerror: null,
132
+ } as unknown as HTMLImageElement;
133
+
134
+ // Simulate async load if src is provided
135
+ if (src) {
136
+ setTimeout(() => {
137
+ if (img.onload) (img.onload as any)();
138
+ }, 0);
139
+ }
140
+
141
+ return img;
142
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Setup helpers for Node.js environments.
3
+ */
4
+ export interface NodeSetupOptions {
5
+ // Add options as needed, e.g. mocking fs, process.env, etc.
6
+ }
7
+
8
+ /**
9
+ * Sets up a Node.js environment for testing.
10
+ * Currently a placeholder for future Node-specific setup.
11
+ */
12
+ export function setupNodeEnvironment(options: NodeSetupOptions = {}) {
13
+ // No-op for now, but provides a hook for future setup
14
+ }
15
+
16
+ /**
17
+ * Teardown for Node.js environment.
18
+ */
19
+ export function teardownNodeEnvironment() {
20
+ // No-op for now
21
+ }