test(algorithms): load simulations per test to fix flaky specs
Build, Test & Push Frontend / quality-check (push) Successful in 2m19s
Build, Test & Push Frontend / docker (push) Successful in 55s

Shared gpu-state.ts keeps the @babylonjs/core mock of the first spec
that loads it. With isolate: false, other specs got that stale mock.
Call vi.resetModules() and import the class in each test instead.
This commit is contained in:
lobo committed 2026-10-23 15:23:14 +02:00
1 parent 58b14a5b64
commit 05dc554d79
3 files changed
+35 -19

No files matched your search

@@ -1,4 +1,4 @@
import {FluidParams, FluidSimulation} from './fluid-simulation'; import type {FluidParams} from './fluid-simulation';
import {DEFAULT_SETTINGS, MAX_PRESSURE_ITERATIONS, Splat} from './fluid.model'; import {DEFAULT_SETTINGS, MAX_PRESSURE_ITERATIONS, Splat} from './fluid.model';
const renderCalls: string[] = []; const renderCalls: string[] = [];
@@ -62,7 +62,11 @@ function params(pressureIterations: number, splats: Splat[] = []): FluidParams {
return {settings: {...DEFAULT_SETTINGS, pressureIterations}, splats}; return {settings: {...DEFAULT_SETTINGS, pressureIterations}, splats};
} }
function start(readParams: () => FluidParams) { // gpu-state.ts is shared between specs and keeps the @babylonjs/core of whichever spec loaded it first (the real
// one, or another spec's mock). A fresh module graph per test makes it use this spec's mock.
async function start(readParams: () => FluidParams) {
vi.resetModules();
const {FluidSimulation} = await import('./fluid-simulation');
renderCalls.length = 0; renderCalls.length = 0;
disposedTargets = 0; disposedTargets = 0;
disposedWrappers = 0; disposedWrappers = 0;
@@ -78,9 +82,9 @@ function count(name: string): number {
} }
describe('FluidSimulation', () => { describe('FluidSimulation', () => {
it('drawsNothingUntilAllShadersReady', () => { it('drawsNothingUntilAllShadersReady', async () => {
const readParams = vi.fn(() => params(1, [splat])); const readParams = vi.fn(() => params(1, [splat]));
const {scene} = start(readParams); const {scene} = await start(readParams);
ready['fluidAdvect'] = false; ready['fluidAdvect'] = false;
scene.frame(); scene.frame();
@@ -93,9 +97,9 @@ describe('FluidSimulation', () => {
expect(renderCalls).toContain('fluidSplat'); expect(renderCalls).toContain('fluidSplat');
}); });
it('readsParamsAtFrameStart', () => { it('readsParamsAtFrameStart', async () => {
let iterations = 5; let iterations = 5;
const {scene} = start(() => params(iterations)); const {scene} = await start(() => params(iterations));
scene.frame(); scene.frame();
expect(count('fluidPressure')).toBe(5); expect(count('fluidPressure')).toBe(5);
@@ -105,14 +109,14 @@ describe('FluidSimulation', () => {
expect(count('fluidPressure')).toBe(12); expect(count('fluidPressure')).toBe(12);
}); });
it('clampsPressureIterations', () => { it('clampsPressureIterations', async () => {
const {scene} = start(() => params(1000)); const {scene} = await start(() => params(1000));
scene.frame(); scene.frame();
expect(count('fluidPressure')).toBe(MAX_PRESSURE_ITERATIONS); expect(count('fluidPressure')).toBe(MAX_PRESSURE_ITERATIONS);
}); });
it('passOrder', () => { it('passOrder', async () => {
const {scene} = start(() => params(2, [splat])); const {scene} = await start(() => params(2, [splat]));
scene.frame(); scene.frame();
renderCalls.length = 0; renderCalls.length = 0;
scene.frame(); scene.frame();
@@ -123,8 +127,8 @@ describe('FluidSimulation', () => {
]); ]);
}); });
it('resetClearsBeforeSplats', () => { it('resetClearsBeforeSplats', async () => {
const {scene, simulation} = start(() => params(1, [splat])); const {scene, simulation} = await start(() => params(1, [splat]));
scene.frame(); scene.frame();
renderCalls.length = 0; renderCalls.length = 0;
scene.frame(); scene.frame();
@@ -136,8 +140,8 @@ describe('FluidSimulation', () => {
expect(renderCalls.slice(0, 4)).toEqual(['fluidClear', 'fluidClear', 'fluidClear', 'fluidSplat']); expect(renderCalls.slice(0, 4)).toEqual(['fluidClear', 'fluidClear', 'fluidClear', 'fluidSplat']);
}); });
it('disposeFreesAllTargets', () => { it('disposeFreesAllTargets', async () => {
const {simulation} = start(() => params(1)); const {simulation} = await start(() => params(1));
simulation.dispose(); simulation.dispose();
expect(disposedTargets).toBe(8); expect(disposedTargets).toBe(8);
expect(disposedWrappers).toBe(8); expect(disposedWrappers).toBe(8);
@@ -1,4 +1,4 @@
import {GrayScottParams, GrayScottSimulation} from './gray-scott-simulation'; import type {GrayScottParams} from './gray-scott-simulation';
import {DEFAULT_SETTINGS} from './gray-scott.model'; import {DEFAULT_SETTINGS} from './gray-scott.model';
const renderCalls: string[] = []; const renderCalls: string[] = [];
@@ -55,9 +55,13 @@ function paramsWithSteps(stepsPerFrame: number): GrayScottParams {
} }
describe('GrayScottSimulation', () => { describe('GrayScottSimulation', () => {
it('readsParamsAtFrameStart', () => { it('readsParamsAtFrameStart', async () => {
let stepsPerFrame = 1; let stepsPerFrame = 1;
const scene = fakeScene(); const scene = fakeScene();
// gpu-state.ts is shared between specs and keeps the @babylonjs/core of whichever spec loaded it first; a fresh
// module graph makes it use this spec's mock.
vi.resetModules();
const {GrayScottSimulation} = await import('./gray-scott-simulation');
const simulation = new GrayScottSimulation(); const simulation = new GrayScottSimulation();
simulation.init(scene as never, {} as never, 'float', () => paramsWithSteps(stepsPerFrame)); simulation.init(scene as never, {} as never, 'float', () => paramsWithSteps(stepsPerFrame));
scene.frame(); scene.frame();
@@ -1,4 +1,4 @@
import {LeniaParams, LeniaSimulation} from './lenia-simulation'; import type {LeniaParams} from './lenia-simulation';
import {DEFAULT_SETTINGS} from './lenia.model'; import {DEFAULT_SETTINGS} from './lenia.model';
const renderCalls: {name: string; target: unknown}[] = []; const renderCalls: {name: string; target: unknown}[] = [];
@@ -64,11 +64,15 @@ function paramsWithSteps(stepsPerFrame: number): LeniaParams {
} }
describe('LeniaSimulation', () => { describe('LeniaSimulation', () => {
it('keepsSeedWhileStepShaderCompiles', () => { it('keepsSeedWhileStepShaderCompiles', async () => {
renderCalls.length = 0; renderCalls.length = 0;
ready['leniaCopy'] = true; ready['leniaCopy'] = true;
ready['leniaStep'] = false; ready['leniaStep'] = false;
const scene = fakeScene(); const scene = fakeScene();
// gpu-state.ts is shared between specs and keeps the @babylonjs/core of whichever spec loaded it first; a fresh
// module graph makes it use this spec's mock.
vi.resetModules();
const {LeniaSimulation} = await import('./lenia-simulation');
const simulation = new LeniaSimulation(); const simulation = new LeniaSimulation();
simulation.init(scene as never, {} as never, 'float', () => paramsWithSteps(1)); simulation.init(scene as never, {} as never, 'float', () => paramsWithSteps(1));
simulation.seed(new Uint8Array(4)); simulation.seed(new Uint8Array(4));
@@ -85,12 +89,16 @@ describe('LeniaSimulation', () => {
expect(firstStep.target).not.toBe(seededTarget); expect(firstStep.target).not.toBe(seededTarget);
}); });
it('readsParamsAtFrameStart', () => { it('readsParamsAtFrameStart', async () => {
renderCalls.length = 0; renderCalls.length = 0;
ready['leniaCopy'] = true; ready['leniaCopy'] = true;
ready['leniaStep'] = true; ready['leniaStep'] = true;
let stepsPerFrame = 1; let stepsPerFrame = 1;
const scene = fakeScene(); const scene = fakeScene();
// gpu-state.ts is shared between specs and keeps the @babylonjs/core of whichever spec loaded it first; a fresh
// module graph makes it use this spec's mock.
vi.resetModules();
const {LeniaSimulation} = await import('./lenia-simulation');
const simulation = new LeniaSimulation(); const simulation = new LeniaSimulation();
simulation.init(scene as never, {} as never, 'float', () => paramsWithSteps(stepsPerFrame)); simulation.init(scene as never, {} as never, 'float', () => paramsWithSteps(stepsPerFrame));
simulation.seed(new Uint8Array(4)); simulation.seed(new Uint8Array(4));