Comprehensive changes including: - Refactor picking slip line handling for multiple document types across print endpoints - Add new `/api/v1/stock-purchases/picking-slip` API for stock booking picking slips - Implement reusable Splitter component and useSplitRatio hook for resizable panels - Add floating/fullscreen invoice viewer to collection confirm page - Add real-time stock booking update event for invoicing tab - Add full test coverage for all new components and APIs - Include temporary chat attachment images for testing purposes
171 lines
5.9 KiB
TypeScript
171 lines
5.9 KiB
TypeScript
import { act, renderHook } from "@testing-library/react";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
|
import { useSplitRatio } from "./use-split-ratio";
|
|
|
|
const STORAGE_KEY = "test:split-ratio";
|
|
|
|
afterEach(() => {
|
|
window.localStorage.clear();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe("useSplitRatio", () => {
|
|
describe("initialisation", () => {
|
|
it("returns the initial value when nothing is stored", () => {
|
|
const { result } = renderHook(() =>
|
|
useSplitRatio({ storageKey: STORAGE_KEY, initial: 0.5 }),
|
|
);
|
|
expect(result.current.ratio).toBe(0.5);
|
|
});
|
|
|
|
it("returns the stored value on a re-render after mount", () => {
|
|
window.localStorage.setItem(STORAGE_KEY, "0.33");
|
|
const { result } = renderHook(() =>
|
|
useSplitRatio({ storageKey: STORAGE_KEY, initial: 0.5 }),
|
|
);
|
|
expect(result.current.ratio).toBeCloseTo(0.33, 5);
|
|
});
|
|
|
|
it("clamps a stored value that fell outside the new bounds", () => {
|
|
window.localStorage.setItem(STORAGE_KEY, "0.05"); // below min
|
|
const { result } = renderHook(() =>
|
|
useSplitRatio({
|
|
storageKey: STORAGE_KEY,
|
|
initial: 0.5,
|
|
min: 0.2,
|
|
max: 0.8,
|
|
}),
|
|
);
|
|
expect(result.current.ratio).toBe(0.2);
|
|
});
|
|
|
|
it("ignores an unparseable stored value and falls back to initial", () => {
|
|
window.localStorage.setItem(STORAGE_KEY, "not-a-number");
|
|
const { result } = renderHook(() =>
|
|
useSplitRatio({ storageKey: STORAGE_KEY, initial: 0.5 }),
|
|
);
|
|
expect(result.current.ratio).toBe(0.5);
|
|
});
|
|
|
|
it("ignores an empty stored value and falls back to initial", () => {
|
|
window.localStorage.setItem(STORAGE_KEY, "");
|
|
const { result } = renderHook(() =>
|
|
useSplitRatio({ storageKey: STORAGE_KEY, initial: 0.5 }),
|
|
);
|
|
expect(result.current.ratio).toBe(0.5);
|
|
});
|
|
});
|
|
|
|
describe("setRatio", () => {
|
|
it("updates the ratio and persists to localStorage", () => {
|
|
const { result } = renderHook(() =>
|
|
useSplitRatio({ storageKey: STORAGE_KEY, initial: 0.5 }),
|
|
);
|
|
act(() => result.current.setRatio(0.6));
|
|
expect(result.current.ratio).toBe(0.6);
|
|
expect(window.localStorage.getItem(STORAGE_KEY)).toBe("0.6");
|
|
});
|
|
|
|
it("clamps values below the min before persisting", () => {
|
|
const { result } = renderHook(() =>
|
|
useSplitRatio({
|
|
storageKey: STORAGE_KEY,
|
|
initial: 0.5,
|
|
min: 0.2,
|
|
max: 0.8,
|
|
}),
|
|
);
|
|
act(() => result.current.setRatio(0.01));
|
|
expect(result.current.ratio).toBe(0.2);
|
|
expect(window.localStorage.getItem(STORAGE_KEY)).toBe("0.2");
|
|
});
|
|
|
|
it("clamps values above the max before persisting", () => {
|
|
const { result } = renderHook(() =>
|
|
useSplitRatio({
|
|
storageKey: STORAGE_KEY,
|
|
initial: 0.5,
|
|
min: 0.2,
|
|
max: 0.8,
|
|
}),
|
|
);
|
|
act(() => result.current.setRatio(0.99));
|
|
expect(result.current.ratio).toBe(0.8);
|
|
expect(window.localStorage.getItem(STORAGE_KEY)).toBe("0.8");
|
|
});
|
|
|
|
it("uses default bounds (0.15, 0.85) when no min/max are passed", () => {
|
|
const { result } = renderHook(() =>
|
|
useSplitRatio({ storageKey: STORAGE_KEY, initial: 0.5 }),
|
|
);
|
|
act(() => result.current.setRatio(0.05));
|
|
expect(result.current.ratio).toBe(0.15);
|
|
act(() => result.current.setRatio(0.95));
|
|
expect(result.current.ratio).toBe(0.85);
|
|
});
|
|
|
|
it("survives a localStorage write failure (quota / Safari private mode)", () => {
|
|
// Stub the prototype method so the failure path runs even when
|
|
// the instance-level setItem isn't spy-able in jsdom. The
|
|
// contract is: the in-memory state still updates, and the
|
|
// failure doesn't break the UI.
|
|
const protoSpy = vi
|
|
.spyOn(Storage.prototype, "setItem")
|
|
.mockImplementation(() => {
|
|
throw new Error("QuotaExceededError");
|
|
});
|
|
|
|
const { result } = renderHook(() =>
|
|
useSplitRatio({ storageKey: STORAGE_KEY, initial: 0.5 }),
|
|
);
|
|
expect(() => {
|
|
act(() => result.current.setRatio(0.7));
|
|
}).not.toThrow();
|
|
expect(result.current.ratio).toBe(0.7);
|
|
// The setter did attempt the write — confirm the spy saw it.
|
|
expect(protoSpy).toHaveBeenCalled();
|
|
|
|
protoSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe("storage key isolation", () => {
|
|
it("two hooks with different keys don't clobber each other", () => {
|
|
window.localStorage.setItem("key-a", "0.25");
|
|
window.localStorage.setItem("key-b", "0.75");
|
|
|
|
const { result: a } = renderHook(() =>
|
|
useSplitRatio({ storageKey: "key-a", initial: 0.5 }),
|
|
);
|
|
const { result: b } = renderHook(() =>
|
|
useSplitRatio({ storageKey: "key-b", initial: 0.5 }),
|
|
);
|
|
|
|
expect(a.current.ratio).toBeCloseTo(0.25, 5);
|
|
expect(b.current.ratio).toBeCloseTo(0.75, 5);
|
|
|
|
act(() => a.current.setRatio(0.3));
|
|
expect(b.current.ratio).toBeCloseTo(0.75, 5); // untouched
|
|
});
|
|
});
|
|
|
|
describe("lazy initialisation", () => {
|
|
it("a fresh mount picks up the latest stored value via the useState initialiser", () => {
|
|
const { result } = renderHook(() =>
|
|
useSplitRatio({ storageKey: STORAGE_KEY, initial: 0.5 }),
|
|
);
|
|
// Simulate another tab writing a new value while this hook was idle.
|
|
window.localStorage.setItem(STORAGE_KEY, "0.61");
|
|
const { result: remounted } = renderHook(() =>
|
|
useSplitRatio({ storageKey: STORAGE_KEY, initial: 0.5 }),
|
|
);
|
|
// The original hook is unaware (no cross-instance sync required
|
|
// — single-tab contract). A fresh mount, however, reads the
|
|
// current localStorage value via the useState initialiser, so
|
|
// it lands on the latest value with no flicker.
|
|
expect(result.current.ratio).toBe(0.5); // unchanged — first hook
|
|
expect(remounted.current.ratio).toBeCloseTo(0.61, 5);
|
|
});
|
|
});
|
|
});
|