feat(drill): DrillStackProvider — zustand-backed peek stack with 2-level cap
This commit is contained in:
@@ -0,0 +1,45 @@
|
|||||||
|
// @vitest-environment happy-dom
|
||||||
|
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||||
|
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { renderHook, act } from "@testing-library/react";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import {
|
||||||
|
DrillStackProvider,
|
||||||
|
useDrillStack,
|
||||||
|
} from "@/components/drill/DrillStackProvider";
|
||||||
|
|
||||||
|
function wrapper({ children }: { children: ReactNode }) {
|
||||||
|
return <DrillStackProvider>{children}</DrillStackProvider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("DrillStackProvider", () => {
|
||||||
|
it("starts with an empty stack", () => {
|
||||||
|
const { result } = renderHook(() => useDrillStack(), { wrapper });
|
||||||
|
expect(result.current.stack).toEqual([]);
|
||||||
|
expect(result.current.openPeek).toBeInstanceOf(Function);
|
||||||
|
expect(result.current.closeTop).toBeInstanceOf(Function);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("openPeek pushes one entry; closeTop pops it", () => {
|
||||||
|
const { result } = renderHook(() => useDrillStack(), { wrapper });
|
||||||
|
act(() => result.current.openPeek({ kind: "payer", payerId: "SKCO0" }));
|
||||||
|
expect(result.current.stack).toEqual([
|
||||||
|
{ kind: "payer", payerId: "SKCO0" },
|
||||||
|
]);
|
||||||
|
act(() => result.current.closeTop());
|
||||||
|
expect(result.current.stack).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("openPeek replaces the previous peek when called twice", () => {
|
||||||
|
const { result } = renderHook(() => useDrillStack(), { wrapper });
|
||||||
|
act(() => result.current.openPeek({ kind: "payer", payerId: "A" }));
|
||||||
|
// The hook only governs peeks (the bottom drawer is owned by the
|
||||||
|
// page, URL-backed, and lives outside this stack). When openPeek
|
||||||
|
// is called while a peek is already on the stack, the new peek
|
||||||
|
// replaces the previous one rather than stacking above it.
|
||||||
|
act(() => result.current.openPeek({ kind: "rule", rule: "R050" }));
|
||||||
|
expect(result.current.stack).toHaveLength(1);
|
||||||
|
expect(result.current.stack[0]).toEqual({ kind: "rule", rule: "R050" });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { createContext, useContext, useMemo, type ReactNode } from "react";
|
||||||
|
import { create } from "zustand";
|
||||||
|
|
||||||
|
export type PeekPayload =
|
||||||
|
| { kind: "payer"; payerId: string }
|
||||||
|
| { kind: "rule"; rule: string };
|
||||||
|
|
||||||
|
interface DrillState {
|
||||||
|
stack: PeekPayload[];
|
||||||
|
openPeek: (p: PeekPayload) => void;
|
||||||
|
closeTop: () => void;
|
||||||
|
closeAll: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One zustand store per provider instance (factory) so multiple
|
||||||
|
// providers (e.g. in tests) don't share state.
|
||||||
|
function makeStore() {
|
||||||
|
return create<DrillState>((set) => ({
|
||||||
|
stack: [],
|
||||||
|
openPeek: (p) =>
|
||||||
|
set((s) => ({
|
||||||
|
// Cap at 2 levels total: one drawer + one peek. When called and
|
||||||
|
// the stack already has one peek, replace it.
|
||||||
|
stack: s.stack.length >= 1 ? [p] : [p],
|
||||||
|
})),
|
||||||
|
closeTop: () => set((s) => ({ stack: s.stack.slice(0, -1) })),
|
||||||
|
closeAll: () => set({ stack: [] }),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
type StoreApi = ReturnType<typeof makeStore>;
|
||||||
|
|
||||||
|
const Ctx = createContext<StoreApi | null>(null);
|
||||||
|
|
||||||
|
export function DrillStackProvider({ children }: { children: ReactNode }) {
|
||||||
|
// useMemo so the store instance is stable across renders.
|
||||||
|
const store = useMemo(makeStore, []);
|
||||||
|
return <Ctx.Provider value={store}>{children}</Ctx.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDrillStack() {
|
||||||
|
const store = useContext(Ctx);
|
||||||
|
if (!store) throw new Error("useDrillStack must be used within DrillStackProvider");
|
||||||
|
// Subscribe to just `stack` so consumers re-render only on stack
|
||||||
|
// changes (not on every state update).
|
||||||
|
const stack = store((s) => s.stack);
|
||||||
|
return {
|
||||||
|
stack,
|
||||||
|
openPeek: store.getState().openPeek,
|
||||||
|
closeTop: store.getState().closeTop,
|
||||||
|
closeAll: store.getState().closeAll,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user