/** * 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}`); } }); });