jevstrudel.git / website / src / jev / nextTrack.test.mjs
1import { describe, expect, it, vi } from 'vitest';
2import { nextTracker } from './nextTrack.mjs';
3
4const song = (id) => ({ id: `jev/${id}`, title: id, description: `about ${id}`, code: `// ${id}` });
5const songs = ['a', 'b', 'c', 'd'].map(song);
6
7function tracker({ answer = (list) => list.find((s) => s.id !== 'jev/a') ?? list[0], recent = [] } = {}) {
8  const picks = [];
9  const played = [];
10  const recorded = [];
11  const loadRecent = vi.fn(async () => recent);
12  const t = nextTracker({
13    pick: async (list, current, heard, options) => {
14      picks.push({ current: current?.id ?? null, heard, options });
15      const next = answer(list.filter((s) => s.id !== current?.id));
16      if (!next) throw new Error('unreachable');
17      return { song: next, p: 0.5 };
18    },
19    play: async (_editor, s) => played.push(s.id),
20    loadRecent,
21    record: (id) => recorded.push(id),
22  });
23  return { t, picks, played, recorded, loadRecent };
24}
25
26describe('nextTracker', () => {
27  it('signed out, Jev hears the titles this tab played, and nothing is recorded', async () => {
28    const { t, picks, played, recorded, loadRecent } = tracker();
29    await t.next({ songs, current: songs[0], user: null });
30    await t.next({ songs, current: songs[1], user: null });
31    expect(picks.map((p) => p.heard)).toEqual([['a'], ['a', 'b']]);
32    expect(picks[1].options).toEqual({});
33    expect(played).toEqual(['jev/b', 'jev/c']);
34    expect(recorded).toEqual([]);
35    expect(loadRecent).not.toHaveBeenCalled();
36  });
37
38  it('signed in, Jev hears the account’s history, oldest first, and each pick is recorded', async () => {
39    const { t, picks, recorded, loadRecent } = tracker({ recent: ['jev/c', 'jev/d'] });
40    const user = { id: 'u1' };
41    const { song } = await t.next({ songs, current: songs[0], user });
42    expect(picks[0].heard).toEqual(['d', 'c']);
43    expect(picks[0].options).toEqual({ recent: ['jev/c', 'jev/d'] });
44    expect(recorded).toEqual([song.id]);
45    await t.next({ songs, current: song, user });
46    // loaded once per account; the pick it made is the newest now
47    expect(loadRecent).toHaveBeenCalledTimes(1);
48    expect(picks[1].options.recent[0]).toBe(song.id);
49  });
50
51  it('with nothing of the site playing, asks with no current song', async () => {
52    const { t, picks } = tracker();
53    await t.next({ songs, current: null, user: null });
54    expect(picks[0]).toMatchObject({ current: null, heard: [] });
55  });
56
57  it('plays the next in the list when Jev cannot answer, and says why', async () => {
58    const { t, played } = tracker({ answer: () => null });
59    const r = await t.next({ songs, current: songs[3], user: null });
60    expect(played).toEqual(['jev/a']);
61    expect(r.error).toBe('unreachable');
62  });
63
64  it('ignores a press while a pick is under way', async () => {
65    const { t, played } = tracker();
66    const [one, two] = await Promise.all([
67      t.next({ songs, current: songs[0], user: null }),
68      t.next({ songs, current: songs[0], user: null }),
69    ]);
70    expect(two).toBeNull();
71    expect(one.song.id).toBe('jev/b');
72    expect(played).toEqual(['jev/b']);
73  });
74});