@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/dist/index.cjs +165 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +159 -15
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
- package/src/engine/helpers/webgl-playwright.ts +56 -6
- package/src/index.ts +1 -1
- package/src/setup/headless-webgl.ts +3 -2
- package/src/shared/md2.ts +143 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quake2ts/test-utils",
|
|
3
|
-
"version": "0.0.
|
|
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.
|
|
59
|
-
"@quake2ts/
|
|
60
|
-
"@quake2ts/
|
|
61
|
-
"@quake2ts/shared": "0.0.
|
|
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/
|
|
118
|
-
"@quake2ts/
|
|
119
|
-
"@quake2ts/
|
|
120
|
-
"@quake2ts/
|
|
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
|
-
|
|
143
|
-
|
|
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
|
-
|
|
188
|
-
canvas.height
|
|
189
|
-
|
|
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
|
-
|
|
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
|
+
}
|