@quake2ts/test-utils 0.0.1 → 0.0.740

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quake2ts/test-utils",
3
- "version": "0.0.1",
3
+ "version": "0.0.740",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -26,25 +26,43 @@
26
26
  "dist",
27
27
  "src"
28
28
  ],
29
- "dependencies": {
30
- "gl-matrix": "^3.4.4",
31
- "@quake2ts/game": "0.0.1",
32
- "@quake2ts/server": "0.0.1",
33
- "@quake2ts/engine": "^0.0.1",
34
- "@quake2ts/shared": "0.0.1"
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/jburnhams/quake2.git",
32
+ "directory": "quake2ts/packages/test-utils"
35
33
  },
36
34
  "peerDependencies": {
37
35
  "@webgpu/types": "^0.1.68",
36
+ "gl-matrix": "^3.4.4",
38
37
  "pixelmatch": "^7.1.0",
39
38
  "playwright": "^1.57.0",
40
39
  "pngjs": "^7.0.0",
41
40
  "vitest": "^1.6.0",
42
- "webgpu": "^0.3.8"
41
+ "webgpu": "^0.3.8",
42
+ "@quake2ts/engine": "^0.0.740",
43
+ "@quake2ts/game": "0.0.740",
44
+ "@quake2ts/server": "0.0.740",
45
+ "@quake2ts/shared": "0.0.740"
43
46
  },
44
47
  "peerDependenciesMeta": {
48
+ "@quake2ts/engine": {
49
+ "optional": true
50
+ },
51
+ "@quake2ts/game": {
52
+ "optional": true
53
+ },
54
+ "@quake2ts/server": {
55
+ "optional": true
56
+ },
57
+ "@quake2ts/shared": {
58
+ "optional": true
59
+ },
45
60
  "@webgpu/types": {
46
61
  "optional": true
47
62
  },
63
+ "gl-matrix": {
64
+ "optional": true
65
+ },
48
66
  "pixelmatch": {
49
67
  "optional": true
50
68
  },
@@ -59,6 +77,7 @@
59
77
  }
60
78
  },
61
79
  "devDependencies": {
80
+ "gl-matrix": "^3.4.4",
62
81
  "@napi-rs/canvas": "^0.1.84",
63
82
  "@types/jsdom": "^27.0.0",
64
83
  "@types/pixelmatch": "^5.2.6",
@@ -72,7 +91,11 @@
72
91
  "tsup": "^8.1.0",
73
92
  "typescript": "^5.4.5",
74
93
  "vitest": "^1.6.0",
75
- "webgpu": "^0.3.8"
94
+ "webgpu": "^0.3.8",
95
+ "@quake2ts/engine": "^0.0.740",
96
+ "@quake2ts/game": "0.0.740",
97
+ "@quake2ts/server": "0.0.740",
98
+ "@quake2ts/shared": "0.0.740"
76
99
  },
