atagen

malice

public
Star0
Watch0
17723bb70856104761f7b2d9cba1e5f0d9100c0a
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git
web/test/back-stack.test.tsTypescript133 lines4.3 KB
1
import assert from 'node:assert/strict';
2
import { test } from 'node:test';
3
import { BackStack, type HistoryLike } from '../src/lib/back-stack.ts';
4
5
class FakeHistory implements HistoryLike {
6
  entries: unknown[] = [{ roomId: 'a' }];
7
  index = 0;
8
  onPop: (state: unknown) => void = () => undefined;
9
  get state(): unknown {
10
    return this.entries[this.index];
11
  }
12
  pushState(state: unknown): void {
13
    this.entries.splice(this.index + 1, Infinity, state);
14
    this.index++;
15
  }
16
  replaceState(state: unknown): void {
17
    this.entries[this.index] = state;
18
  }
19
  back(): void {
20
    if (this.index === 0) return;
21
    this.index--;
22
    queueMicrotask(() => this.onPop(this.state));
23
  }
24
}
25
26
function setup() {
27
  const history = new FakeHistory();
28
  const stack = new BackStack(history);
29
  const routed: unknown[] = [];
30
  history.onPop = (state) => {
31
    if (!stack.pop(state)) routed.push(state);
32
  };
33
  return { history, stack, routed };
34
}
35
36
const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
37
38
test('opening a layer adds one entry and back closes it', async () => {
39
  const { history, stack, routed } = setup();
40
  let open = true;
41
  stack.sync('card', true, () => (open = false));
42
  assert.equal(history.entries.length, 2);
43
  history.back();
44
  await tick();
45
  assert.equal(open, false);
46
  assert.equal(history.index, 0);
47
  assert.deepEqual(routed, []);
48
});
49
50
test('closing through the UI pops its entry and swallows the popstate', async () => {
51
  const { history, stack, routed } = setup();
52
  stack.sync('card', true, () => undefined);
53
  stack.sync('card', false, () => undefined);
54
  await tick();
55
  assert.equal(history.index, 0);
56
  assert.deepEqual(routed, []);
57
  assert.equal(stack.depth, 0);
58
});
59
60
test('back closes only the topmost of several layers', async () => {
61
  const { history, stack } = setup();
62
  const closed: string[] = [];
63
  stack.sync('drawer', true, () => closed.push('drawer'));
64
  stack.sync('card', true, () => closed.push('card'));
65
  history.back();
66
  await tick();
67
  assert.deepEqual(closed, ['card']);
68
  assert.equal(stack.depth, 1);
69
});
70
71
test('a route recorded under an open layer replaces its entry', async () => {
72
  const { history, stack, routed } = setup();
73
  stack.sync('drawer', true, () => undefined);
74
  stack.route({ roomId: 'b' }, '/?room=b');
75
  stack.sync('drawer', false, () => undefined);
76
  await tick();
77
  assert.deepEqual(history.entries, [{ roomId: 'a' }, { roomId: 'b' }]);
78
  history.back();
79
  await tick();
80
  assert.deepEqual(routed, [{ roomId: 'a' }]);
81
});
82
83
test('closing a layer then pressing back does not skip a room', async () => {
84
  const { history, stack, routed } = setup();
85
  stack.route({ roomId: 'b' }, '/?room=b');
86
  stack.sync('card', true, () => undefined);
87
  stack.sync('card', false, () => undefined);
88
  await tick();
89
  assert.deepEqual(history.state, { roomId: 'b' });
90
  history.back();
91
  await tick();
92
  assert.deepEqual(routed, [{ roomId: 'a' }]);
93
});
94
95
test('a route recorded right after a UI close waits for the pop', async () => {
96
  const { history, stack } = setup();
97
  stack.sync('menu', true, () => undefined);
98
  stack.sync('menu', false, () => undefined);
99
  stack.route({ roomId: 'b' }, '/?room=b');
100
  await tick();
101
  assert.deepEqual(history.entries, [{ roomId: 'a' }, { roomId: 'b' }]);
102
});
103
104
test('a layer closed under another leaves a stale entry that back skips', async () => {
105
  const { history, stack, routed } = setup();
106
  let dialog = true;
107
  stack.sync('menu', true, () => undefined);
108
  stack.sync('dialog', true, () => (dialog = false));
109
  stack.sync('menu', false, () => undefined);
110
  history.back();
111
  await tick();
112
  assert.equal(dialog, false);
113
  assert.equal(history.index, 0);
114
  assert.equal(routed.length, 0);
115
});
116
117
test('init strips a layer marker left behind by a reload', () => {
118
  const { history, stack } = setup();
119
  history.entries = [{ roomId: 'a' }, { roomId: 'a', backLayer: 'card' }];
120
  history.index = 1;
121
  stack.init();
122
  assert.deepEqual(history.state, { roomId: 'a' });
123
});
124
125
test('a settle timeout releases a pop whose popstate never arrives', async () => {
126
  const { history, stack } = setup();
127
  stack.sync('x', true, () => undefined);
128
  history.back = () => undefined;
129
  stack.sync('x', false, () => undefined);
130
  stack.route({ roomId: 'b' }, '/?room=b');
131
  await new Promise((resolve) => setTimeout(resolve, 500));
132
  assert.deepEqual(history.state, { roomId: 'b' });
133
});