@quake2ts/test-utils 0.0.805 → 0.0.808

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 CHANGED
@@ -88,6 +88,7 @@ __export(index_exports, {
88
88
  createInputInjector: () => createInputInjector,
89
89
  createInterpolationTestData: () => createInterpolationTestData,
90
90
  createItemEntityFactory: () => createItemEntityFactory,
91
+ createLoggingRenderer: () => createLoggingRenderer,
91
92
  createMessageReaderMock: () => createMessageReaderMock,
92
93
  createMessageWriterMock: () => createMessageWriterMock,
93
94
  createMockAI: () => createMockAI,
@@ -206,6 +207,7 @@ __export(index_exports, {
206
207
  createMonsterEntityFactory: () => createMonsterEntityFactory,
207
208
  createMultiplayerTestScenario: () => createMultiplayerTestScenario,
208
209
  createNetChanMock: () => createNetChanMock,
210
+ createNullRenderer: () => createNullRenderer,
209
211
  createPacketMock: () => createPacketMock,
210
212
  createPhysicsTestContext: () => createPhysicsTestContext,
211
213
  createPhysicsTestScenario: () => createPhysicsTestScenario,
@@ -233,6 +235,8 @@ __export(index_exports, {
233
235
  createWebGLPlaywrightSetup: () => createWebGLPlaywrightSetup,
234
236
  createWebGLRenderTestSetup: () => createWebGLRenderTestSetup,
235
237
  expectAnimationSnapshot: () => expectAnimationSnapshot,
238
+ expectNoDoubleTransform: () => expectNoDoubleTransform,
239
+ expectRendererCalls: () => expectRendererCalls,
236
240
  expectSnapshot: () => expectSnapshot,
237
241
  findPakFile: () => findPakFile,
238
242
  flipPixelsVertically: () => flipPixelsVertically,
@@ -292,6 +296,7 @@ __export(index_exports, {
292
296
  teardownNodeEnvironment: () => teardownNodeEnvironment,
293
297
  testComputeShader: () => testComputeShader,
294
298
  testPipelineRendering: () => testPipelineRendering,
299
+ testWebGLAnimation: () => testWebGLAnimation,
295
300
  testWebGLRenderer: () => testWebGLRenderer,
296
301
  throttleBandwidth: () => throttleBandwidth,
297
302
  verifySmoothing: () => verifySmoothing,
@@ -4543,9 +4548,153 @@ async function renderAndExpectSnapshot(setup, renderFn, options) {
4543
4548
  });
4544
4549
  }
4545
4550
 
4551
+ // src/visual/animation-snapshots.ts
4552
+ var import_upng_js = __toESM(require("upng-js"), 1);
4553
+ var import_promises2 = __toESM(require("fs/promises"), 1);
4554
+ var import_fs2 = require("fs");
4555
+ var import_path2 = __toESM(require("path"), 1);
4556
+ async function loadAPNG(filepath) {
4557
+ const buffer = await import_promises2.default.readFile(filepath);
4558
+ const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
4559
+ const img = import_upng_js.default.decode(arrayBuffer);
4560
+ const framesRGBA = import_upng_js.default.toRGBA8(img);
4561
+ const frames = framesRGBA.map((buffer2) => new Uint8ClampedArray(buffer2));
4562
+ return {
4563
+ width: img.width,
4564
+ height: img.height,
4565
+ frames
4566
+ };
4567
+ }
4568
+ async function saveAPNG(filepath, frames, width, height, delayMs) {
4569
+ const buffers = frames.map((f) => {
4570
+ const dst = new ArrayBuffer(f.byteLength);
4571
+ new Uint8ClampedArray(dst).set(f);
4572
+ return dst;
4573
+ });
4574
+ const delays = new Array(frames.length).fill(delayMs);
4575
+ const pngBuffer = import_upng_js.default.encode(buffers, width, height, 0, delays);
4576
+ await import_promises2.default.mkdir(import_path2.default.dirname(filepath), { recursive: true });
4577
+ await import_promises2.default.writeFile(filepath, Buffer.from(pngBuffer));
4578
+ }
4579
+ async function expectAnimationSnapshot(renderAndCaptureFrame, options) {
4580
+ const {
4581
+ name,
4582
+ width,
4583
+ height,
4584
+ frameCount,
4585
+ fps = 10,
4586
+ updateBaseline = false,
4587
+ snapshotDir = import_path2.default.join(process.cwd(), "tests", "__snapshots__"),
4588
+ threshold = 0.1,
4589
+ maxDifferencePercent = 0.1
4590
+ } = options;
4591
+ if (!width || !height) {
4592
+ throw new Error("Width and height are required for expectAnimationSnapshot");
4593
+ }
4594
+ const baselinePath = getSnapshotPath(name, "baseline", snapshotDir);
4595
+ const actualPath = getSnapshotPath(name, "actual", snapshotDir);
4596
+ const diffPath = getSnapshotPath(name, "diff", snapshotDir);
4597
+ const alwaysSave = process.env.ALWAYS_SAVE_SNAPSHOTS === "1";
4598
+ const actualFrames = [];
4599
+ for (let i = 0; i < frameCount; i++) {
4600
+ const frameData = await renderAndCaptureFrame(i);
4601
+ if (frameData.length !== width * height * 4) {
4602
+ throw new Error(`Frame ${i} dimension mismatch: expected length ${width * height * 4}, got ${frameData.length}`);
4603
+ }
4604
+ actualFrames.push(frameData);
4605
+ }
4606
+ const delayMs = 1e3 / fps;
4607
+ let baselineFrames = null;
4608
+ let shouldUpdateBaseline = updateBaseline || !(0, import_fs2.existsSync)(baselinePath);
4609
+ if (!shouldUpdateBaseline) {
4610
+ try {
4611
+ const baseline = await loadAPNG(baselinePath);
4612
+ if (baseline.width !== width || baseline.height !== height) {
4613
+ console.warn(`Baseline dimensions mismatch (${baseline.width}x${baseline.height} vs ${width}x${height}). Forcing update.`);
4614
+ shouldUpdateBaseline = true;
4615
+ } else if (baseline.frames.length !== frameCount) {
4616
+ console.warn(`Baseline frame count mismatch (${baseline.frames.length} vs ${frameCount}). Forcing update.`);
4617
+ shouldUpdateBaseline = true;
4618
+ } else {
4619
+ baselineFrames = baseline.frames;
4620
+ }
4621
+ } catch (e) {
4622
+ console.warn(`Failed to load baseline APNG: ${e}. Forcing update.`);
4623
+ shouldUpdateBaseline = true;
4624
+ }
4625
+ }
4626
+ if (shouldUpdateBaseline) {
4627
+ console.log(`Creating/Updating baseline for ${name} at ${baselinePath}`);
4628
+ await saveAPNG(baselinePath, actualFrames, width, height, delayMs);
4629
+ return;
4630
+ }
4631
+ if (!baselineFrames) {
4632
+ throw new Error("Baseline frames missing despite checks.");
4633
+ }
4634
+ const frameStats = [];
4635
+ const diffFrames = [];
4636
+ let totalDiffPixels = 0;
4637
+ let totalPixels = 0;
4638
+ for (let i = 0; i < frameCount; i++) {
4639
+ const result2 = await compareSnapshots(actualFrames[i], baselineFrames[i], width, height, options);
4640
+ frameStats.push(result2);
4641
+ if (result2.diffImage) {
4642
+ diffFrames.push(result2.diffImage);
4643
+ } else {
4644
+ diffFrames.push(new Uint8ClampedArray(width * height * 4));
4645
+ }
4646
+ totalDiffPixels += result2.pixelsDifferent;
4647
+ totalPixels += width * height;
4648
+ }
4649
+ const avgPercentDifferent = totalDiffPixels / totalPixels * 100;
4650
+ const passed = avgPercentDifferent <= (maxDifferencePercent || 0);
4651
+ const result = {
4652
+ passed,
4653
+ totalPixels,
4654
+ totalDiffPixels,
4655
+ percentDifferent: avgPercentDifferent,
4656
+ frameStats
4657
+ };
4658
+ const statsPath = import_path2.default.join(snapshotDir, "stats", `${name}.json`);
4659
+ await import_promises2.default.mkdir(import_path2.default.dirname(statsPath), { recursive: true });
4660
+ await import_promises2.default.writeFile(statsPath, JSON.stringify({
4661
+ passed: result.passed,
4662
+ percentDifferent: result.percentDifferent,
4663
+ pixelsDifferent: result.totalDiffPixels,
4664
+ totalPixels: result.totalPixels,
4665
+ threshold: options.threshold ?? 0.1,
4666
+ maxDifferencePercent: options.maxDifferencePercent ?? 0.1,
4667
+ frameCount
4668
+ }, null, 2));
4669
+ if (!passed || alwaysSave) {
4670
+ await saveAPNG(actualPath, actualFrames, width, height, delayMs);
4671
+ await saveAPNG(diffPath, diffFrames, width, height, delayMs);
4672
+ }
4673
+ if (!passed) {
4674
+ const failThreshold = 10;
4675
+ const errorMessage = `Animation snapshot comparison failed for ${name}: ${result.percentDifferent.toFixed(2)}% different (${result.totalDiffPixels} pixels total). See ${diffPath} for details.`;
4676
+ if (result.percentDifferent <= failThreshold) {
4677
+ console.warn(`[WARNING] ${errorMessage} (Marked as failed in report but passing test execution due to <${failThreshold}% difference)`);
4678
+ } else {
4679
+ throw new Error(errorMessage);
4680
+ }
4681
+ }
4682
+ }
4683
+
4546
4684
  // src/engine/helpers/webgl-playwright.ts
4547
4685
  var import_http = require("http");
4548
- var import_path2 = __toESM(require("path"), 1);
4686
+ var import_path3 = __toESM(require("path"), 1);
4687
+ var import_fs3 = __toESM(require("fs"), 1);
4688
+ function findWorkspaceRoot(startDir) {
4689
+ let currentDir = startDir;
4690
+ while (currentDir !== import_path3.default.parse(currentDir).root) {
4691
+ if (import_fs3.default.existsSync(import_path3.default.join(currentDir, "pnpm-workspace.yaml"))) {
4692
+ return currentDir;
4693
+ }
4694
+ currentDir = import_path3.default.dirname(currentDir);
4695
+ }
4696
+ return process.cwd();
4697
+ }
4549
4698
  async function createWebGLPlaywrightSetup(options = {}) {
4550
4699
  const width = options.width ?? 256;
4551
4700
  const height = options.height ?? 256;
@@ -4564,7 +4713,7 @@ async function createWebGLPlaywrightSetup(options = {}) {
4564
4713
  } catch (e) {
4565
4714
  throw new Error('Failed to load "serve-handler" package. Please ensure it is installed.');
4566
4715
  }
4567
- const repoRoot = import_path2.default.resolve(__dirname, "../../../../..");
4716
+ const repoRoot = findWorkspaceRoot(__dirname);
4568
4717
  const staticServer = (0, import_http.createServer)((request, response) => {
4569
4718
  return handler(request, response, {
4570
4719
  public: repoRoot,
@@ -4628,9 +4777,9 @@ async function createWebGLPlaywrightSetup(options = {}) {
4628
4777
  cleanup
4629
4778
  };
4630
4779
  }
4631
- async function renderAndCaptureWebGLPlaywright(page, renderFn, width, height) {
4780
+ async function renderAndCaptureWebGLPlaywright(page, renderFn, width, height, frameIndex = 0) {
4632
4781
  try {
4633
- const pixelData = await page.evaluate(({ code, width: width2, height: height2 }) => {
4782
+ const pixelData = await page.evaluate(({ code, width: width2, height: height2, frameIndex: frameIndex2 }) => {
4634
4783
  const renderer = window.testRenderer;
4635
4784
  const gl = window.testGl;
4636
4785
  const canvas = window.testCanvas;
@@ -4643,8 +4792,8 @@ async function renderAndCaptureWebGLPlaywright(page, renderFn, width, height) {
4643
4792
  gl.viewport(0, 0, width2, height2);
4644
4793
  }
4645
4794
  try {
4646
- const fn = new Function("renderer", "gl", code);
4647
- fn(renderer, gl);
4795
+ const fn = new Function("renderer", "gl", "frameIndex", code);
4796
+ fn(renderer, gl, frameIndex2);
4648
4797
  } catch (err) {
4649
4798
  throw new Error(`Renderer Execution Error: ${err.message}
4650
4799
  Code:
@@ -4652,7 +4801,7 @@ ${code}`);
4652
4801
  }
4653
4802
  gl.finish();
4654
4803
  return window.captureCanvas();
4655
- }, { code: renderFn, width, height });
4804
+ }, { code: renderFn, width, height, frameIndex });
4656
4805
  return new Uint8ClampedArray(pixelData);
4657
4806
  } catch (err) {
4658
4807
  throw new Error(`Browser Test Error: ${err.message}`);
@@ -4679,6 +4828,33 @@ async function testWebGLRenderer(renderCode, options) {
4679
4828
  await setup.cleanup();
4680
4829
  }
4681
4830
  }
4831
+ async function testWebGLAnimation(renderCode, options) {
4832
+ const setup = await createWebGLPlaywrightSetup(options);
4833
+ try {
4834
+ await expectAnimationSnapshot(async (frameIndex) => {
4835
+ return renderAndCaptureWebGLPlaywright(
4836
+ setup.page,
4837
+ renderCode,
4838
+ options.width,
4839
+ options.height,
4840
+ frameIndex
4841
+ );
4842
+ }, {
4843
+ name: options.name,
4844
+ description: options.description,
4845
+ width: setup.width,
4846
+ height: setup.height,
4847
+ frameCount: options.frameCount,
4848
+ fps: options.fps,
4849
+ updateBaseline: options.updateBaseline,
4850
+ snapshotDir: options.snapshotDir,
4851
+ threshold: options.threshold,
4852
+ maxDifferencePercent: options.maxDifferencePercent
4853
+ });
4854
+ } finally {
4855
+ await setup.cleanup();
4856
+ }
4857
+ }
4682
4858
 
4683
4859
  // src/engine/helpers/pipeline-test-template.ts
4684
4860
  var import_vitest18 = require("vitest");
@@ -4778,9 +4954,31 @@ function createSolidTexture(width, height, color) {
4778
4954
  return data;
4779
4955
  }
4780
4956
 
4957
+ // src/engine/renderers.ts
4958
+ var import_engine8 = require("@quake2ts/engine");
4959
+ var import_vitest19 = require("vitest");
4960
+ function createNullRenderer(width = 800, height = 600) {
4961
+ return new import_engine8.NullRenderer(width, height);
4962
+ }
4963
+ function createLoggingRenderer(targetSystem = import_engine8.CoordinateSystem.QUAKE, options) {
4964
+ return new import_engine8.LoggingRenderer({
4965
+ targetSystem,
4966
+ ...options
4967
+ });
4968
+ }
4969
+ function expectRendererCalls(renderer, expectedCalls) {
4970
+ const actualCalls = renderer.getCallLog();
4971
+ (0, import_vitest19.expect)(actualCalls).toEqual(expectedCalls);
4972
+ }
4973
+ function expectNoDoubleTransform(renderer) {
4974
+ const logs = renderer.getLogs();
4975
+ const warnings = logs.filter((log) => log.includes("double-transform"));
4976
+ (0, import_vitest19.expect)(warnings).toHaveLength(0);
4977
+ }
4978
+
4781
4979
  // src/client/helpers/view.ts
4782
4980
  var import_gl_matrix = require("gl-matrix");
4783
- var import_engine8 = require("@quake2ts/engine");
4981
+ var import_engine9 = require("@quake2ts/engine");
4784
4982
  function toVec3(v) {
4785
4983
  if (v instanceof Float32Array && v.length === 3) {
4786
4984
  return v;
@@ -4794,7 +4992,7 @@ function toVec3(v) {
4794
4992
  return import_gl_matrix.vec3.create();
4795
4993
  }
4796
4994
  function createMockCamera(overrides = {}) {
4797
- const camera = new import_engine8.Camera();
4995
+ const camera = new import_engine9.Camera();
4798
4996
  if (overrides.position) {
4799
4997
  camera.position = toVec3(overrides.position);
4800
4998
  }
@@ -4882,7 +5080,7 @@ function simulateCameraMovement(camera, input, deltaTime) {
4882
5080
  }
4883
5081
 
4884
5082
  // src/client/helpers/hud.ts
4885
- var import_vitest19 = require("vitest");
5083
+ var import_vitest20 = require("vitest");
4886
5084
  function createMockHudState(overrides) {
4887
5085
  const defaultPs = {
4888
5086
  damageAlpha: 0,
@@ -4912,11 +5110,11 @@ function createMockHudState(overrides) {
4912
5110
  defaultStats[2] = 25;
4913
5111
  defaultStats[4] = 50;
4914
5112
  const defaultMessages = {
4915
- drawCenterPrint: import_vitest19.vi.fn(),
4916
- drawNotifications: import_vitest19.vi.fn(),
4917
- addCenterPrint: import_vitest19.vi.fn(),
4918
- addNotification: import_vitest19.vi.fn(),
4919
- clear: import_vitest19.vi.fn()
5113
+ drawCenterPrint: import_vitest20.vi.fn(),
5114
+ drawNotifications: import_vitest20.vi.fn(),
5115
+ addCenterPrint: import_vitest20.vi.fn(),
5116
+ addNotification: import_vitest20.vi.fn(),
5117
+ clear: import_vitest20.vi.fn()
4920
5118
  };
4921
5119
  return {
4922
5120
  ps: overrides?.ps ?? defaultPs,
@@ -4935,7 +5133,7 @@ function createMockHudState(overrides) {
4935
5133
  function createMockScoreboard(players = []) {
4936
5134
  return {
4937
5135
  players,
4938
- draw: import_vitest19.vi.fn()
5136
+ draw: import_vitest20.vi.fn()
4939
5137
  };
4940
5138
  }
4941
5139
  function createMockChatMessage(text, sender, timestamp = Date.now()) {
@@ -4954,7 +5152,7 @@ function createMockNotification(type, message, duration = 3e3) {
4954
5152
  }
4955
5153
 
4956
5154
  // src/client/mocks/network.ts
4957
- var import_engine9 = require("@quake2ts/engine");
5155
+ var import_engine10 = require("@quake2ts/engine");
4958
5156
  function createMockServerMessage(type, data = new Uint8Array()) {
4959
5157
  return { type, data };
4960
5158
  }
@@ -4966,7 +5164,7 @@ function createMockSnapshot(serverFrame, entities = [], playerState, deltaFrame
4966
5164
  areaBytes: 0,
4967
5165
  areaBits: new Uint8Array(),
4968
5166
  playerState: {
4969
- ...(0, import_engine9.createEmptyProtocolPlayerState)(),
5167
+ ...(0, import_engine10.createEmptyProtocolPlayerState)(),
4970
5168
  ...playerState
4971
5169
  },
4972
5170
  packetEntities: {
@@ -4983,7 +5181,7 @@ function createMockDeltaFrame(serverFrame, deltaFrame, entities = [], playerStat
4983
5181
  areaBytes: 0,
4984
5182
  areaBits: new Uint8Array(),
4985
5183
  playerState: {
4986
- ...(0, import_engine9.createEmptyProtocolPlayerState)(),
5184
+ ...(0, import_engine10.createEmptyProtocolPlayerState)(),
4987
5185
  ...playerState
4988
5186
  },
4989
5187
  packetEntities: {
@@ -5003,7 +5201,7 @@ function simulatePacketLoss(messages, lossPercent) {
5003
5201
  return messages.filter(() => Math.random() * 100 >= lossPercent);
5004
5202
  }
5005
5203
  function createMockEntityState(number, modelIndex = 0, origin = { x: 0, y: 0, z: 0 }, overrides) {
5006
- const state = (0, import_engine9.createEmptyEntityState)();
5204
+ const state = (0, import_engine10.createEmptyEntityState)();
5007
5205
  state.number = number;
5008
5206
  state.modelindex = modelIndex;
5009
5207
  state.origin.x = origin.x ?? 0;
@@ -5034,12 +5232,12 @@ function createMockFogData(overrides = {}) {
5034
5232
  }
5035
5233
 
5036
5234
  // src/client/mocks/download.ts
5037
- var import_vitest20 = require("vitest");
5235
+ var import_vitest21 = require("vitest");
5038
5236
  function createMockDownloadManager(overrides) {
5039
5237
  return {
5040
- download: import_vitest20.vi.fn().mockResolvedValue(new ArrayBuffer(0)),
5041
- cancel: import_vitest20.vi.fn(),
5042
- getProgress: import_vitest20.vi.fn().mockReturnValue(0),
5238
+ download: import_vitest21.vi.fn().mockResolvedValue(new ArrayBuffer(0)),
5239
+ cancel: import_vitest21.vi.fn(),
5240
+ getProgress: import_vitest21.vi.fn().mockReturnValue(0),
5043
5241
  ...overrides
5044
5242
  };
5045
5243
  }
@@ -5148,20 +5346,20 @@ var createMockConnectionState = (state = "connected") => ({
5148
5346
  });
5149
5347
 
5150
5348
  // src/client/mocks/console.ts
5151
- var import_vitest21 = require("vitest");
5349
+ var import_vitest22 = require("vitest");
5152
5350
  function createMockConsole(overrides) {
5153
5351
  const history = [];
5154
5352
  const errors = [];
5155
5353
  const commands = {};
5156
5354
  const cvars = {};
5157
5355
  return {
5158
- print: import_vitest21.vi.fn((text) => {
5356
+ print: import_vitest22.vi.fn((text) => {
5159
5357
  history.push(text);
5160
5358
  }),
5161
- error: import_vitest21.vi.fn((text) => {
5359
+ error: import_vitest22.vi.fn((text) => {
5162
5360
  errors.push(text);
5163
5361
  }),
5164
- execute: import_vitest21.vi.fn((text) => {
5362
+ execute: import_vitest22.vi.fn((text) => {
5165
5363
  const parts = text.trim().split(/\s+/);
5166
5364
  const cmd = parts[0];
5167
5365
  const args = parts.slice(1);
@@ -5171,11 +5369,11 @@ function createMockConsole(overrides) {
5171
5369
  history.push(`Unknown command "${cmd}"`);
5172
5370
  }
5173
5371
  }),
5174
- addCommand: import_vitest21.vi.fn((name, handler) => {
5372
+ addCommand: import_vitest22.vi.fn((name, handler) => {
5175
5373
  commands[name] = handler;
5176
5374
  }),
5177
- getCvar: import_vitest21.vi.fn((name) => cvars[name]),
5178
- setCvar: import_vitest21.vi.fn((name, value) => {
5375
+ getCvar: import_vitest22.vi.fn((name) => cvars[name]),
5376
+ setCvar: import_vitest22.vi.fn((name, value) => {
5179
5377
  cvars[name] = value;
5180
5378
  }),
5181
5379
  getHistory: () => history,
@@ -5281,139 +5479,6 @@ var verifySmoothing = (states) => {
5281
5479
  };
5282
5480
  };
5283
5481
 
5284
- // src/visual/animation-snapshots.ts
5285
- var import_upng_js = __toESM(require("upng-js"), 1);
5286
- var import_promises2 = __toESM(require("fs/promises"), 1);
5287
- var import_fs2 = require("fs");
5288
- var import_path3 = __toESM(require("path"), 1);
5289
- async function loadAPNG(filepath) {
5290
- const buffer = await import_promises2.default.readFile(filepath);
5291
- const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
5292
- const img = import_upng_js.default.decode(arrayBuffer);
5293
- const framesRGBA = import_upng_js.default.toRGBA8(img);
5294
- const frames = framesRGBA.map((buffer2) => new Uint8ClampedArray(buffer2));
5295
- return {
5296
- width: img.width,
5297
- height: img.height,
5298
- frames
5299
- };
5300
- }
5301
- async function saveAPNG(filepath, frames, width, height, delayMs) {
5302
- const buffers = frames.map((f) => {
5303
- const dst = new ArrayBuffer(f.byteLength);
5304
- new Uint8ClampedArray(dst).set(f);
5305
- return dst;
5306
- });
5307
- const delays = new Array(frames.length).fill(delayMs);
5308
- const pngBuffer = import_upng_js.default.encode(buffers, width, height, 0, delays);
5309
- await import_promises2.default.mkdir(import_path3.default.dirname(filepath), { recursive: true });
5310
- await import_promises2.default.writeFile(filepath, Buffer.from(pngBuffer));
5311
- }
5312
- async function expectAnimationSnapshot(renderAndCaptureFrame, options) {
5313
- const {
5314
- name,
5315
- width,
5316
- height,
5317
- frameCount,
5318
- fps = 10,
5319
- updateBaseline = false,
5320
- snapshotDir = import_path3.default.join(process.cwd(), "tests", "__snapshots__"),
5321
- threshold = 0.1,
5322
- maxDifferencePercent = 0.1
5323
- } = options;
5324
- if (!width || !height) {
5325
- throw new Error("Width and height are required for expectAnimationSnapshot");
5326
- }
5327
- const baselinePath = getSnapshotPath(name, "baseline", snapshotDir);
5328
- const actualPath = getSnapshotPath(name, "actual", snapshotDir);
5329
- const diffPath = getSnapshotPath(name, "diff", snapshotDir);
5330
- const alwaysSave = process.env.ALWAYS_SAVE_SNAPSHOTS === "1";
5331
- const actualFrames = [];
5332
- for (let i = 0; i < frameCount; i++) {
5333
- const frameData = await renderAndCaptureFrame(i);
5334
- if (frameData.length !== width * height * 4) {
5335
- throw new Error(`Frame ${i} dimension mismatch: expected length ${width * height * 4}, got ${frameData.length}`);
5336
- }
5337
- actualFrames.push(frameData);
5338
- }
5339
- const delayMs = 1e3 / fps;
5340
- let baselineFrames = null;
5341
- let shouldUpdateBaseline = updateBaseline || !(0, import_fs2.existsSync)(baselinePath);
5342
- if (!shouldUpdateBaseline) {
5343
- try {
5344
- const baseline = await loadAPNG(baselinePath);
5345
- if (baseline.width !== width || baseline.height !== height) {
5346
- console.warn(`Baseline dimensions mismatch (${baseline.width}x${baseline.height} vs ${width}x${height}). Forcing update.`);
5347
- shouldUpdateBaseline = true;
5348
- } else if (baseline.frames.length !== frameCount) {
5349
- console.warn(`Baseline frame count mismatch (${baseline.frames.length} vs ${frameCount}). Forcing update.`);
5350
- shouldUpdateBaseline = true;
5351
- } else {
5352
- baselineFrames = baseline.frames;
5353
- }
5354
- } catch (e) {
5355
- console.warn(`Failed to load baseline APNG: ${e}. Forcing update.`);
5356
- shouldUpdateBaseline = true;
5357
- }
5358
- }
5359
- if (shouldUpdateBaseline) {
5360
- console.log(`Creating/Updating baseline for ${name} at ${baselinePath}`);
5361
- await saveAPNG(baselinePath, actualFrames, width, height, delayMs);
5362
- return;
5363
- }
5364
- if (!baselineFrames) {
5365
- throw new Error("Baseline frames missing despite checks.");
5366
- }
5367
- const frameStats = [];
5368
- const diffFrames = [];
5369
- let totalDiffPixels = 0;
5370
- let totalPixels = 0;
5371
- for (let i = 0; i < frameCount; i++) {
5372
- const result2 = await compareSnapshots(actualFrames[i], baselineFrames[i], width, height, options);
5373
- frameStats.push(result2);
5374
- if (result2.diffImage) {
5375
- diffFrames.push(result2.diffImage);
5376
- } else {
5377
- diffFrames.push(new Uint8ClampedArray(width * height * 4));
5378
- }
5379
- totalDiffPixels += result2.pixelsDifferent;
5380
- totalPixels += width * height;
5381
- }
5382
- const avgPercentDifferent = totalDiffPixels / totalPixels * 100;
5383
- const passed = avgPercentDifferent <= (maxDifferencePercent || 0);
5384
- const result = {
5385
- passed,
5386
- totalPixels,
5387
- totalDiffPixels,
5388
- percentDifferent: avgPercentDifferent,
5389
- frameStats
5390
- };
5391
- const statsPath = import_path3.default.join(snapshotDir, "stats", `${name}.json`);
5392
- await import_promises2.default.mkdir(import_path3.default.dirname(statsPath), { recursive: true });
5393
- await import_promises2.default.writeFile(statsPath, JSON.stringify({
5394
- passed: result.passed,
5395
- percentDifferent: result.percentDifferent,
5396
- pixelsDifferent: result.totalDiffPixels,
5397
- totalPixels: result.totalPixels,
5398
- threshold: options.threshold ?? 0.1,
5399
- maxDifferencePercent: options.maxDifferencePercent ?? 0.1,
5400
- frameCount
5401
- }, null, 2));
5402
- if (!passed || alwaysSave) {
5403
- await saveAPNG(actualPath, actualFrames, width, height, delayMs);
5404
- await saveAPNG(diffPath, diffFrames, width, height, delayMs);
5405
- }
5406
- if (!passed) {
5407
- const failThreshold = 10;
5408
- const errorMessage = `Animation snapshot comparison failed for ${name}: ${result.percentDifferent.toFixed(2)}% different (${result.totalDiffPixels} pixels total). See ${diffPath} for details.`;
5409
- if (result.percentDifferent <= failThreshold) {
5410
- console.warn(`[WARNING] ${errorMessage} (Marked as failed in report but passing test execution due to <${failThreshold}% difference)`);
5411
- } else {
5412
- throw new Error(errorMessage);
5413
- }
5414
- }
5415
- }
5416
-
5417
5482
  // src/e2e/playwright.ts
5418
5483
  async function createPlaywrightTestClient(options = {}) {
5419
5484
  let playwright;
@@ -5619,6 +5684,7 @@ function createVisualTestScenario(sceneName) {
5619
5684
  createInputInjector,
5620
5685
  createInterpolationTestData,
5621
5686
  createItemEntityFactory,
5687
+ createLoggingRenderer,
5622
5688
  createMessageReaderMock,
5623
5689
  createMessageWriterMock,
5624
5690
  createMockAI,
@@ -5737,6 +5803,7 @@ function createVisualTestScenario(sceneName) {
5737
5803
  createMonsterEntityFactory,
5738
5804
  createMultiplayerTestScenario,
5739
5805
  createNetChanMock,
5806
+ createNullRenderer,
5740
5807
  createPacketMock,
5741
5808
  createPhysicsTestContext,
5742
5809
  createPhysicsTestScenario,
@@ -5764,6 +5831,8 @@ function createVisualTestScenario(sceneName) {
5764
5831
  createWebGLPlaywrightSetup,
5765
5832
  createWebGLRenderTestSetup,
5766
5833
  expectAnimationSnapshot,
5834
+ expectNoDoubleTransform,
5835
+ expectRendererCalls,
5767
5836
  expectSnapshot,
5768
5837
  findPakFile,
5769
5838
  flipPixelsVertically,
@@ -5823,6 +5892,7 @@ function createVisualTestScenario(sceneName) {
5823
5892
  teardownNodeEnvironment,
5824
5893
  testComputeShader,
5825
5894
  testPipelineRendering,
5895
+ testWebGLAnimation,
5826
5896
  testWebGLRenderer,
5827
5897
  throttleBandwidth,
5828
5898
  verifySmoothing,