mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 23:16:21 +00:00
feat(navigator): extract useDoorState (TDD) – Task 2
- Add `src/hooks/rooms/widgets/useDoorState.ts`: useBetween-based singleton wrapping DoorbellMessageEvent / RoomDoorbellAcceptedEvent / FlatAccessDeniedMessageEvent / GenericErrorEvent / GetGuestRoomResultEvent; all 5 handlers wrapped in useCallback([]) so their references are stable across useBetween tick() calls and the effect dep-array never triggers re-registration. - Add `src/hooks/rooms/widgets/useDoorState.test.tsx`: 11-case Vitest suite (initial state, 5 event transitions, 2 no-op guards, GetGuestRoomResultEvent doorbell/password paths, reset()). - Extend `src/nitro-renderer.mock.ts`: new MessageEvent base class with callBack/type/getParser; DoorbellMessageEvent / RoomDoorbellAcceptedEvent / FlatAccessDeniedMessageEvent / GenericErrorEvent / GetGuestRoomResultEvent concrete stubs; RoomDataParser.DOORBELL_STATE + PASSWORD_STATE; separate msgListeners map (cleared independently of NitroEvent listeners so useBetween subscriptions survive between test cases); WeakMap wrapper for correct removeMessageEvent; GetCommunication routes to msgListeners. All 11 useDoorState tests pass; full suite 453/456 (3 pre-existing FloorplanCanvasSVG jsdom/SVG-CTM failures unrelated to this task).
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { DoorbellMessageEvent, FlatAccessDeniedMessageEvent,
|
||||
GenericErrorEvent, GetGuestRoomResultEvent, RoomDataParser,
|
||||
RoomDoorbellAcceptedEvent } from '@nitrots/nitro-renderer';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { DoorStateType } from '../../../api';
|
||||
import { clearMockEventDispatcher, mockEventDispatcher } from '../../../nitro-renderer.mock';
|
||||
import { useDoorState } from './useDoorState';
|
||||
|
||||
const makeParserlessEvent = (klass: any, parser: any) =>
|
||||
{
|
||||
const ev = new klass();
|
||||
(ev as any).getParser = () => parser;
|
||||
return ev;
|
||||
};
|
||||
|
||||
describe('useDoorState', () =>
|
||||
{
|
||||
beforeEach(() =>
|
||||
{
|
||||
clearMockEventDispatcher();
|
||||
});
|
||||
|
||||
it('exposes the initial NONE snapshot', () =>
|
||||
{
|
||||
const { result } = renderHook(() => useDoorState());
|
||||
expect(result.current.snapshot.state).toBe(DoorStateType.NONE);
|
||||
expect(result.current.snapshot.roomInfo).toBeNull();
|
||||
});
|
||||
|
||||
it('DoorbellMessageEvent with empty userName -> STATE_WAITING', () =>
|
||||
{
|
||||
const { result } = renderHook(() => useDoorState());
|
||||
act(() =>
|
||||
{
|
||||
mockEventDispatcher.dispatchEvent(makeParserlessEvent(DoorbellMessageEvent, { userName: '' }));
|
||||
});
|
||||
expect(result.current.snapshot.state).toBe(DoorStateType.STATE_WAITING);
|
||||
});
|
||||
|
||||
it('DoorbellMessageEvent with non-empty userName does NOT change state', () =>
|
||||
{
|
||||
const { result } = renderHook(() => useDoorState());
|
||||
const before = result.current.snapshot.state;
|
||||
act(() =>
|
||||
{
|
||||
mockEventDispatcher.dispatchEvent(makeParserlessEvent(DoorbellMessageEvent, { userName: 'someone' }));
|
||||
});
|
||||
expect(result.current.snapshot.state).toBe(before);
|
||||
});
|
||||
|
||||
it('RoomDoorbellAcceptedEvent (empty userName) -> STATE_ACCEPTED', () =>
|
||||
{
|
||||
const { result } = renderHook(() => useDoorState());
|
||||
act(() =>
|
||||
{
|
||||
mockEventDispatcher.dispatchEvent(makeParserlessEvent(RoomDoorbellAcceptedEvent, { userName: '' }));
|
||||
});
|
||||
expect(result.current.snapshot.state).toBe(DoorStateType.STATE_ACCEPTED);
|
||||
});
|
||||
|
||||
it('FlatAccessDeniedMessageEvent (empty userName) -> STATE_NO_ANSWER', () =>
|
||||
{
|
||||
const { result } = renderHook(() => useDoorState());
|
||||
act(() =>
|
||||
{
|
||||
mockEventDispatcher.dispatchEvent(makeParserlessEvent(FlatAccessDeniedMessageEvent, { userName: '' }));
|
||||
});
|
||||
expect(result.current.snapshot.state).toBe(DoorStateType.STATE_NO_ANSWER);
|
||||
});
|
||||
|
||||
it('GenericErrorEvent -100002 -> STATE_WRONG_PASSWORD', () =>
|
||||
{
|
||||
const { result } = renderHook(() => useDoorState());
|
||||
act(() =>
|
||||
{
|
||||
mockEventDispatcher.dispatchEvent(makeParserlessEvent(GenericErrorEvent, { errorCode: -100002 }));
|
||||
});
|
||||
expect(result.current.snapshot.state).toBe(DoorStateType.STATE_WRONG_PASSWORD);
|
||||
});
|
||||
|
||||
it('GenericErrorEvent 4010 does NOT touch door state', () =>
|
||||
{
|
||||
const { result } = renderHook(() => useDoorState());
|
||||
const before = result.current.snapshot.state;
|
||||
act(() =>
|
||||
{
|
||||
mockEventDispatcher.dispatchEvent(makeParserlessEvent(GenericErrorEvent, { errorCode: 4010 }));
|
||||
});
|
||||
expect(result.current.snapshot.state).toBe(before);
|
||||
});
|
||||
|
||||
it('GetGuestRoomResultEvent with roomForward + DOORBELL_STATE -> START_DOORBELL', () =>
|
||||
{
|
||||
const { result } = renderHook(() => useDoorState());
|
||||
const fakeRoomData: any = { roomId: 42, roomName: 'r', ownerName: 'other', doorMode: RoomDataParser.DOORBELL_STATE };
|
||||
act(() =>
|
||||
{
|
||||
mockEventDispatcher.dispatchEvent(makeParserlessEvent(GetGuestRoomResultEvent, {
|
||||
roomForward: true,
|
||||
isGroupMember: false,
|
||||
data: fakeRoomData
|
||||
}));
|
||||
});
|
||||
expect(result.current.snapshot.state).toBe(DoorStateType.START_DOORBELL);
|
||||
expect(result.current.snapshot.roomInfo).toBe(fakeRoomData);
|
||||
});
|
||||
|
||||
it('GetGuestRoomResultEvent with roomForward + PASSWORD_STATE -> START_PASSWORD', () =>
|
||||
{
|
||||
const { result } = renderHook(() => useDoorState());
|
||||
const fakeRoomData: any = { roomId: 42, roomName: 'r', ownerName: 'other', doorMode: RoomDataParser.PASSWORD_STATE };
|
||||
act(() =>
|
||||
{
|
||||
mockEventDispatcher.dispatchEvent(makeParserlessEvent(GetGuestRoomResultEvent, {
|
||||
roomForward: true,
|
||||
isGroupMember: false,
|
||||
data: fakeRoomData
|
||||
}));
|
||||
});
|
||||
expect(result.current.snapshot.state).toBe(DoorStateType.START_PASSWORD);
|
||||
});
|
||||
|
||||
it('GetGuestRoomResultEvent with non-bell/password doorMode does NOT change state', () =>
|
||||
{
|
||||
const { result } = renderHook(() => useDoorState());
|
||||
const before = result.current.snapshot.state;
|
||||
act(() =>
|
||||
{
|
||||
mockEventDispatcher.dispatchEvent(makeParserlessEvent(GetGuestRoomResultEvent, {
|
||||
roomForward: true,
|
||||
isGroupMember: false,
|
||||
data: { ownerName: 'other', doorMode: 99 }
|
||||
}));
|
||||
});
|
||||
expect(result.current.snapshot.state).toBe(before);
|
||||
});
|
||||
|
||||
it('reset() returns snapshot to NONE', () =>
|
||||
{
|
||||
const { result } = renderHook(() => useDoorState());
|
||||
act(() =>
|
||||
{
|
||||
mockEventDispatcher.dispatchEvent(makeParserlessEvent(DoorbellMessageEvent, { userName: '' }));
|
||||
});
|
||||
expect(result.current.snapshot.state).toBe(DoorStateType.STATE_WAITING);
|
||||
act(() => result.current.reset());
|
||||
expect(result.current.snapshot.state).toBe(DoorStateType.NONE);
|
||||
expect(result.current.snapshot.roomInfo).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import { DoorbellMessageEvent, FlatAccessDeniedMessageEvent,
|
||||
GenericErrorEvent, GetGuestRoomResultEvent,
|
||||
GetSessionDataManager, RoomDataParser,
|
||||
RoomDoorbellAcceptedEvent } from '@nitrots/nitro-renderer';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useBetween } from 'use-between';
|
||||
import { DoorStateType } from '../../../api';
|
||||
import { useMessageEvent } from '../../events';
|
||||
|
||||
export type DoorStateSnapshot = {
|
||||
roomInfo: RoomDataParser | null;
|
||||
state: number;
|
||||
};
|
||||
|
||||
const INITIAL: DoorStateSnapshot = { roomInfo: null, state: DoorStateType.NONE };
|
||||
|
||||
const useDoorStateStore = () =>
|
||||
{
|
||||
const [ snapshot, setSnapshot ] = useState<DoorStateSnapshot>(INITIAL);
|
||||
|
||||
const handleDoorbell = useCallback((event: DoorbellMessageEvent) =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
if(parser.userName && parser.userName.length > 0) return;
|
||||
setSnapshot(prev => ({ ...prev, state: DoorStateType.STATE_WAITING }));
|
||||
}, []);
|
||||
|
||||
const handleAccepted = useCallback((event: RoomDoorbellAcceptedEvent) =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
if(parser.userName && parser.userName.length > 0) return;
|
||||
setSnapshot(prev => ({ ...prev, state: DoorStateType.STATE_ACCEPTED }));
|
||||
}, []);
|
||||
|
||||
const handleDenied = useCallback((event: FlatAccessDeniedMessageEvent) =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
if(parser.userName && parser.userName.length > 0) return;
|
||||
setSnapshot(prev => ({ ...prev, state: DoorStateType.STATE_NO_ANSWER }));
|
||||
}, []);
|
||||
|
||||
const handleGenericError = useCallback((event: GenericErrorEvent) =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
if(parser.errorCode !== -100002) return;
|
||||
setSnapshot(prev => ({ ...prev, state: DoorStateType.STATE_WRONG_PASSWORD }));
|
||||
}, []);
|
||||
|
||||
const handleGuestRoom = useCallback((event: GetGuestRoomResultEvent) =>
|
||||
{
|
||||
const parser = event.getParser();
|
||||
if(!parser.roomForward) return;
|
||||
if(parser.data.ownerName === GetSessionDataManager().userName) return;
|
||||
if(parser.isGroupMember) return;
|
||||
if(parser.data.doorMode === RoomDataParser.DOORBELL_STATE)
|
||||
{
|
||||
setSnapshot({ roomInfo: parser.data, state: DoorStateType.START_DOORBELL });
|
||||
return;
|
||||
}
|
||||
if(parser.data.doorMode === RoomDataParser.PASSWORD_STATE)
|
||||
{
|
||||
setSnapshot({ roomInfo: parser.data, state: DoorStateType.START_PASSWORD });
|
||||
}
|
||||
}, []);
|
||||
|
||||
useMessageEvent<DoorbellMessageEvent>(DoorbellMessageEvent, handleDoorbell);
|
||||
useMessageEvent<RoomDoorbellAcceptedEvent>(RoomDoorbellAcceptedEvent, handleAccepted);
|
||||
useMessageEvent<FlatAccessDeniedMessageEvent>(FlatAccessDeniedMessageEvent, handleDenied);
|
||||
useMessageEvent<GenericErrorEvent>(GenericErrorEvent, handleGenericError);
|
||||
useMessageEvent<GetGuestRoomResultEvent>(GetGuestRoomResultEvent, handleGuestRoom);
|
||||
|
||||
const reset = useCallback(() => setSnapshot(INITIAL), []);
|
||||
|
||||
return { snapshot, setSnapshot, reset };
|
||||
};
|
||||
|
||||
export const useDoorState = () => useBetween(useDoorStateStore);
|
||||
Reference in New Issue
Block a user