jevstrudel.git / packages / core / test / serialized.test.mjs
1// jevstrudel: evaluations run one at a time and the newest wins
2// (repl.mjs, `serialized`); a stop or newer start during beforeStart
3// cancels a start (cyclist.mjs). Before, a visitor's first click released
4// the welcome song's evaluation beside the song they clicked, and whichever
5// set its pattern last played (2026-09-27).
6import { describe, expect, it, vi } from 'vitest';
7import { repl } from '../repl.mjs';
8import { Cyclist } from '../cyclist.mjs';
9import { pure } from '../pattern.mjs';
10
11const deferred = () => {
12  let resolve;
13  const promise = new Promise((r) => (resolve = r));
14  return { promise, resolve };
15};
16
17const settle = () => new Promise((r) => setTimeout(r, 0));
18
19const quietRepl = (options) =>
20  repl({ getTime: () => 0, setInterval: () => 0, clearInterval: () => {}, ...options });
21
22describe('repl.evaluate', () => {
23  it('runs a newer evaluation only after the one before it settles, and skips the superseded one', async () => {
24    globalThis.__first = pure('first');
25    globalThis.__second = pure('second');
26    const gate = deferred();
27    const calls = [];
28    let evals = 0;
29    const r = quietRepl({
30      beforeEval: ({ code }) => {
31        calls.push(`before ${code}`);
32        // the first evaluation waits, as the welcome song waits for a click
33        return evals++ === 0 ? gate.promise : undefined;
34      },
35      afterEval: ({ code }) => calls.push(`after ${code}`),
36    });
37    const first = r.evaluate('globalThis.__first', false);
38    await settle();
39    const second = r.evaluate('globalThis.__second', false);
40    await settle();
41    // the second has not begun while the first waits
42    expect(calls).toEqual(['before globalThis.__first']);
43    gate.resolve();
44    await Promise.all([first, second]);
45    expect(calls).toEqual(['before globalThis.__first', 'before globalThis.__second', 'after globalThis.__second']);
46    expect(r.scheduler.pattern.firstCycleValues).toEqual(['second']);
47  });
48
49  it('skips an evaluation queued behind another when a newer one arrives', async () => {
50    globalThis.__a = pure('a');
51    globalThis.__b = pure('b');
52    globalThis.__c = pure('c');
53    const gate = deferred();
54    const before = [];
55    const r = quietRepl({
56      beforeEval: ({ code }) => {
57        before.push(code);
58        return before.length === 1 ? gate.promise : undefined;
59      },
60    });
61    const runs = [r.evaluate('globalThis.__a', false)];
62    await settle();
63    runs.push(r.evaluate('globalThis.__b', false), r.evaluate('globalThis.__c', false));
64    gate.resolve();
65    await Promise.all(runs);
66    // b never began: c superseded it while a was still settling (and a,
67    // superseded in its beforeEval, set no pattern)
68    expect(before).toEqual(['globalThis.__a', 'globalThis.__c']);
69    expect(r.scheduler.pattern.firstCycleValues).toEqual(['c']);
70  });
71});
72
73describe('Cyclist.start', () => {
74  const cyclist = (beforeStart) => {
75    const c = new Cyclist({ getTime: () => 0, setInterval: () => 0, clearInterval: () => {}, beforeStart });
76    c.pattern = pure(1);
77    c.clock.start = vi.fn();
78    return c;
79  };
80
81  it('does not start once stopped while beforeStart was awaited', async () => {
82    const gate = deferred();
83    const c = cyclist(() => gate.promise);
84    const starting = c.start();
85    c.stop();
86    gate.resolve();
87    await starting;
88    expect(c.started).toBe(false);
89    expect(c.clock.start).not.toHaveBeenCalled();
90  });
91
92  it('starts once, for the newest start', async () => {
93    const gates = [deferred(), deferred()];
94    let n = 0;
95    const c = cyclist(() => gates[n++].promise);
96    const a = c.start();
97    const b = c.start();
98    gates[1].resolve();
99    await b;
100    gates[0].resolve();
101    await a;
102    expect(c.started).toBe(true);
103    expect(c.clock.start).toHaveBeenCalledTimes(1);
104  });
105});