Files
siftlode/frontend/src/lib/useUndoable.dom.test.ts
T
peter 7dbda60815 fix(ui): an undo in the same tick as a rebase steps back to a rebased snapshot
rebase updated the state only, so the mirror the callbacks read still held the removed
row and an undo right after it wrote that row back for a moment.
2026-10-04 04:16:52 +02:00

41 lines
1.7 KiB
TypeScript

// @vitest-environment jsdom
//
// The undo container chains calls made in one tick: a `reset` not yet committed is what the next
// call builds on. A `rebase` read the mirror ref, which only follows a commit, so it rebased the
// value from BEFORE the reset and wrote that back — the playlist page, remounted under StrictMode
// over a cached list, re-ran its seeding effect (reset, then rebase) and painted an empty list.
import { describe, expect, it } from "vitest";
import { act, renderHook } from "@testing-library/react";
import { useUndoable } from "./useUndoable";
describe("useUndoable", () => {
it("rebases the value a reset in the same tick installed, not the one before it", () => {
const { result } = renderHook(() => useUndoable<number[]>([]));
act(() => {
result.current.reset([1, 2]);
result.current.rebase((xs) => xs.map((x) => x * 10));
});
expect(result.current.value).toEqual([10, 20]);
});
it("keeps the undo history through a rebase", () => {
const { result } = renderHook(() => useUndoable<number[]>([1]));
act(() => result.current.set([1, 2]));
act(() => result.current.rebase((xs) => xs.map((x) => x + 1)));
expect(result.current.value).toEqual([2, 3]);
act(() => result.current.undo());
expect(result.current.value).toEqual([2]);
});
it("undoes to a rebased snapshot when the undo comes in the same tick as the rebase", () => {
const { result } = renderHook(() => useUndoable<number[]>([1, 2]));
act(() => result.current.set([2, 1]));
act(() => {
result.current.rebase((xs) => xs.filter((x) => x !== 2)); // a row removed
result.current.undo();
});
expect(result.current.value).toEqual([1]);
expect(result.current.canRedo).toBe(true);
});
});