feat: interactive traffic with car-following, overtakes and flash-to-pass
This commit is contained in:
78
tests/unit/traffic-pass.test.ts
Normal file
78
tests/unit/traffic-pass.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { createPulseTap, readSwitchboard, setMusicPulseAudible } from '@/audio/music-pulse';
|
||||
import { TRAFFIC_PASS_GAP_SECONDS, TRAFFIC_PASS_PAN, playTrafficPass } from '@/audio/traffic-pass';
|
||||
|
||||
/** A minimal audio graph that records what the swell builds. */
|
||||
function fakeContext() {
|
||||
const pans: number[] = [];
|
||||
const param = () => ({ setValueAtTime: (v: number) => pans.push(v), exponentialRampToValueAtTime: () => undefined });
|
||||
const node = (extra: object = {}) => ({ connect: () => undefined, disconnect: () => undefined, ...extra });
|
||||
const context = {
|
||||
state: 'running',
|
||||
currentTime: 10,
|
||||
sampleRate: 8000,
|
||||
started: 0,
|
||||
createGain: () => node({ context, gain: { setValueAtTime: () => undefined, exponentialRampToValueAtTime: () => undefined } }),
|
||||
createAnalyser: () => {
|
||||
throw new Error('no analyser');
|
||||
},
|
||||
createBuffer: (_channels: number, length: number) => ({ sampleRate: 8000, getChannelData: () => new Float32Array(length) }),
|
||||
createBufferSource: () =>
|
||||
node({
|
||||
buffer: null,
|
||||
onended: null,
|
||||
start: () => {
|
||||
context.started += 1;
|
||||
},
|
||||
stop: () => undefined,
|
||||
}),
|
||||
createBiquadFilter: () =>
|
||||
node({
|
||||
type: '',
|
||||
Q: { setValueAtTime: () => undefined },
|
||||
frequency: { setValueAtTime: () => undefined, exponentialRampToValueAtTime: () => undefined },
|
||||
}),
|
||||
createStereoPanner: () => node({ pan: param() }),
|
||||
};
|
||||
return { context, pans };
|
||||
}
|
||||
|
||||
describe('traffic pass swell', () => {
|
||||
afterEach(() => setMusicPulseAudible(false));
|
||||
|
||||
it('is silent with no audible soundtrack', () => {
|
||||
expect(readSwitchboard()).toBeNull();
|
||||
expect(playTrafficPass(1)).toBe(false);
|
||||
});
|
||||
|
||||
it('plays once per gap, panned to the passing side', () => {
|
||||
const { context, pans } = fakeContext();
|
||||
createPulseTap(context as unknown as BaseAudioContext, { connect: () => undefined } as unknown as AudioNode);
|
||||
setMusicPulseAudible(true);
|
||||
expect(playTrafficPass(-1)).toBe(true);
|
||||
expect(pans).toContain(-TRAFFIC_PASS_PAN);
|
||||
expect(playTrafficPass(1)).toBe(false);
|
||||
context.currentTime += TRAFFIC_PASS_GAP_SECONDS + 0.01;
|
||||
expect(playTrafficPass(1)).toBe(true);
|
||||
expect(pans).toContain(TRAFFIC_PASS_PAN);
|
||||
expect(context.started).toBe(2);
|
||||
});
|
||||
|
||||
it('never plays on a suspended context', () => {
|
||||
const { context } = fakeContext();
|
||||
context.state = 'suspended';
|
||||
createPulseTap(context as unknown as BaseAudioContext, { connect: () => undefined } as unknown as AudioNode);
|
||||
setMusicPulseAudible(true);
|
||||
expect(playTrafficPass(1)).toBe(false);
|
||||
});
|
||||
|
||||
it('stays out of the initial bundle: only the scene chunk imports it, lazily, never on lite', () => {
|
||||
const environment = readFileSync(resolve(process.cwd(), 'src/scene/world/environment.ts'), 'utf8');
|
||||
expect(environment).toContain("import('@/audio/traffic-pass')");
|
||||
expect(environment).not.toMatch(/from '@\/audio\/traffic-pass'/u);
|
||||
const budgets = readFileSync(resolve(process.cwd(), 'scripts/lib/bundle-budgets.mjs'), 'utf8');
|
||||
expect(budgets).toContain('traffic-pass-');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user