157 lines
3.5 KiB
TypeScript
157 lines
3.5 KiB
TypeScript
import {render, screen, act} from "@testing-library/react";
|
|
import "@testing-library/jest-dom";
|
|
import {type Cell, cell, useCell} from "../../src/utils/cellStore.ts";
|
|
|
|
describe("cell store (unit)", () => {
|
|
it("returns initial value with get()", () => {
|
|
const c = cell(123);
|
|
expect(c.get()).toBe(123);
|
|
});
|
|
|
|
it("updates value with set(next)", () => {
|
|
const c = cell("a");
|
|
c.set("b");
|
|
expect(c.get()).toBe("b");
|
|
});
|
|
|
|
it("gives previous value in set(updater)", () => {
|
|
const c = cell(1);
|
|
c.set((prev) => prev + 2);
|
|
expect(c.get()).toBe(3);
|
|
});
|
|
|
|
it("calls subscribe callback on set", () => {
|
|
const c = cell(0);
|
|
const cb = jest.fn();
|
|
const unsub = c.subscribe(cb);
|
|
|
|
c.set(1);
|
|
c.set(2);
|
|
|
|
expect(cb).toHaveBeenCalledTimes(2);
|
|
unsub();
|
|
});
|
|
|
|
it("stops notifications when unsubscribing", () => {
|
|
const c = cell(0);
|
|
const cb = jest.fn();
|
|
const unsub = c.subscribe(cb);
|
|
|
|
c.set(1);
|
|
unsub();
|
|
c.set(2);
|
|
|
|
expect(cb).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("updates multiple listeners", () => {
|
|
const c = cell("x");
|
|
const a = jest.fn();
|
|
const b = jest.fn();
|
|
const ua = c.subscribe(a);
|
|
const ub = c.subscribe(b);
|
|
|
|
c.set("y");
|
|
expect(a).toHaveBeenCalledTimes(1);
|
|
expect(b).toHaveBeenCalledTimes(1);
|
|
|
|
ua();
|
|
ub();
|
|
});
|
|
});
|
|
|
|
describe("cell store (integration)", () => {
|
|
function View({c, label}: { c: Cell<any>; label: string }) {
|
|
const v = useCell(c);
|
|
// count renders to verify re-render behavior
|
|
(View as any).__renders = ((View as any).__renders ?? 0) + 1;
|
|
return <div data-testid={label}>{String(v)}</div>;
|
|
}
|
|
|
|
it("reads initial value and updates on set", () => {
|
|
const c = cell("hello");
|
|
|
|
render(<View c={c} label="value"/>);
|
|
|
|
expect(screen.getByTestId("value")).toHaveTextContent("hello");
|
|
|
|
act(() => {
|
|
c.set("world");
|
|
});
|
|
|
|
expect(screen.getByTestId("value")).toHaveTextContent("world");
|
|
});
|
|
|
|
it("triggers one re-render with set", () => {
|
|
const c = cell(1);
|
|
(View as any).__renders = 0;
|
|
|
|
render(<View c={c} label="num"/>);
|
|
|
|
const rendersAfterMount = (View as any).__renders;
|
|
|
|
act(() => {
|
|
c.set((prev: number) => prev + 1);
|
|
});
|
|
|
|
// exactly one extra render from the update
|
|
expect((View as any).__renders).toBe(rendersAfterMount + 1);
|
|
expect(screen.getByTestId("num")).toHaveTextContent("2");
|
|
});
|
|
|
|
it("unsubscribes on unmount (no errors on later sets)", () => {
|
|
const c = cell("a");
|
|
|
|
const {unmount} = render(<View c={c} label="value"/>);
|
|
|
|
unmount();
|
|
|
|
// should not throw even though there was a subscriber
|
|
expect(() =>
|
|
act(() => {
|
|
c.set("b");
|
|
})
|
|
).not.toThrow();
|
|
});
|
|
|
|
it("only re-renders components that use the cell", () => {
|
|
const a = cell("A");
|
|
const b = cell("B");
|
|
|
|
let rendersA = 0;
|
|
let rendersB = 0;
|
|
|
|
function A() {
|
|
const v = useCell(a);
|
|
rendersA++;
|
|
return <div data-testid="A">{v}</div>;
|
|
}
|
|
|
|
function B() {
|
|
const v = useCell(b);
|
|
rendersB++;
|
|
return <div data-testid="B">{v}</div>;
|
|
}
|
|
|
|
render(
|
|
<>
|
|
<A/>
|
|
<B/>
|
|
</>
|
|
);
|
|
|
|
const rendersAAfterMount = rendersA;
|
|
const rendersBAfterMount = rendersB;
|
|
|
|
act(() => {
|
|
a.set("A2"); // only A should update
|
|
});
|
|
|
|
expect(screen.getByTestId("A")).toHaveTextContent("A2");
|
|
expect(screen.getByTestId("B")).toHaveTextContent("B");
|
|
|
|
expect(rendersA).toBe(rendersAAfterMount + 1);
|
|
expect(rendersB).toBe(rendersBAfterMount); // unchanged
|
|
});
|
|
});
|