77
100
  "scripts": {
78
101
  "build": "tsup src/index.ts --format esm,cjs --dts",
package/src/e2e/visual.ts CHANGED
@@ -1,172 +1,105 @@
1
- import { Canvas, Image } from '@napi-rs/canvas';
1
+ import { Page } from 'playwright';
2
+ import { PNG } from 'pngjs';
3
+ import pixelmatch from 'pixelmatch';
4
+ import { Canvas } from '@napi-rs/canvas';
2
5
  import fs from 'fs/promises';
3
6
  import path from 'path';
4
7
 
5
8
  export interface VisualDiff {
6
- diffPercentage: number;
7
- diffImage?: Buffer;
9
+ diffPixels: number;
10
+ diffPercentage: number;
11
+ isMatch: boolean;
12
+ diffImage?: Buffer;
8
13
  }
9
14
 
10
- /**
11
- * Captures a screenshot of the game using Playwright.
12
- * @param page Playwright Page object
13
- * @param name Output filename (without extension)
14
- */
15
- export async function captureGameScreenshot(page: any, name: string): Promise<Buffer> {
16
- return await page.screenshot({ path: `${name}.png` });
15
+ export interface VisualScenario {
16
+ sceneName: string;
17
+ setup?: (page: Page) => Promise<void>;
17
18
  }
18
19
 
19
20
  /**
20
- * Saves a canvas to a PNG file.
21
- * Compatible with both JSDOM/HTMLCanvasElement and @napi-rs/canvas.
21
+ * Captures a screenshot of the game canvas.
22
22
  */
23
- export async function takeScreenshot(canvas: Canvas | HTMLCanvasElement, filepath: string): Promise<void> {
24
- let buffer: Buffer;
25
-
26
- // Handle @napi-rs/canvas Canvas object
27
- if ('toBuffer' in canvas && typeof canvas.toBuffer === 'function') {
28
- buffer = canvas.toBuffer('image/png');
29
- }
30
- // Handle JSDOM HTMLCanvasElement (if backed by node-canvas or similar)
31
- else if ('toDataURL' in canvas) {
32
- const dataUrl = canvas.toDataURL('image/png');
33
- const base64 = dataUrl.replace(/^data:image\/png;base64,/, '');
34
- buffer = Buffer.from(base64, 'base64');
35
- } else {
36
- throw new Error('Unsupported canvas type for screenshot');
37
- }
38
-
39
- await fs.mkdir(path.dirname(filepath), { recursive: true });
40
- await fs.writeFile(filepath, buffer);
23
+ export async function captureGameScreenshot(page: Page, name: string): Promise<Buffer> {
24
+ const canvasElement = page.locator('canvas');
25
+ if (await canvasElement.count() > 0) {
26
+ return await canvasElement.screenshot({ path: `${name}.png` });
27
+ }
28
+ return await page.screenshot({ path: `${name}.png` });
41
29
  }
42
30
 
43
31
  /**
44
- * Compares a canvas state against a baseline image file.
45
- * Returns true if they match (within optional threshold logic, currently strict pixel match).
46
- * If baseline does not exist, it saves the current state as baseline and returns true.
32
+ * Saves a canvas to a PNG file.
47
33
  */
48
- export async function compareScreenshot(canvas: Canvas | HTMLCanvasElement, baselinePath: string): Promise<boolean> {
49
- try {
50
- await fs.access(baselinePath);
51
- } catch {
52
- // Baseline doesn't exist, save current as baseline
53
- console.warn(`Baseline not found at ${baselinePath}, saving current as baseline.`);
54
- await takeScreenshot(canvas, baselinePath);
55
- return true;
56
- }
57
-
58
- // Load baseline
59
- const baselineBuffer = await fs.readFile(baselinePath);
60
- const baselineImage = new Image();
61
- baselineImage.src = baselineBuffer;
62
-
63
- // Get dimensions (assume match for now, or fail)
64
- const width = baselineImage.width;
65
- const height = baselineImage.height;
66
-
67
- // Get current image data
68
- // We need to draw both to canvases to get pixel data easily with @napi-rs/canvas
69
- // If input is already a canvas, we can use it.
70
-
71
- // Helper to get buffer from input canvas
72
- let currentBuffer: Buffer;
73
- if ('toBuffer' in canvas && typeof canvas.toBuffer === 'function') {
74
- currentBuffer = canvas.toBuffer('image/png');
75
- } else if ('toDataURL' in canvas) {
76
- const dataUrl = canvas.toDataURL('image/png');
77
- currentBuffer = Buffer.from(dataUrl.replace(/^data:image\/png;base64,/, ''), 'base64');
78
- } else {
79
- throw new Error('Unsupported canvas type');
80
- }
81
-
82
- // Simple Buffer comparison first (fastest)
83
- if (baselineBuffer.equals(currentBuffer)) {
84
- return true;
85
- }
86
-
87
- // If buffers differ, it could be metadata or compression. Do pixel check.
88
- // Note: Creating a new canvas to draw the image onto for pixel access
89
- // This requires the 'Canvas' constructor which we imported.
90
- const baselineCanvas = new Canvas(width, height);
91
- const ctx = baselineCanvas.getContext('2d');
92
- ctx.drawImage(baselineImage, 0, 0);
93
- const baselineData = ctx.getImageData(0, 0, width, height).data;
94
-
95
- // Load current buffer to image to draw (handles JSDOM/napi differences uniformally)
96
- const currentImage = new Image();
97
- currentImage.src = currentBuffer;
98
-
99
- if (currentImage.width !== width || currentImage.height !== height) {
100
- console.error(`Dimension mismatch: Baseline ${width}x${height} vs Current ${currentImage.width}x${currentImage.height}`);
101
- return false;
102
- }
103
-
104
- const currentCanvas = new Canvas(width, height);
105
- const ctx2 = currentCanvas.getContext('2d');
106
- ctx2.drawImage(currentImage, 0, 0);
107
- const currentData = ctx2.getImageData(0, 0, width, height).data;
108
-
109
- let diffPixels = 0;
110
- const totalPixels = width * height;
111
-
112
- // Simple pixel diff
113
- for (let i = 0; i < baselineData.length; i += 4) {
114
- if (baselineData[i] !== currentData[i] || // R
115
- baselineData[i+1] !== currentData[i+1] || // G
116
- baselineData[i+2] !== currentData[i+2] || // B
117
- baselineData[i+3] !== currentData[i+3]) { // A
118
- diffPixels++;
119
- }
120
- }
121
-
122
- if (diffPixels > 0) {
123
- console.error(`Visual regression: ${diffPixels} pixels differ (${(diffPixels/totalPixels*100).toFixed(2)}%)`);
124
- // Save diff image? (Optional, skipping for now)
125
- return false;
126
- }
127
-
128
- return true;
34
+ export async function takeScreenshot(canvas: Canvas | HTMLCanvasElement, filepath: string): Promise<void> {
35
+ let buffer: Buffer;
36
+
37
+ // Handle @napi-rs/canvas Canvas object
38
+ if ('toBuffer' in canvas && typeof canvas.toBuffer === 'function') {
39
+ buffer = canvas.toBuffer('image/png');
40
+ }
41
+ // Handle JSDOM HTMLCanvasElement
42
+ else if ('toDataURL' in canvas) {
43
+ const dataUrl = canvas.toDataURL('image/png');
44
+ const base64 = dataUrl.replace(/^data:image\/png;base64,/, '');
45
+ buffer = Buffer.from(base64, 'base64');
46
+ } else {
47
+ throw new Error('Unsupported canvas type for screenshot');
48
+ }
49
+
50
+ await fs.mkdir(path.dirname(filepath), { recursive: true });
51
+ await fs.writeFile(filepath, buffer);
129
52
  }
130
53
 
131
54
  /**
132
- * Compares two screenshots (Buffers).
133
- * Uses simple buffer check. Kept for backward compatibility or direct buffer comparison.
55
+ * Compares two image buffers using pixelmatch.
134
56
  */
135
57
  export function compareScreenshots(baseline: Buffer, current: Buffer, threshold: number = 0.01): VisualDiff {
136
- // Basic length check first
137
- if (baseline.length !== current.length) {
138
- return { diffPercentage: 1.0 };
58
+ try {
59
+ const img1 = PNG.sync.read(baseline);
60
+ const img2 = PNG.sync.read(current);
61
+
62
+ if (img1.width !== img2.width || img1.height !== img2.height) {
63
+ throw new Error(`Image dimensions do not match: ${img1.width}x${img1.height} vs ${img2.width}x${img2.height}`);
139
64
  }
140
65
 
141
- let diffPixels = 0;
142
- const totalPixels = baseline.length; // Approximate bytes
66
+ const { width, height } = img1;
67
+ const diff = new PNG({ width, height });
143
68
 
144
- for (let i = 0; i < baseline.length; i++) {
145
- if (baseline[i] !== current[i]) {
146
- diffPixels++;
147
- }
148
- }
69
+ const diffPixels = pixelmatch(
70
+ img1.data,
71
+ img2.data,
72
+ diff.data,
73
+ width,
74
+ height,
75
+ { threshold }
76
+ );
149
77
 
150
- const diffPercentage = diffPixels / totalPixels;
78
+ const diffPercentage = diffPixels / (width * height);
79
+ const isMatch = diffPixels === 0;
151
80
 
152
81
  return {
153
- diffPercentage,
82
+ diffPixels,
83
+ diffPercentage,
84
+ isMatch,
85
+ diffImage: PNG.sync.write(diff)
154
86
  };
155
- }
156
-
157
- export interface VisualScenario {
158
- sceneName: string;
159
- setup: () => Promise<void>;
87
+ } catch (error) {
88
+ // Fallback or error handling
89
+ console.error('Error comparing screenshots:', error);
90
+ return {
91
+ diffPixels: -1,
92
+ diffPercentage: 1.0,
93
+ isMatch: false
94
+ };
95
+ }
160
96
  }
161
97
 
162
98
  /**
163
99
  * Creates a visual test scenario.
164
100
  */
165
101
  export function createVisualTestScenario(sceneName: string): VisualScenario {
166
- return {
167
- sceneName,
168
- setup: async () => {
169
- // Setup scene logic
170
- }
171
- };
102
+ return {
103
+ sceneName
104
+ };
172
105
  }
@@ -157,6 +157,23 @@ export function createPlayerEntityFactory(overrides: Partial<Entity> = {}): Part
157
157
  movetype: MoveType.Walk,
158
158
  svflags: ServerFlags.Player,
159
159
  viewheight: 22,
160
+ client: {
161
+ inventory: {
162
+ ammo: { counts: [], caps: [] },
163
+ ownedWeapons: new Set(),
164
+ powerups: new Map(),
165
+ keys: new Set(),
166
+ items: new Set()
167
+ },
168
+ weaponStates: {
169
+ currentWeapon: null,
170
+ lastFireTime: 0,
171
+ weaponFrame: 0,
172
+ weaponIdleTime: 0,
173
+ states: new Map(), // Initialize states map correctly
174
+ activeWeaponId: null
175
+ }
176
+ } as any,
160
177
  ...overrides
161
178
  });
162
179
  }
@@ -228,12 +228,8 @@ export function createPhysicsTestContext(bspModel?: BspModel): TestContext {
228
228
  // For now, we'll just store the model on the context if we extended TestContext,
229
229
  // but the task specifically asks to "Include collision world, traces".
230
230
 
231
- // In a real scenario, we might want to hook up a real BSP trace function here
231
+ // In a real scenario, we might want to hook up a real BSP physics engine mock here
232
232
  // or a mock that uses the BSP data.
233
- // Since we don't have a full BSP physics engine mock ready to drop in,
234
- // we will stick with the default trace mock which is already set up in createTestContext,
235
- // but we acknowledge the bspModel parameter for future expansion where we might
236
- // use it to seed the trace results.
237
233
  }
238
234
 
239
235
  return context;
@@ -308,3 +304,37 @@ export function createGameImportsAndEngine(overrides?: {
308
304
 
309
305
  return { imports, engine };
310
306
  }
307
+
308
+ /**
309
+ * Creates a mock GameExports object with mocked properties.
310
+ * This is useful for testing game logic that consumes the game object.
311
+ *
312
+ * @param overrides Optional overrides for the game object properties
313
+ */
314
+ export function createMockGameExports(overrides: Partial<any> = {}): any {
315
+ return {
316
+ init: vi.fn(),
317
+ shutdown: vi.fn(),
318
+ frame: vi.fn().mockReturnValue({ state: {} }),
319
+ clientThink: vi.fn(),
320
+ time: 0,
321
+ spawnWorld: vi.fn(),
322
+ deathmatch: false,
323
+ coop: false,
324
+ gameImports: {},
325
+ gameEngine: {},
326
+ entities: {
327
+ spawn: vi.fn(),
328
+ free: vi.fn(),
329
+ find: vi.fn(),
330
+ findByClassname: vi.fn(),
331
+ findByRadius: vi.fn(() => []),
332
+ forEachEntity: vi.fn(),
333
+ timeSeconds: 0,
334
+ ...overrides.entities,
335
+ },
336
+ multicast: vi.fn(),
337
+ unicast: vi.fn(),
338
+ ...overrides
339
+ };
340
+ }
package/src/index.ts CHANGED
@@ -12,7 +12,7 @@ export * from './game/helpers/save.js';
12
12
  export * from './game/mocks/ai.js';
13
13
  export * from './game/mocks/combat.js';
14
14
  export * from './game/mocks/items.js';
15
- export * from './game/mocks.js';
15
+ // Removed export * from './game/mocks.js';
16
16
  export * from './server/mocks/transport.js';
17
17
  export * from './server/mockTransport.js';
18
18
  export * from './server/mockNetDriver.js';
@@ -4,11 +4,27 @@ import { Entity } from '@quake2ts/game';
4
4
  import { vi } from 'vitest';
5
5
  import { createMockNetDriver } from './transport.js';
6
6
 
7
- // Define GameState interface locally or import from where it should be.
8
- // Based on grep, GameStateSnapshot is in @quake2ts/game.
9
- // But test-utils/src/game/mocks.ts defines a local GameState interface.
10
- // Let's use that one or define a compatible one here.
11
- import { GameState } from '../../game/mocks.js';
7
+ // Define GameState interface locally as we removed game/mocks.ts
8
+ export interface GameState {
9
+ levelName: string;
10
+ time: number;
11
+ entities: Entity[];
12
+ clients: any[]; // Mock client objects
13
+ }
14
+
15
+ /**
16
+ * Creates a mock game state object.
17
+ * @param overrides Optional overrides for the game state.
18
+ */
19
+ export function createMockGameState(overrides?: Partial<GameState>): GameState {
20
+ return {
21
+ levelName: 'test_level',
22
+ time: 0,
23
+ entities: [],
24
+ clients: [],
25
+ ...overrides
26
+ };
27
+ }
12
28
 
13
29
  /**
14
30
  * Creates a mock server state object.
@@ -152,11 +168,3 @@ export function createMockServer(overrides?: Partial<MockServer>): MockServer {
152
168
  ...overrides
153
169
  };
154
170
  }
155
-
156
- // Re-export GameState from game mocks if needed, or use the one from game/mocks
157
- // Since we have a createMockGameState in game/mocks.ts, we should probably use that or alias it.
158
- // The task says "Add createMockGameState() factory".
159
- // If it is already in game/mocks.ts, we can just export it from there or re-export it here.
160
- // But to avoid duplication, I will just re-export the one from game mocks if the intention is to have it available under server utils.
161
-
162
- export { createMockGameState, type GameState } from '../../game/mocks.js';
package/src/game/mocks.ts DELETED
@@ -1,105 +0,0 @@
1
- import { Entity, GameExports, GameImports } from '@quake2ts/game';
2
- import { vi } from 'vitest';
3
-
4
- /**
5
- * Interface for mock GameState.
6
- */
7
- export interface GameState {
8
- levelName: string;
9
- time: number;
10
- entities: Entity[];
11
- clients: any[]; // Mock client objects
12
- }
13
-
14
- /**
15
- * Creates a mock game state object.
16
- * @param overrides Optional overrides for the game state.
17
- */
18
- export function createMockGameState(overrides?: Partial<GameState>): GameState {
19
- return {
20
- levelName: 'test_level',
21
- time: 0,
22
- entities: [],
23
- clients: [],
24
- ...overrides
25
- };
26
- }
27
-
28
- /**
29
- * Creates a mock GameExports object.
30
- */
31
- export function createMockGameExports(overrides?: Partial<GameExports>): GameExports {
32
- return {
33
- init: vi.fn(),
34
- shutdown: vi.fn(),
35
- spawnWorld: vi.fn(),
36
- frame: vi.fn().mockReturnValue({ state: {} }),
37
- clientConnect: vi.fn().mockReturnValue(true),
38
- clientBegin: vi.fn().mockReturnValue({ index: 1, origin: { x: 0, y: 0, z: 0 } }),
39
- clientDisconnect: vi.fn(),
40
- clientThink: vi.fn(),
41
- respawn: vi.fn(),
42
- entities: {
43
- getByIndex: vi.fn(),
44
- forEachEntity: vi.fn(),
45
- findByRadius: vi.fn(),
46
- find: vi.fn(),
47
- checkAnyCollision: vi.fn(),
48
- trace: vi.fn(),
49
- pointcontents: vi.fn(),
50
- link: vi.fn(),
51
- unlink: vi.fn(),
52
- spawn: vi.fn(),
53
- free: vi.fn(),
54
- activeCount: 0,
55
- world: { classname: 'worldspawn' } as any,
56
- } as any,
57
- multicast: vi.fn(),
58
- unicast: vi.fn(),
59
- configstring: vi.fn(),
60
- serverCommand: vi.fn(),
61
- sound: vi.fn(),
62
- soundIndex: vi.fn(),
63
- centerprintf: vi.fn(),
64
- trace: vi.fn(),
65
- time: 0,
66
- deathmatch: false,
67
- skill: 1,
68
- rogue: false,
69
- xatrix: false,
70
- coop: false,
71
- friendlyFire: false,
72
- random: {
73
- next: vi.fn(),
74
- nextFloat: vi.fn(),
75
- range: vi.fn(),
76
- crandom: vi.fn(),
77
- getState: vi.fn(),
78
- setState: vi.fn()
79
- } as any,
80
- createSave: vi.fn(),
81
- loadSave: vi.fn(),
82
- serialize: vi.fn(),
83
- loadState: vi.fn(),
84
- setGodMode: vi.fn(),
85
- setNoclip: vi.fn(),
86
- setNotarget: vi.fn(),
87
- giveItem: vi.fn(),
88
- damage: vi.fn(),
89
- teleport: vi.fn(),
90
- registerHooks: vi.fn(),
91
- hooks: {
92
- onMapLoad: vi.fn(),
93
- onMapUnload: vi.fn(),
94
- onPlayerSpawn: vi.fn(),
95
- onPlayerDeath: vi.fn(),
96
- register: vi.fn(),
97
- onPickup: vi.fn(), // Added onPickup mock
98
- } as any,
99
- setSpectator: vi.fn(),
100
- registerEntitySpawn: vi.fn(),
101
- unregisterEntitySpawn: vi.fn(),
102
- getCustomEntities: vi.fn(),
103
- ...overrides
104
- };
105
- }