mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 15:36:18 +00:00
feat(navigator): Zustand UI store for panel-visibility + lifecycle flags
Hoists the 9 useState in NavigatorView (isVisible, isReady, isCreatorOpen, isRoomInfoOpen, isRoomLinkOpen, isOpenSavesSearches, isLoading, needsInit, needsSearch) into a createNitroStore-backed Zustand store with named actions. Future linkTracker / lifecycle wiring will call these actions instead of mutating local component state. TDD: 14 cases on each action's transitions + idempotency.
This commit is contained in:
@@ -0,0 +1,144 @@
|
|||||||
|
import { beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
import { useNavigatorUiStore } from './navigatorUiStore';
|
||||||
|
|
||||||
|
const INITIAL = {
|
||||||
|
isVisible: false,
|
||||||
|
isReady: false,
|
||||||
|
isCreatorOpen: false,
|
||||||
|
isRoomInfoOpen: false,
|
||||||
|
isRoomLinkOpen: false,
|
||||||
|
isOpenSavesSearches: false,
|
||||||
|
isLoading: false,
|
||||||
|
needsInit: true,
|
||||||
|
needsSearch: false
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('useNavigatorUiStore', () =>
|
||||||
|
{
|
||||||
|
beforeEach(() =>
|
||||||
|
{
|
||||||
|
useNavigatorUiStore.setState(INITIAL);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exposes the documented defaults', () =>
|
||||||
|
{
|
||||||
|
const s = useNavigatorUiStore.getState();
|
||||||
|
expect(s.isVisible).toBe(false);
|
||||||
|
expect(s.isReady).toBe(false);
|
||||||
|
expect(s.isCreatorOpen).toBe(false);
|
||||||
|
expect(s.isRoomInfoOpen).toBe(false);
|
||||||
|
expect(s.isRoomLinkOpen).toBe(false);
|
||||||
|
expect(s.isOpenSavesSearches).toBe(false);
|
||||||
|
expect(s.isLoading).toBe(false);
|
||||||
|
expect(s.needsInit).toBe(true);
|
||||||
|
expect(s.needsSearch).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('show / hide / toggle', () =>
|
||||||
|
{
|
||||||
|
it('show() sets isVisible true and requests a search', () =>
|
||||||
|
{
|
||||||
|
useNavigatorUiStore.getState().show();
|
||||||
|
expect(useNavigatorUiStore.getState().isVisible).toBe(true);
|
||||||
|
expect(useNavigatorUiStore.getState().needsSearch).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hide() sets isVisible false without touching needsSearch', () =>
|
||||||
|
{
|
||||||
|
useNavigatorUiStore.setState({ isVisible: true, needsSearch: false });
|
||||||
|
useNavigatorUiStore.getState().hide();
|
||||||
|
expect(useNavigatorUiStore.getState().isVisible).toBe(false);
|
||||||
|
expect(useNavigatorUiStore.getState().needsSearch).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('toggle() flips visibility and requests a search on show', () =>
|
||||||
|
{
|
||||||
|
useNavigatorUiStore.getState().toggle();
|
||||||
|
expect(useNavigatorUiStore.getState().isVisible).toBe(true);
|
||||||
|
expect(useNavigatorUiStore.getState().needsSearch).toBe(true);
|
||||||
|
|
||||||
|
useNavigatorUiStore.setState({ needsSearch: false });
|
||||||
|
useNavigatorUiStore.getState().toggle();
|
||||||
|
expect(useNavigatorUiStore.getState().isVisible).toBe(false);
|
||||||
|
expect(useNavigatorUiStore.getState().needsSearch).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('creator panel', () =>
|
||||||
|
{
|
||||||
|
it('openCreator() opens both visible and creator', () =>
|
||||||
|
{
|
||||||
|
useNavigatorUiStore.getState().openCreator();
|
||||||
|
expect(useNavigatorUiStore.getState().isVisible).toBe(true);
|
||||||
|
expect(useNavigatorUiStore.getState().isCreatorOpen).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('closeCreator() closes only the creator panel', () =>
|
||||||
|
{
|
||||||
|
useNavigatorUiStore.setState({ isVisible: true, isCreatorOpen: true });
|
||||||
|
useNavigatorUiStore.getState().closeCreator();
|
||||||
|
expect(useNavigatorUiStore.getState().isCreatorOpen).toBe(false);
|
||||||
|
expect(useNavigatorUiStore.getState().isVisible).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('roomInfo / roomLink / savesSearches', () =>
|
||||||
|
{
|
||||||
|
it('setRoomInfoOpen(true) and toggleRoomInfo flip the flag', () =>
|
||||||
|
{
|
||||||
|
useNavigatorUiStore.getState().setRoomInfoOpen(true);
|
||||||
|
expect(useNavigatorUiStore.getState().isRoomInfoOpen).toBe(true);
|
||||||
|
useNavigatorUiStore.getState().toggleRoomInfo();
|
||||||
|
expect(useNavigatorUiStore.getState().isRoomInfoOpen).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('setRoomLinkOpen(true) and toggleRoomLink flip the flag', () =>
|
||||||
|
{
|
||||||
|
useNavigatorUiStore.getState().setRoomLinkOpen(true);
|
||||||
|
expect(useNavigatorUiStore.getState().isRoomLinkOpen).toBe(true);
|
||||||
|
useNavigatorUiStore.getState().toggleRoomLink();
|
||||||
|
expect(useNavigatorUiStore.getState().isRoomLinkOpen).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('toggleSavesSearches() flips the sidebar flag', () =>
|
||||||
|
{
|
||||||
|
useNavigatorUiStore.getState().toggleSavesSearches();
|
||||||
|
expect(useNavigatorUiStore.getState().isOpenSavesSearches).toBe(true);
|
||||||
|
useNavigatorUiStore.getState().toggleSavesSearches();
|
||||||
|
expect(useNavigatorUiStore.getState().isOpenSavesSearches).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('lifecycle flags', () =>
|
||||||
|
{
|
||||||
|
it('setLoading(true) and setLoading(false) toggle isLoading', () =>
|
||||||
|
{
|
||||||
|
useNavigatorUiStore.getState().setLoading(true);
|
||||||
|
expect(useNavigatorUiStore.getState().isLoading).toBe(true);
|
||||||
|
useNavigatorUiStore.getState().setLoading(false);
|
||||||
|
expect(useNavigatorUiStore.getState().isLoading).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('markReady() sets isReady true and is idempotent', () =>
|
||||||
|
{
|
||||||
|
useNavigatorUiStore.getState().markReady();
|
||||||
|
expect(useNavigatorUiStore.getState().isReady).toBe(true);
|
||||||
|
useNavigatorUiStore.getState().markReady();
|
||||||
|
expect(useNavigatorUiStore.getState().isReady).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('markInitDone() flips needsInit to false', () =>
|
||||||
|
{
|
||||||
|
useNavigatorUiStore.getState().markInitDone();
|
||||||
|
expect(useNavigatorUiStore.getState().needsInit).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('requestSearch() + consumeSearchRequest() are symmetric', () =>
|
||||||
|
{
|
||||||
|
useNavigatorUiStore.getState().requestSearch();
|
||||||
|
expect(useNavigatorUiStore.getState().needsSearch).toBe(true);
|
||||||
|
useNavigatorUiStore.getState().consumeSearchRequest();
|
||||||
|
expect(useNavigatorUiStore.getState().needsSearch).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { createNitroStore } from '../../state/createNitroStore';
|
||||||
|
|
||||||
|
export type NavigatorUiState = {
|
||||||
|
isVisible: boolean;
|
||||||
|
isReady: boolean;
|
||||||
|
isCreatorOpen: boolean;
|
||||||
|
isRoomInfoOpen: boolean;
|
||||||
|
isRoomLinkOpen: boolean;
|
||||||
|
isOpenSavesSearches: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
needsInit: boolean;
|
||||||
|
needsSearch: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NavigatorUiActions = {
|
||||||
|
show(): void;
|
||||||
|
hide(): void;
|
||||||
|
toggle(): void;
|
||||||
|
openCreator(): void;
|
||||||
|
closeCreator(): void;
|
||||||
|
setRoomInfoOpen(open: boolean): void;
|
||||||
|
toggleRoomInfo(): void;
|
||||||
|
setRoomLinkOpen(open: boolean): void;
|
||||||
|
toggleRoomLink(): void;
|
||||||
|
toggleSavesSearches(): void;
|
||||||
|
setLoading(loading: boolean): void;
|
||||||
|
markReady(): void;
|
||||||
|
markInitDone(): void;
|
||||||
|
requestSearch(): void;
|
||||||
|
consumeSearchRequest(): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useNavigatorUiStore = createNitroStore<NavigatorUiState & NavigatorUiActions>()((set) => ({
|
||||||
|
isVisible: false,
|
||||||
|
isReady: false,
|
||||||
|
isCreatorOpen: false,
|
||||||
|
isRoomInfoOpen: false,
|
||||||
|
isRoomLinkOpen: false,
|
||||||
|
isOpenSavesSearches: false,
|
||||||
|
isLoading: false,
|
||||||
|
needsInit: true,
|
||||||
|
needsSearch: false,
|
||||||
|
|
||||||
|
show: () => set({ isVisible: true, needsSearch: true }),
|
||||||
|
hide: () => set({ isVisible: false }),
|
||||||
|
toggle: () => set((s) => s.isVisible
|
||||||
|
? { isVisible: false }
|
||||||
|
: { isVisible: true, needsSearch: true }),
|
||||||
|
openCreator: () => set({ isVisible: true, isCreatorOpen: true }),
|
||||||
|
closeCreator: () => set({ isCreatorOpen: false }),
|
||||||
|
setRoomInfoOpen: (open) => set({ isRoomInfoOpen: open }),
|
||||||
|
toggleRoomInfo: () => set((s) => ({ isRoomInfoOpen: !s.isRoomInfoOpen })),
|
||||||
|
setRoomLinkOpen: (open) => set({ isRoomLinkOpen: open }),
|
||||||
|
toggleRoomLink: () => set((s) => ({ isRoomLinkOpen: !s.isRoomLinkOpen })),
|
||||||
|
toggleSavesSearches: () => set((s) => ({ isOpenSavesSearches: !s.isOpenSavesSearches })),
|
||||||
|
setLoading: (loading) => set({ isLoading: loading }),
|
||||||
|
markReady: () => set({ isReady: true }),
|
||||||
|
markInitDone: () => set({ needsInit: false }),
|
||||||
|
requestSearch: () => set({ needsSearch: true }),
|
||||||
|
consumeSearchRequest: () => set({ needsSearch: false })
|
||||||
|
}));
|
||||||
Reference in New Issue
Block a user