@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.
- package/README.md +454 -0
- package/dist/index.cjs +5432 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2150 -0
- package/dist/index.d.ts +2150 -0
- package/dist/index.js +5165 -0
- package/dist/index.js.map +1 -0
- package/package.json +82 -0
- package/src/client/helpers/hud.ts +114 -0
- package/src/client/helpers/prediction.ts +136 -0
- package/src/client/helpers/view.ts +201 -0
- package/src/client/mocks/console.ts +75 -0
- package/src/client/mocks/download.ts +48 -0
- package/src/client/mocks/input.ts +246 -0
- package/src/client/mocks/network.ts +148 -0
- package/src/client/mocks/state.ts +148 -0
- package/src/e2e/network.ts +47 -0
- package/src/e2e/playwright.ts +90 -0
- package/src/e2e/visual.ts +172 -0
- package/src/engine/helpers/pipeline-test-template.ts +113 -0
- package/src/engine/helpers/webgpu-rendering.ts +251 -0
- package/src/engine/mocks/assets.ts +129 -0
- package/src/engine/mocks/audio.ts +152 -0
- package/src/engine/mocks/buffers.ts +88 -0
- package/src/engine/mocks/lighting.ts +64 -0
- package/src/engine/mocks/particles.ts +76 -0
- package/src/engine/mocks/renderer.ts +218 -0
- package/src/engine/mocks/webgl.ts +267 -0
- package/src/engine/mocks/webgpu.ts +262 -0
- package/src/engine/rendering.ts +103 -0
- package/src/game/factories.ts +204 -0
- package/src/game/helpers/physics.ts +171 -0
- package/src/game/helpers/save.ts +232 -0
- package/src/game/helpers.ts +310 -0
- package/src/game/mocks/ai.ts +67 -0
- package/src/game/mocks/combat.ts +61 -0
- package/src/game/mocks/items.ts +166 -0
- package/src/game/mocks.ts +105 -0
- package/src/index.ts +93 -0
- package/src/server/helpers/bandwidth.ts +127 -0
- package/src/server/helpers/multiplayer.ts +158 -0
- package/src/server/helpers/snapshot.ts +241 -0
- package/src/server/mockNetDriver.ts +106 -0
- package/src/server/mockTransport.ts +50 -0
- package/src/server/mocks/commands.ts +93 -0
- package/src/server/mocks/connection.ts +139 -0
- package/src/server/mocks/master.ts +97 -0
- package/src/server/mocks/physics.ts +32 -0
- package/src/server/mocks/state.ts +162 -0
- package/src/server/mocks/transport.ts +161 -0
- package/src/setup/audio.ts +118 -0
- package/src/setup/browser.ts +249 -0
- package/src/setup/canvas.ts +142 -0
- package/src/setup/node.ts +21 -0
- package/src/setup/storage.ts +60 -0
- package/src/setup/timing.ts +142 -0
- package/src/setup/webgl.ts +8 -0
- package/src/setup/webgpu.ts +113 -0
- package/src/shared/bsp.ts +145 -0
- package/src/shared/collision.ts +64 -0
- package/src/shared/factories.ts +88 -0
- package/src/shared/math.ts +65 -0
- package/src/shared/mocks.ts +243 -0
- package/src/shared/pak-loader.ts +45 -0
- 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
|
+
}
|