@quake2ts/test-utils 0.0.873 → 0.0.874

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.873",
3
+ "version": "0.0.874",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -55,10 +55,10 @@
55
55
  "serve-handler": "^6.1.6",
56
56
  "vitest": "^1.6.0",
57
57
  "webgpu": "^0.3.8",
58
- "@quake2ts/engine": "^0.0.873",
59
- "@quake2ts/game": "0.0.873",
60
- "@quake2ts/server": "0.0.873",
61
- "@quake2ts/shared": "0.0.873"
58
+ "@quake2ts/engine": "^0.0.874",
59
+ "@quake2ts/server": "0.0.874",
60
+ "@quake2ts/game": "0.0.874",
61
+ "@quake2ts/shared": "0.0.874"
62
62
  },
63
63
  "peerDependenciesMeta": {
64
64
  "@quake2ts/engine": {
@@ -114,10 +114,10 @@
114
114
  "typescript": "^5.9.3",
115
115
  "vitest": "^4.0.16",
116
116
  "webgpu": "^0.3.8",
117
- "@quake2ts/engine": "^0.0.873",
118
- "@quake2ts/game": "0.0.873",
119
- "@quake2ts/shared": "0.0.873",
120
- "@quake2ts/server": "0.0.873"
117
+ "@quake2ts/game": "0.0.874",
118
+ "@quake2ts/engine": "^0.0.874",
119
+ "@quake2ts/server": "0.0.874",
120
+ "@quake2ts/shared": "0.0.874"
121
121
  },
122
122
  "dependencies": {
123
123
  "upng-js": "^2.1.0"
@@ -28,6 +28,15 @@ export interface WebGLPlaywrightOptions {
28
28
  headless?: boolean;
29
29
  }
30
30
 
31
+ // Singleton state for reusing the browser/server across tests
32
+ let sharedSetup: {
33
+ browser: Browser;
34
+ context: BrowserContext;
35
+ page: Page;
36
+ server: any;
37
+ serverUrl: string;
38
+ } | undefined;
39
+
31
40
  function findWorkspaceRoot(startDir: string): string {
32
41
  let currentDir = startDir;
33
42
  while (currentDir !== path.parse(currentDir).root) {
@@ -51,6 +60,34 @@ export async function createWebGLPlaywrightSetup(
51
60
  const height = options.height ?? 256;
52
61
  const headless = options.headless ?? true;
53
62
 
63
+ // Re-use existing setup if available
64
+ if (sharedSetup) {
65
+ const { page, browser, context, server } = sharedSetup;
66
+
67
+ // Ensure the page is still valid (not crashed)
68
+ if (!page.isClosed()) {
69
+ // Resize viewport to match current test requirements
70
+ await page.setViewportSize({ width, height });
71
+
72
+ return {
73
+ browser,
74
+ context,
75
+ page,
76
+ width,
77
+ height,
78
+ server,
79
+ // No-op cleanup for shared instance to keep it alive for next test
80
+ cleanup: async () => {
81
+ // We intentionally do not close the browser/server here.
82
+ // It will be closed when the Node process exits.
83
+ }
84
+ };
85
+ } else {
86
+ // If page is closed/crashed, discard shared setup and recreate
87
+ sharedSetup = undefined;
88
+ }
89
+ }
90
+
54
91
  // Dynamic imports for optional dependencies
55
92
  let chromium;
56
93
  let handler;
@@ -122,7 +159,7 @@ export async function createWebGLPlaywrightSetup(
122
159
  // Log browser console for debugging
123
160
  page.on('console', msg => {
124
161
  if (msg.type() === 'error') console.error(`[Browser Error] ${msg.text()}`);
125
- else console.log(`[Browser] ${msg.text()}`);
162
+ // else console.log(`[Browser] ${msg.text()}`); // Reduce noise
126
163
  });
127
164
 
128
165
  page.on('pageerror', err => {
@@ -138,9 +175,19 @@ export async function createWebGLPlaywrightSetup(
138
175
  // Wait for renderer to be ready
139
176
  await page.waitForFunction(() => (window as any).testRenderer !== undefined, { timeout: 5000 });
140
177
 
178
+ // Store as shared setup
179
+ sharedSetup = {
180
+ browser,
181
+ context,
182
+ page,
183
+ server: staticServer,
184
+ serverUrl
185
+ };
186
+
141
187
  const cleanup = async () => {
142
- await browser.close();
143
- staticServer.close();
188
+ // For the initial creator, we effectively "hand off" ownership to the global sharedSetup.
189
+ // So we don't close it here either, unless we want to implement ref-counting.
190
+ // For simplicity, we keep it open until process exit.
144
191
  };
145
192
 
146
193
  return {
@@ -184,9 +231,12 @@ export async function renderAndCaptureWebGLPlaywright(
184
231
 
185
232
  // Resize canvas if needed
186
233
  if (width !== undefined && height !== undefined) {
187
- canvas.width = width;
188
- canvas.height = height;
189
- gl.viewport(0, 0, width, height);
234
+ // Only resize if actually changed to avoid flicker/overhead
235
+ if (canvas.width !== width || canvas.height !== height) {
236
+ canvas.width = width;
237
+ canvas.height = height;
238
+ gl.viewport(0, 0, width, height);
239
+ }
190
240
  }
191
241
 
192
242
  try {
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ export * from './shared/pak-loader.js';
5
5
  export * from './shared/math.js';
6
6
  export * from './shared/collision.js';
7
7
  export * from './shared/factories.js';
8
+ export * from './shared/md2.js';
8
9
  export * from './game/factories.js';
9
10
  export * from './game/helpers.js';
10
11
  export * from './game/helpers/spawn.js';
@@ -105,7 +106,6 @@ export type {
105
106
  export type { TraceMock, SurfaceMock } from './shared/collision.js';
106
107
  export type { Transform } from './shared/math.js';
107
108
  export type {
108
- CaptureOptions,
109
109
  ComparisonResult,
110
110
  ComparisonOptions,
111
111
  SnapshotTestOptions
@@ -1,6 +1,7 @@
1
1
  // Remove top-level import to avoid runtime crash when gl is missing
2
2
  // import createGL from 'gl';
3
3
  import type { WebGLContextState } from '@quake2ts/engine';
4
+ import { createRequire } from 'module';
4
5
 
5
6
  export interface HeadlessWebGLOptions {
6
7
  width?: number;
@@ -28,10 +29,10 @@ export function createHeadlessWebGL(
28
29
 
29
30
  let createGL;
30
31
  try {
31
- // eslint-disable-next-line @typescript-eslint/no-require-imports
32
+ const require = createRequire(import.meta.url);
32
33
  createGL = require('gl');
33
34
  } catch (e) {
34
- throw new Error('gl package not found or failed to load. Install it to run WebGL tests.');
35
+ throw new Error('gl package not found or failed to load. Install it to run WebGL tests. Error: ' + e);
35
36
  }
36
37
 
37
38
  // Create WebGL context using 'gl' package
@@ -0,0 +1,143 @@
1
+ import { ANORMS, Vec3 } from '@quake2ts/shared';
2
+ import {
3
+ Md2Frame,
4
+ Md2GlCommand,
5
+ Md2GlCommandVertex,
6
+ Md2Header,
7
+ Md2Model,
8
+ Md2Skin,
9
+ Md2TexCoord,
10
+ Md2Triangle,
11
+ Md2Vertex
12
+ } from '@quake2ts/engine';
13
+
14
+ export function createSimpleMd2Model(): Md2Model {
15
+ // Create a simple cube model
16
+ // 8 vertices
17
+ const scale: Vec3 = { x: 1, y: 1, z: 1 };
18
+ const translate: Vec3 = { x: 0, y: 0, z: 0 };
19
+
20
+ // Vertices for a cube -10 to 10
21
+ const basePositions: Vec3[] = [
22
+ { x: -10, y: -10, z: -10 }, // 0
23
+ { x: 10, y: -10, z: -10 }, // 1
24
+ { x: 10, y: 10, z: -10 }, // 2
25
+ { x: -10, y: 10, z: -10 }, // 3
26
+ { x: -10, y: -10, z: 10 }, // 4
27
+ { x: 10, y: -10, z: 10 }, // 5
28
+ { x: 10, y: 10, z: 10 }, // 6
29
+ { x: -10, y: 10, z: 10 }, // 7
30
+ ];
31
+
32
+ // Helper to create a frame with slight modification for animation
33
+ const createFrame = (name: string, offset: number): Md2Frame => {
34
+ const vertices: Md2Vertex[] = basePositions.map((pos, index) => {
35
+ // Simple animation: move vertices along normals based on offset
36
+ // For a cube, normals are axis aligned. We'll just fake it.
37
+ const modPos = {
38
+ x: pos.x + (index % 2 === 0 ? offset : -offset),
39
+ y: pos.y,
40
+ z: pos.z
41
+ };
42
+ return {
43
+ position: modPos,
44
+ normalIndex: 0, // Dummy normal index
45
+ normal: { x: 0, y: 0, z: 1 } // Dummy normal
46
+ };
47
+ });
48
+
49
+ return {
50
+ name,
51
+ vertices,
52
+ minBounds: { x: -20, y: -20, z: -20 },
53
+ maxBounds: { x: 20, y: 20, z: 20 }
54
+ };
55
+ };
56
+
57
+ // Create 20 frames
58
+ const frames: Md2Frame[] = [];
59
+ for (let i = 0; i < 20; i++) {
60
+ frames.push(createFrame(`frame${i}`, i * 0.5));
61
+ }
62
+
63
+ // Create simple triangles (cube faces)
64
+ // 6 faces * 2 triangles = 12 triangles
65
+ const triangles: Md2Triangle[] = [
66
+ // Front
67
+ { vertexIndices: [0, 1, 2], texCoordIndices: [0, 1, 2] },
68
+ { vertexIndices: [0, 2, 3], texCoordIndices: [0, 2, 3] },
69
+ // Back
70
+ { vertexIndices: [5, 4, 7], texCoordIndices: [1, 0, 3] },
71
+ { vertexIndices: [5, 7, 6], texCoordIndices: [1, 3, 2] },
72
+ // Top
73
+ { vertexIndices: [3, 2, 6], texCoordIndices: [0, 1, 2] },
74
+ { vertexIndices: [3, 6, 7], texCoordIndices: [0, 2, 3] },
75
+ // Bottom
76
+ { vertexIndices: [4, 5, 1], texCoordIndices: [0, 1, 2] },
77
+ { vertexIndices: [4, 1, 0], texCoordIndices: [0, 2, 3] },
78
+ // Right
79
+ { vertexIndices: [1, 5, 6], texCoordIndices: [0, 1, 2] },
80
+ { vertexIndices: [1, 6, 2], texCoordIndices: [0, 2, 3] },
81
+ // Left
82
+ { vertexIndices: [4, 0, 3], texCoordIndices: [0, 1, 2] },
83
+ { vertexIndices: [4, 3, 7], texCoordIndices: [0, 2, 3] },
84
+ ];
85
+
86
+ // Dummy GL Commands (Strip/Fan)
87
+ // For simplicity we can just make GL commands match triangles or leave empty if the renderer supports raw triangles
88
+ // The renderer likely uses glCommands if available. Let's create a simple strip for each triangle.
89
+ const glCommands: Md2GlCommand[] = triangles.map(t => ({
90
+ mode: 'strip', // or 'fan', doesn't matter for 3 verts
91
+ vertices: [
92
+ { s: 0, t: 0, vertexIndex: t.vertexIndices[0] },
93
+ { s: 1, t: 0, vertexIndex: t.vertexIndices[1] },
94
+ { s: 1, t: 1, vertexIndex: t.vertexIndices[2] }
95
+ ]
96
+ }));
97
+
98
+ const header: Md2Header = {
99
+ ident: 844121161,
100
+ version: 8,
101
+ skinWidth: 32,
102
+ skinHeight: 32,
103
+ frameSize: 40 + 8 * 4, // header + verts * 4
104
+ numSkins: 1,
105
+ numVertices: 8,
106
+ numTexCoords: 4, // Simplified
107
+ numTriangles: 12,
108
+ numGlCommands: 12, // One per triangle for simplicity
109
+ numFrames: 20,
110
+ offsetSkins: 0,
111
+ offsetTexCoords: 0,
112
+ offsetTriangles: 0,
113
+ offsetFrames: 0,
114
+ offsetGlCommands: 0,
115
+ offsetEnd: 0,
116
+ magic: 844121161
117
+ };
118
+
119
+ return {
120
+ header,
121
+ skins: [{ name: 'skin.pcx' }],
122
+ texCoords: [
123
+ { s: 0, t: 0 },
124
+ { s: 32, t: 0 },
125
+ { s: 32, t: 32 },
126
+ { s: 0, t: 32 }
127
+ ],
128
+ triangles,
129
+ frames,
130
+ glCommands
131
+ };
132
+ }
133
+
134
+ export async function loadMd2Model(filename: string): Promise<Md2Model> {
135
+ // In a real scenario this might load from disk or a pak.
136
+ // For tests, we'll return the procedural model if the name matches 'simple-cube.md2'
137
+ if (filename === 'simple-cube.md2') {
138
+ return createSimpleMd2Model();
139
+ }
140
+
141
+ // TODO: Add logic to load from actual fixtures if needed
142
+ throw new Error(`Model ${filename} not found in test fixtures`);
143
+ }