atagen

malice

public
Star0
Watch0
5059ed753f8be331e027bc2b84dfbf30b240252e
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git

Blame: web/test/back-stack.test.ts

0e50e0d137afatagen2026-10-26 05:50 UTC1
import assert from 'node:assert/strict';
0e50e0d137afatagen2026-10-26 05:50 UTC2
import { test } from 'node:test';
0e50e0d137afatagen2026-10-26 05:50 UTC3
import { BackStack, type HistoryLike } from '../src/lib/back-stack.ts';
0e50e0d137afatagen2026-10-26 05:50 UTC4
0e50e0d137afatagen2026-10-26 05:50 UTC5
class FakeHistory implements HistoryLike {
0e50e0d137afatagen2026-10-26 05:50 UTC6
  entries: unknown[] = [{ roomId: 'a' }];
0e50e0d137afatagen2026-10-26 05:50 UTC7
  index = 0;
0e50e0d137afatagen2026-10-26 05:50 UTC8
  onPop: (state: unknown) => void = () => undefined;
0e50e0d137afatagen2026-10-26 05:50 UTC9
  get state(): unknown {
0e50e0d137afatagen2026-10-26 05:50 UTC10
    return this.entries[this.index];
0e50e0d137afatagen2026-10-26 05:50 UTC11
  }
0e50e0d137afatagen2026-10-26 05:50 UTC12
  pushState(state: unknown): void {
0e50e0d137afatagen2026-10-26 05:50 UTC13
    this.entries.splice(this.index + 1, Infinity, state);
0e50e0d137afatagen2026-10-26 05:50 UTC14
    this.index++;
0e50e0d137afatagen2026-10-26 05:50 UTC15
  }
0e50e0d137afatagen2026-10-26 05:50 UTC16
  replaceState(state: unknown): void {
0e50e0d137afatagen2026-10-26 05:50 UTC17
    this.entries[this.index] = state;
0e50e0d137afatagen2026-10-26 05:50 UTC18
  }
0e50e0d137afatagen2026-10-26 05:50 UTC19
  back(): void {
0e50e0d137afatagen2026-10-26 05:50 UTC20
    if (this.index === 0) return;
0e50e0d137afatagen2026-10-26 05:50 UTC21
    this.index--;
0e50e0d137afatagen2026-10-26 05:50 UTC22
    queueMicrotask(() => this.onPop(this.state));
0e50e0d137afatagen2026-10-26 05:50 UTC23
  }
0e50e0d137afatagen2026-10-26 05:50 UTC24
}
0e50e0d137afatagen2026-10-26 05:50 UTC25
0e50e0d137afatagen2026-10-26 05:50 UTC26
function setup() {
0e50e0d137afatagen2026-10-26 05:50 UTC27
  const history = new FakeHistory();
0e50e0d137afatagen2026-10-26 05:50 UTC28
  const stack = new BackStack(history);
0e50e0d137afatagen2026-10-26 05:50 UTC29
  const routed: unknown[] = [];
0e50e0d137afatagen2026-10-26 05:50 UTC30
  history.onPop = (state) => {
0e50e0d137afatagen2026-10-26 05:50 UTC31
    if (!stack.pop(state)) routed.push(state);
0e50e0d137afatagen2026-10-26 05:50 UTC32
  };
0e50e0d137afatagen2026-10-26 05:50 UTC33
  return { history, stack, routed };
0e50e0d137afatagen2026-10-26 05:50 UTC34
}
0e50e0d137afatagen2026-10-26 05:50 UTC35
0e50e0d137afatagen2026-10-26 05:50 UTC36
const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
0e50e0d137afatagen2026-10-26 05:50 UTC37
0e50e0d137afatagen2026-10-26 05:50 UTC38
test('opening a layer adds one entry and back closes it', async () => {
0e50e0d137afatagen2026-10-26 05:50 UTC39
  const { history, stack, routed } = setup();
0e50e0d137afatagen2026-10-26 05:50 UTC40
  let open = true;
0e50e0d137afatagen2026-10-26 05:50 UTC41
  stack.sync('card', true, () => (open = false));
0e50e0d137afatagen2026-10-26 05:50 UTC42
  assert.equal(history.entries.length, 2);
0e50e0d137afatagen2026-10-26 05:50 UTC43
  history.back();
0e50e0d137afatagen2026-10-26 05:50 UTC44
  await tick();
0e50e0d137afatagen2026-10-26 05:50 UTC45
  assert.equal(open, false);
0e50e0d137afatagen2026-10-26 05:50 UTC46
  assert.equal(history.index, 0);
0e50e0d137afatagen2026-10-26 05:50 UTC47
  assert.deepEqual(routed, []);
0e50e0d137afatagen2026-10-26 05:50 UTC48
});
0e50e0d137afatagen2026-10-26 05:50 UTC49
0e50e0d137afatagen2026-10-26 05:50 UTC50
test('closing through the UI pops its entry and swallows the popstate', async () => {
0e50e0d137afatagen2026-10-26 05:50 UTC51
  const { history, stack, routed } = setup();
0e50e0d137afatagen2026-10-26 05:50 UTC52
  stack.sync('card', true, () => undefined);
0e50e0d137afatagen2026-10-26 05:50 UTC53
  stack.sync('card', false, () => undefined);
0e50e0d137afatagen2026-10-26 05:50 UTC54
  await tick();
0e50e0d137afatagen2026-10-26 05:50 UTC55
  assert.equal(history.index, 0);
0e50e0d137afatagen2026-10-26 05:50 UTC56
  assert.deepEqual(routed, []);
0e50e0d137afatagen2026-10-26 05:50 UTC57
  assert.equal(stack.depth, 0);
0e50e0d137afatagen2026-10-26 05:50 UTC58
});
0e50e0d137afatagen2026-10-26 05:50 UTC59
0e50e0d137afatagen2026-10-26 05:50 UTC60
test('back closes only the topmost of several layers', async () => {
0e50e0d137afatagen2026-10-26 05:50 UTC61
  const { history, stack } = setup();
0e50e0d137afatagen2026-10-26 05:50 UTC62
  const closed: string[] = [];
0e50e0d137afatagen2026-10-26 05:50 UTC63
  stack.sync('drawer', true, () => closed.push('drawer'));
0e50e0d137afatagen2026-10-26 05:50 UTC64
  stack.sync('card', true, () => closed.push('card'));
0e50e0d137afatagen2026-10-26 05:50 UTC65
  history.back();
0e50e0d137afatagen2026-10-26 05:50 UTC66
  await tick();
0e50e0d137afatagen2026-10-26 05:50 UTC67
  assert.deepEqual(closed, ['card']);
0e50e0d137afatagen2026-10-26 05:50 UTC68
  assert.equal(stack.depth, 1);
0e50e0d137afatagen2026-10-26 05:50 UTC69
});
0e50e0d137afatagen2026-10-26 05:50 UTC70
0e50e0d137afatagen2026-10-26 05:50 UTC71
test('a route recorded under an open layer replaces its entry', async () => {
0e50e0d137afatagen2026-10-26 05:50 UTC72
  const { history, stack, routed } = setup();
0e50e0d137afatagen2026-10-26 05:50 UTC73
  stack.sync('drawer', true, () => undefined);
0e50e0d137afatagen2026-10-26 05:50 UTC74
  stack.route({ roomId: 'b' }, '/?room=b');
0e50e0d137afatagen2026-10-26 05:50 UTC75
  stack.sync('drawer', false, () => undefined);
0e50e0d137afatagen2026-10-26 05:50 UTC76
  await tick();
0e50e0d137afatagen2026-10-26 05:50 UTC77
  assert.deepEqual(history.entries, [{ roomId: 'a' }, { roomId: 'b' }]);
0e50e0d137afatagen2026-10-26 05:50 UTC78
  history.back();
0e50e0d137afatagen2026-10-26 05:50 UTC79
  await tick();
0e50e0d137afatagen2026-10-26 05:50 UTC80
  assert.deepEqual(routed, [{ roomId: 'a' }]);
0e50e0d137afatagen2026-10-26 05:50 UTC81
});
0e50e0d137afatagen2026-10-26 05:50 UTC82
0e50e0d137afatagen2026-10-26 05:50 UTC83
test('closing a layer then pressing back does not skip a room', async () => {
0e50e0d137afatagen2026-10-26 05:50 UTC84
  const { history, stack, routed } = setup();
0e50e0d137afatagen2026-10-26 05:50 UTC85
  stack.route({ roomId: 'b' }, '/?room=b');
0e50e0d137afatagen2026-10-26 05:50 UTC86
  stack.sync('card', true, () => undefined);
0e50e0d137afatagen2026-10-26 05:50 UTC87
  stack.sync('card', false, () => undefined);
0e50e0d137afatagen2026-10-26 05:50 UTC88
  await tick();
0e50e0d137afatagen2026-10-26 05:50 UTC89
  assert.deepEqual(history.state, { roomId: 'b' });
0e50e0d137afatagen2026-10-26 05:50 UTC90
  history.back();
0e50e0d137afatagen2026-10-26 05:50 UTC91
  await tick();
0e50e0d137afatagen2026-10-26 05:50 UTC92
  assert.deepEqual(routed, [{ roomId: 'a' }]);
0e50e0d137afatagen2026-10-26 05:50 UTC93
});
0e50e0d137afatagen2026-10-26 05:50 UTC94
0e50e0d137afatagen2026-10-26 05:50 UTC95
test('a route recorded right after a UI close waits for the pop', async () => {
0e50e0d137afatagen2026-10-26 05:50 UTC96
  const { history, stack } = setup();
0e50e0d137afatagen2026-10-26 05:50 UTC97
  stack.sync('menu', true, () => undefined);
0e50e0d137afatagen2026-10-26 05:50 UTC98
  stack.sync('menu', false, () => undefined);
0e50e0d137afatagen2026-10-26 05:50 UTC99
  stack.route({ roomId: 'b' }, '/?room=b');
0e50e0d137afatagen2026-10-26 05:50 UTC100
  await tick();
0e50e0d137afatagen2026-10-26 05:50 UTC101
  assert.deepEqual(history.entries, [{ roomId: 'a' }, { roomId: 'b' }]);
0e50e0d137afatagen2026-10-26 05:50 UTC102
});
0e50e0d137afatagen2026-10-26 05:50 UTC103
0e50e0d137afatagen2026-10-26 05:50 UTC104
test('a layer closed under another leaves a stale entry that back skips', async () => {
0e50e0d137afatagen2026-10-26 05:50 UTC105
  const { history, stack, routed } = setup();
0e50e0d137afatagen2026-10-26 05:50 UTC106
  let dialog = true;
0e50e0d137afatagen2026-10-26 05:50 UTC107
  stack.sync('menu', true, () => undefined);
0e50e0d137afatagen2026-10-26 05:50 UTC108
  stack.sync('dialog', true, () => (dialog = false));
0e50e0d137afatagen2026-10-26 05:50 UTC109
  stack.sync('menu', false, () => undefined);
0e50e0d137afatagen2026-10-26 05:50 UTC110
  history.back();
0e50e0d137afatagen2026-10-26 05:50 UTC111
  await tick();
0e50e0d137afatagen2026-10-26 05:50 UTC112
  assert.equal(dialog, false);
0e50e0d137afatagen2026-10-26 05:50 UTC113
  assert.equal(history.index, 0);
0e50e0d137afatagen2026-10-26 05:50 UTC114
  assert.equal(routed.length, 0);
0e50e0d137afatagen2026-10-26 05:50 UTC115
});
0e50e0d137afatagen2026-10-26 05:50 UTC116
0e50e0d137afatagen2026-10-26 05:50 UTC117
test('init strips a layer marker left behind by a reload', () => {
0e50e0d137afatagen2026-10-26 05:50 UTC118
  const { history, stack } = setup();
0e50e0d137afatagen2026-10-26 05:50 UTC119
  history.entries = [{ roomId: 'a' }, { roomId: 'a', backLayer: 'card' }];
0e50e0d137afatagen2026-10-26 05:50 UTC120
  history.index = 1;
0e50e0d137afatagen2026-10-26 05:50 UTC121
  stack.init();
0e50e0d137afatagen2026-10-26 05:50 UTC122
  assert.deepEqual(history.state, { roomId: 'a' });
0e50e0d137afatagen2026-10-26 05:50 UTC123
});
0e50e0d137afatagen2026-10-26 05:50 UTC124
0e50e0d137afatagen2026-10-26 05:50 UTC125
test('a settle timeout releases a pop whose popstate never arrives', async () => {
0e50e0d137afatagen2026-10-26 05:50 UTC126
  const { history, stack } = setup();
0e50e0d137afatagen2026-10-26 05:50 UTC127
  stack.sync('x', true, () => undefined);
0e50e0d137afatagen2026-10-26 05:50 UTC128
  history.back = () => undefined;
0e50e0d137afatagen2026-10-26 05:50 UTC129
  stack.sync('x', false, () => undefined);
0e50e0d137afatagen2026-10-26 05:50 UTC130
  stack.route({ roomId: 'b' }, '/?room=b');
0e50e0d137afatagen2026-10-26 05:50 UTC131
  await new Promise((resolve) => setTimeout(resolve, 500));
0e50e0d137afatagen2026-10-26 05:50 UTC132
  assert.deepEqual(history.state, { roomId: 'b' });
0e50e0d137afatagen2026-10-26 05:50 UTC133
});