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