@quake2ts/test-utils 0.0.807 → 0.0.809

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
@@ -296,6 +296,7 @@ __export(index_exports, {
296
296
  teardownNodeEnvironment: () => teardownNodeEnvironment,
297
297
  testComputeShader: () => testComputeShader,
298
298
  testPipelineRendering: () => testPipelineRendering,
299
+ testWebGLAnimation: () => testWebGLAnimation,
299
300
  testWebGLRenderer: () => testWebGLRenderer,
300
301
  throttleBandwidth: () => throttleBandwidth,
301
302
  verifySmoothing: () => verifySmoothing,
@@ -4547,9 +4548,153 @@ async function renderAndExpectSnapshot(setup, renderFn, options) {
4547
4548
  });
4548
4549
  }
4549
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
+
4550
4684
  // src/engine/helpers/webgl-playwright.ts
4551
4685
  var import_http = require("http");
4552
- 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
+ }
4553
4698
  async function createWebGLPlaywrightSetup(options = {}) {
4554
4699
  const width = options.width ?? 256;
4555
4700
  const height = options.height ?? 256;
@@ -4568,7 +4713,7 @@ async function createWebGLPlaywrightSetup(options = {}) {
4568
4713
  } catch (e) {
4569
4714
  throw new Error('Failed to load "serve-handler" package. Please ensure it is installed.');
4570
4715
  }
4571
- const repoRoot = import_path2.default.resolve(__dirname, "../../../../..");
4716
+ const repoRoot = findWorkspaceRoot(__dirname);
4572
4717
  const staticServer = (0, import_http.createServer)((request, response) => {
4573
4718
  return handler(request, response, {
4574
4719
  public: repoRoot,
@@ -4632,9 +4777,9 @@ async function createWebGLPlaywrightSetup(options = {}) {
4632
4777
  cleanup
4633
4778
  };
4634
4779
  }
4635
- async function renderAndCaptureWebGLPlaywright(page, renderFn, width, height) {
4780
+ async function renderAndCaptureWebGLPlaywright(page, renderFn, width, height, frameIndex = 0) {
4636
4781
  try {
4637
- const pixelData = await page.evaluate(({ code, width: width2, height: height2 }) => {
4782
+ const pixelData = await page.evaluate(({ code, width: width2, height: height2, frameIndex: frameIndex2 }) => {
4638
4783
  const renderer = window.testRenderer;
4639
4784
  const gl = window.testGl;
4640
4785
  const canvas = window.testCanvas;
@@ -4647,8 +4792,8 @@ async function renderAndCaptureWebGLPlaywright(page, renderFn, width, height) {
4647
4792
  gl.viewport(0, 0, width2, height2);
4648
4793
  }
4649
4794
  try {
4650
- const fn = new Function("renderer", "gl", code);
4651
- fn(renderer, gl);
4795
+ const fn = new Function("renderer", "gl", "frameIndex", code);
4796
+ fn(renderer, gl, frameIndex2);
4652
4797
  } catch (err) {
4653
4798
  throw new Error(`Renderer Execution Error: ${err.message}
4654
4799
  Code:
@@ -4656,7 +4801,7 @@ ${code}`);
4656
4801
  }
4657
4802
  gl.finish();
4658
4803
  return window.captureCanvas();
4659
- }, { code: renderFn, width, height });
4804
+ }, { code: renderFn, width, height, frameIndex });
4660
4805
  return new Uint8ClampedArray(pixelData);
4661
4806
  } catch (err) {
4662
4807
  throw new Error(`Browser Test Error: ${err.message}`);
@@ -4683,6 +4828,33 @@ async function testWebGLRenderer(renderCode, options) {
4683
4828
  await setup.cleanup();
4684
4829
  }
4685
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
+ }
4686
4858
 
4687
4859
  // src/engine/helpers/pipeline-test-template.ts
4688
4860
  var import_vitest18 = require("vitest");
@@ -5307,139 +5479,6 @@ var verifySmoothing = (states) => {
5307
5479
  };
5308
5480
  };
5309
5481
 
5310
- // src/visual/animation-snapshots.ts
5311
- var import_upng_js = __toESM(require("upng-js"), 1);
5312
- var import_promises2 = __toESM(require("fs/promises"), 1);
5313
- var import_fs2 = require("fs");
5314
- var import_path3 = __toESM(require("path"), 1);
5315
- async function loadAPNG(filepath) {
5316
- const buffer = await import_promises2.default.readFile(filepath);
5317
- const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
5318
- const img = import_upng_js.default.decode(arrayBuffer);
5319
- const framesRGBA = import_upng_js.default.toRGBA8(img);
5320
- const frames = framesRGBA.map((buffer2) => new Uint8ClampedArray(buffer2));
5321
- return {
5322
- width: img.width,
5323
- height: img.height,
5324
- frames
5325
- };
5326
- }
5327
- async function saveAPNG(filepath, frames, width, height, delayMs) {
5328
- const buffers = frames.map((f) => {
5329
- const dst = new ArrayBuffer(f.byteLength);
5330
- new Uint8ClampedArray(dst).set(f);
5331
- return dst;
5332
- });
5333
- const delays = new Array(frames.length).fill(delayMs);
5334
- const pngBuffer = import_upng_js.default.encode(buffers, width, height, 0, delays);
5335
- await import_promises2.default.mkdir(import_path3.default.dirname(filepath), { recursive: true });
5336
- await import_promises2.default.writeFile(filepath, Buffer.from(pngBuffer));
5337
- }
5338
- async function expectAnimationSnapshot(renderAndCaptureFrame, options) {
5339
- const {
5340
- name,
5341
- width,
5342
- height,
5343
- frameCount,
5344
- fps = 10,
5345
- updateBaseline = false,
5346
- snapshotDir = import_path3.default.join(process.cwd(), "tests", "__snapshots__"),
5347
- threshold = 0.1,
5348
- maxDifferencePercent = 0.1
5349
- } = options;
5350
- if (!width || !height) {
5351
- throw new Error("Width and height are required for expectAnimationSnapshot");
5352
- }
5353
- const baselinePath = getSnapshotPath(name, "baseline", snapshotDir);
5354
- const actualPath = getSnapshotPath(name, "actual", snapshotDir);
5355
- const diffPath = getSnapshotPath(name, "diff", snapshotDir);
5356
- const alwaysSave = process.env.ALWAYS_SAVE_SNAPSHOTS === "1";
5357
- const actualFrames = [];
5358
- for (let i = 0; i < frameCount; i++) {
5359
- const frameData = await renderAndCaptureFrame(i);
5360
- if (frameData.length !== width * height * 4) {
5361
- throw new Error(`Frame ${i} dimension mismatch: expected length ${width * height * 4}, got ${frameData.length}`);
5362
- }
5363
- actualFrames.push(frameData);
5364
- }
5365
- const delayMs = 1e3 / fps;
5366
- let baselineFrames = null;
5367
- let shouldUpdateBaseline = updateBaseline || !(0, import_fs2.existsSync)(baselinePath);
5368
- if (!shouldUpdateBaseline) {
5369
- try {
5370
- const baseline = await loadAPNG(baselinePath);
5371
- if (baseline.width !== width || baseline.height !== height) {
5372
- console.warn(`Baseline dimensions mismatch (${baseline.width}x${baseline.height} vs ${width}x${height}). Forcing update.`);
5373
- shouldUpdateBaseline = true;
5374
- } else if (baseline.frames.length !== frameCount) {
5375
- console.warn(`Baseline frame count mismatch (${baseline.frames.length} vs ${frameCount}). Forcing update.`);
5376
- shouldUpdateBaseline = true;
5377
- } else {
5378
- baselineFrames = baseline.frames;
5379
- }
5380
- } catch (e) {
5381
- console.warn(`Failed to load baseline APNG: ${e}. Forcing update.`);
5382
- shouldUpdateBaseline = true;
5383
- }
5384
- }
5385
- if (shouldUpdateBaseline) {
5386
- console.log(`Creating/Updating baseline for ${name} at ${baselinePath}`);
5387
- await saveAPNG(baselinePath, actualFrames, width, height, delayMs);
5388
- return;
5389
- }
5390
- if (!baselineFrames) {
5391
- throw new Error("Baseline frames missing despite checks.");
5392
- }
5393
- const frameStats = [];
5394
- const diffFrames = [];
5395
- let totalDiffPixels = 0;
5396
- let totalPixels = 0;
5397
- for (let i = 0; i < frameCount; i++) {
5398
- const result2 = await compareSnapshots(actualFrames[i], baselineFrames[i], width, height, options);
5399
- frameStats.push(result2);
5400
- if (result2.diffImage) {
5401
- diffFrames.push(result2.diffImage);
5402
- } else {
5403
- diffFrames.push(new Uint8ClampedArray(width * height * 4));
5404
- }
5405
- totalDiffPixels += result2.pixelsDifferent;
5406
- totalPixels += width * height;
5407
- }
5408
- const avgPercentDifferent = totalDiffPixels / totalPixels * 100;
5409
- const passed = avgPercentDifferent <= (maxDifferencePercent || 0);
5410
- const result = {
5411
- passed,
5412
- totalPixels,
5413
- totalDiffPixels,
5414
- percentDifferent: avgPercentDifferent,
5415
- frameStats
5416
- };
5417
- const statsPath = import_path3.default.join(snapshotDir, "stats", `${name}.json`);
5418
- await import_promises2.default.mkdir(import_path3.default.dirname(statsPath), { recursive: true });
5419
- await import_promises2.default.writeFile(statsPath, JSON.stringify({
5420
- passed: result.passed,
5421
- percentDifferent: result.percentDifferent,
5422
- pixelsDifferent: result.totalDiffPixels,
5423
- totalPixels: result.totalPixels,
5424
- threshold: options.threshold ?? 0.1,
5425
- maxDifferencePercent: options.maxDifferencePercent ?? 0.1,
5426
- frameCount
5427
- }, null, 2));
5428
- if (!passed || alwaysSave) {
5429
- await saveAPNG(actualPath, actualFrames, width, height, delayMs);
5430
- await saveAPNG(diffPath, diffFrames, width, height, delayMs);
5431
- }
5432
- if (!passed) {
5433
- const failThreshold = 10;
5434
- const errorMessage = `Animation snapshot comparison failed for ${name}: ${result.percentDifferent.toFixed(2)}% different (${result.totalDiffPixels} pixels total). See ${diffPath} for details.`;
5435
- if (result.percentDifferent <= failThreshold) {
5436
- console.warn(`[WARNING] ${errorMessage} (Marked as failed in report but passing test execution due to <${failThreshold}% difference)`);
5437
- } else {
5438
- throw new Error(errorMessage);
5439
- }
5440
- }
5441
- }
5442
-
5443
5482
  // src/e2e/playwright.ts
5444
5483
  async function createPlaywrightTestClient(options = {}) {
5445
5484
  let playwright;
@@ -5853,6 +5892,7 @@ function createVisualTestScenario(sceneName) {
5853
5892
  teardownNodeEnvironment,
5854
5893
  testComputeShader,
5855
5894
  testPipelineRendering,
5895
+ testWebGLAnimation,
5856
5896
  testWebGLRenderer,
5857
5897
  throttleBandwidth,
5858
5898
  verifySmoothing,