memlnaut-nisps/playground/js/shapeseq/tests/smoke.test.js

254 lines
8.5 KiB
JavaScript
Raw Normal View History

/**
* Smoke tests for shapeseq modules.
*
* Validates that every module can be imported and that non-browser modules
* construct correctly. Browser-dependent modules are imported inside
* try/catch blocks so the suite stays green in a pure Node environment.
*/
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
// ── Pure modules (no browser APIs required) ─────────────────────────
import {
createStep, createPattern, clonePattern,
mergePatterns, setStep, validatePattern,
} from '../pattern.js';
import { Primitive, CATEGORIES, applyBoundary } from '../primitive.js';
import {
EuclideanRhythm, ProbabilityGate, PitchWalker,
Ratchet, SwingGroove, DensityMorph, IntervalLock,
VelocityShaper, PRIMITIVE_REGISTRY,
} from '../primitives.js';
import {
createPRNG, next, nextInt, fork, getState, setState, randomSeed,
} from '../prng.js';
import { Chain } from '../chain.js';
import {
SEQ, ML, UI, EventBus,
} from '../event-bus.js';
import { createParamMap, map, mapWithSchema } from '../param-map.js';
import {
createProjection, applyProjection, VELOCITY_CURVES,
} from '../projection.js';
// ── Tests: pattern ──────────────────────────────────────────────────
describe('pattern', () => {
it('createStep returns an object', () => {
const step = createStep();
assert.equal(typeof step, 'object');
});
it('createPattern builds a pattern with the requested step count', () => {
const pat = createPattern(8);
assert.ok(pat);
assert.ok(validatePattern(pat));
});
it('clonePattern produces an independent copy', () => {
const a = createPattern(4);
const b = clonePattern(a);
assert.deepStrictEqual(a, b);
assert.notEqual(a, b);
});
it('mergePatterns does not throw', () => {
const a = createPattern(4);
const b = createPattern(4);
assert.ok(mergePatterns(a, b, 'additive'));
});
});
// ── Tests: prng ─────────────────────────────────────────────────────
describe('prng', () => {
it('createPRNG returns a deterministic generator', () => {
const a = createPRNG(42);
const b = createPRNG(42);
assert.deepStrictEqual(next(a), next(b));
});
it('nextInt returns values within range', () => {
const rng = createPRNG(1);
for (let i = 0; i < 50; i++) {
const result = nextInt(rng, 0, 10);
// nextInt may return a number or { value, nextState }
const v = typeof result === 'number' ? result : result.value;
assert.ok(v >= 0 && v <= 10, `${v} out of range`);
}
});
it('fork creates a child PRNG', () => {
const rng = createPRNG(7);
const child = fork(rng, 'sub');
assert.ok(child);
const result = next(child);
assert.equal(typeof result.value, 'number');
});
it('getState / setState round-trips', () => {
const rng = createPRNG(99);
next(rng); next(rng);
const snap = getState(rng);
const v1 = next(rng);
setState(rng, snap);
const v2 = next(rng);
assert.deepStrictEqual(v1, v2);
});
it('randomSeed returns a number', () => {
assert.equal(typeof randomSeed(), 'number');
});
});
// ── Tests: primitive ────────────────────────────────────────────────
describe('primitive', () => {
it('CATEGORIES is a frozen array', () => {
assert.ok(Array.isArray(CATEGORIES));
assert.ok(Object.isFrozen(CATEGORIES));
});
it('Primitive can be constructed with a schema', () => {
// Grab the first registered primitive's schema as a reference
const key = Object.keys(PRIMITIVE_REGISTRY)[0];
const Cls = PRIMITIVE_REGISTRY[key];
const inst = new Cls();
assert.ok(inst instanceof Primitive);
});
});
// ── Tests: primitives ───────────────────────────────────────────────
describe('primitives', () => {
it('PRIMITIVE_REGISTRY contains all expected primitives', () => {
const expected = [
'EuclideanRhythm', 'ProbabilityGate', 'PitchWalker', 'Ratchet',
'SwingGroove', 'DensityMorph', 'IntervalLock', 'VelocityShaper',
];
for (const name of expected) {
assert.ok(name in PRIMITIVE_REGISTRY, `missing: ${name}`);
}
});
it('each registered primitive instantiates without error', () => {
for (const [name, Cls] of Object.entries(PRIMITIVE_REGISTRY)) {
const inst = new Cls();
assert.ok(inst instanceof Primitive, `${name} is not a Primitive`);
}
});
});
// ── Tests: chain ────────────────────────────────────────────────────
describe('chain', () => {
it('Chain constructs with defaults', () => {
const c = new Chain();
assert.ok(c);
});
});
// ── Tests: event-bus ────────────────────────────────────────────────
describe('event-bus', () => {
it('SEQ, ML, UI are frozen namespace objects', () => {
assert.ok(Object.isFrozen(SEQ));
assert.ok(Object.isFrozen(ML));
assert.ok(Object.isFrozen(UI));
});
it('EventBus can be constructed without AudioContext', () => {
const bus = new EventBus();
assert.ok(bus);
});
it('on/emit round-trip works', () => {
const bus = new EventBus();
let received = null;
bus.on(SEQ.STEP, (data) => { received = data; });
bus.emit(SEQ.STEP, { idx: 3 });
assert.equal(received.idx, 3);
});
});
// ── Tests: param-map ────────────────────────────────────────────────
describe('param-map', () => {
it('createParamMap returns an object', () => {
const pm = createParamMap(8);
assert.ok(pm);
});
it('map transforms an array of outputs', () => {
const outputs = new Array(8).fill(0.5);
const result = map(outputs, 4);
assert.equal(result.length, 4);
});
});
// ── Tests: projection ───────────────────────────────────────────────
describe('projection', () => {
it('createProjection returns a valid config object', () => {
const config = createProjection();
assert.ok(config);
assert.equal(typeof config.velocityCurve, 'string');
assert.equal(typeof config.gateThreshold, 'number');
assert.ok(config.pitchRange);
});
it('VELOCITY_CURVES has 3 entries', () => {
assert.equal(VELOCITY_CURVES.length, 3);
});
});
// ── Browser-dependent modules ───────────────────────────────────────
// These modules reference AudioContext, DOM APIs, or parent-directory
// imports that may not resolve in Node. We verify the import itself
// doesn't throw at module-evaluation time (syntax / top-level errors).
describe('browser-dependent modules (import-only)', () => {
it('clock.js imports without top-level error', async () => {
// ClockEngine requires AudioContext at *construction*, but the
// module should load fine.
const mod = await import('../clock.js');
assert.ok(mod.ClockEngine, 'ClockEngine export exists');
});
it('step-viz.js imports without top-level error', async () => {
const mod = await import('../step-viz.js');
assert.ok(mod.StepVisualizer, 'StepVisualizer export exists');
});
it('sequencer.js imports without top-level error', async () => {
const mod = await import('../sequencer.js');
assert.ok(mod.ShapeSeqEngine, 'ShapeSeqEngine export exists');
});
it('chain-ui.js imports without top-level error', async () => {
const mod = await import('../chain-ui.js');
assert.ok(mod.ChainBuilderUI, 'ChainBuilderUI export exists');
});
it('seq-iml.js imports without top-level error', async () => {
// seq-iml imports from ../nisps/nisps-wasm.js which may not
// resolve in Node. We catch and skip gracefully.
try {
const mod = await import('../seq-iml.js');
assert.ok(mod.createSequenceIML, 'createSequenceIML export exists');
} catch (err) {
// Expected — parent-directory import of nisps-wasm may fail
assert.ok(true, `skipped: ${err.message}`);
}
});
});