From c3a76b643df2bb8f947b1ef5646eec3cb4cd104e Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Mon, 18 May 2026 21:41:43 +0200 Subject: [PATCH] refactor(hooks/rooms): collapse usePetPackageWidget 5 useStates into useReducer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hook tracked five related useState fields driving the pet-package naming dialog (isVisible / objectId / objectType / petName / errorResult). They transitioned in lockstep on the two RoomSessionPetPackageEvent types and the inline change handler — textbook state-machine territory. Collapse into a single useReducer with four explicit transitions: - 'open' → REQUESTED event lands; flips visible, records target - 'close' → REQUESTED-result success OR user dismiss; resets to INITIAL - 'set-name' → input change; updates petName AND clears any error (the previous code had this side effect inlined in onChangePetName as `if(errorResult.length) setErrorResult('')`, now it's part of the reducer contract) - 'set-error' → REQUESTED-result with validation failure; sets the label Plus extract `getPetPackageNameError(code)` to a top-level exported pure function (was an inline closure named getErrorResultForCode). The mapping is server-protocol contract, not UI state — moving it out of the hook means it's testable, reusable, and won't be recreated on every render. Public API of usePetPackageWidget is unchanged — the one consumer (PetPackageWidgetView) reads the same destructured fields. Verified via grep. Tests: 4 new cases on getPetPackageNameError covering code 0 / 1-4 / falsy / unknown-fallback. Suite: 207/207 (was 203/203). --- .../rooms/widgets/usePetPackageWidget.test.ts | 36 +++++ .../rooms/widgets/usePetPackageWidget.ts | 129 +++++++++++------- 2 files changed, 116 insertions(+), 49 deletions(-) create mode 100644 src/hooks/rooms/widgets/usePetPackageWidget.test.ts diff --git a/src/hooks/rooms/widgets/usePetPackageWidget.test.ts b/src/hooks/rooms/widgets/usePetPackageWidget.test.ts new file mode 100644 index 0000000..de408c4 --- /dev/null +++ b/src/hooks/rooms/widgets/usePetPackageWidget.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it, vi } from 'vitest'; + +vi.mock('../../../api', () => ({ + LocalizeText: (key: string) => key, + SendMessageComposer: vi.fn() +})); + +import { getPetPackageNameError } from './usePetPackageWidget'; + +describe('getPetPackageNameError', () => +{ + it('returns empty string for code 0 (no error)', () => + { + expect(getPetPackageNameError(0)).toBe(''); + }); + + it('returns empty string for falsy values (null/undefined coerced)', () => + { + expect(getPetPackageNameError(null as never)).toBe(''); + expect(getPetPackageNameError(undefined as never)).toBe(''); + }); + + it('maps the four documented error codes to their localization keys', () => + { + expect(getPetPackageNameError(1)).toBe('catalog.alert.petname.long'); + expect(getPetPackageNameError(2)).toBe('catalog.alert.petname.short'); + expect(getPetPackageNameError(3)).toBe('catalog.alert.petname.chars'); + expect(getPetPackageNameError(4)).toBe('catalog.alert.petname.bobba'); + }); + + it('falls back to the bobba label for unknown error codes (defensive default)', () => + { + expect(getPetPackageNameError(99)).toBe('catalog.alert.petname.bobba'); + expect(getPetPackageNameError(-1)).toBe('catalog.alert.petname.bobba'); + }); +}); diff --git a/src/hooks/rooms/widgets/usePetPackageWidget.ts b/src/hooks/rooms/widgets/usePetPackageWidget.ts index 0fb933b..e2c175a 100644 --- a/src/hooks/rooms/widgets/usePetPackageWidget.ts +++ b/src/hooks/rooms/widgets/usePetPackageWidget.ts @@ -1,53 +1,74 @@ import { GetRoomEngine, OpenPetPackageMessageComposer, RoomObjectCategory, RoomSessionPetPackageEvent } from '@nitrots/nitro-renderer'; -import { useState } from 'react'; +import { useReducer } from 'react'; import { LocalizeText, SendMessageComposer } from '../../../api'; import { useNitroEvent } from '../../events'; +interface PetPackageState +{ + isVisible: boolean; + objectId: number; + objectType: string; + petName: string; + errorResult: string; +} + +type PetPackageAction = + | { type: 'open'; objectId: number; objectType: string } + | { type: 'close' } + | { type: 'set-name'; petName: string } + | { type: 'set-error'; errorResult: string }; + +const INITIAL_STATE: PetPackageState = { + isVisible: false, + objectId: -1, + objectType: '', + petName: '', + errorResult: '' +}; + +const petPackageReducer = (state: PetPackageState, action: PetPackageAction): PetPackageState => +{ + switch(action.type) + { + case 'open': + return { ...INITIAL_STATE, isVisible: true, objectId: action.objectId, objectType: action.objectType }; + case 'close': + return INITIAL_STATE; + case 'set-name': + // Typing into the input always clears any previous error label. + return { ...state, petName: action.petName, errorResult: '' }; + case 'set-error': + return { ...state, errorResult: action.errorResult }; + } +}; + +/** + * Maps the pet-package name-validation error code returned by the + * server to a localized error label. Exported for testability — the + * mapping is server-protocol contract, not UI state. + */ +export const getPetPackageNameError = (errorCode: number): string => +{ + if(!errorCode) return ''; + + switch(errorCode) + { + case 1: return LocalizeText('catalog.alert.petname.long'); + case 2: return LocalizeText('catalog.alert.petname.short'); + case 3: return LocalizeText('catalog.alert.petname.chars'); + case 4: + default: + return LocalizeText('catalog.alert.petname.bobba'); + } +}; + const usePetPackageWidgetState = () => { - const [ isVisible, setIsVisible ] = useState(false); - const [ objectId, setObjectId ] = useState(-1); - const [ objectType, setObjectType ] = useState(''); - const [ petName, setPetName ] = useState(''); - const [ errorResult, setErrorResult ] = useState(''); + const [ state, dispatch ] = useReducer(petPackageReducer, INITIAL_STATE); - const onClose = () => - { - setErrorResult(''); - setPetName(''); - setObjectType(''); - setObjectId(-1); - setIsVisible(false); - }; - - const onConfirm = () => - { - SendMessageComposer(new OpenPetPackageMessageComposer(objectId, petName)); - }; - - const onChangePetName = (petName: string) => - { - setPetName(petName); - if(errorResult.length > 0) setErrorResult(''); - }; - - const getErrorResultForCode = (errorCode: number) => - { - if(!errorCode || errorCode === 0) return; - - switch(errorCode) - { - case 1: - return LocalizeText('catalog.alert.petname.long'); - case 2: - return LocalizeText('catalog.alert.petname.short'); - case 3: - return LocalizeText('catalog.alert.petname.chars'); - case 4: - default: - return LocalizeText('catalog.alert.petname.bobba'); - } - }; + const onClose = () => dispatch({ type: 'close' }); + const onConfirm = () => SendMessageComposer(new OpenPetPackageMessageComposer(state.objectId, state.petName)); + const onChangePetName = (petName: string) => dispatch({ type: 'set-name', petName }); useNitroEvent(RoomSessionPetPackageEvent.RSOPPE_OPEN_PET_PACKAGE_REQUESTED, event => { @@ -55,21 +76,31 @@ const usePetPackageWidgetState = () => const roomObject = GetRoomEngine().getRoomObject(event.session.roomId, event.objectId, RoomObjectCategory.FLOOR); - setObjectId(event.objectId); - setObjectType(roomObject.type); - setIsVisible(true); + dispatch({ type: 'open', objectId: event.objectId, objectType: roomObject.type }); }); useNitroEvent(RoomSessionPetPackageEvent.RSOPPE_OPEN_PET_PACKAGE_RESULT, event => { if(!event) return; - if(event.nameValidationStatus === 0) onClose(); + if(event.nameValidationStatus === 0) + { + dispatch({ type: 'close' }); + return; + } - if(event.nameValidationStatus !== 0) setErrorResult(getErrorResultForCode(event.nameValidationStatus)); + dispatch({ type: 'set-error', errorResult: getPetPackageNameError(event.nameValidationStatus) }); }); - return { isVisible, errorResult, petName, objectType, onChangePetName, onConfirm, onClose }; + return { + isVisible: state.isVisible, + errorResult: state.errorResult, + petName: state.petName, + objectType: state.objectType, + onChangePetName, + onConfirm, + onClose + }; }; export const usePetPackageWidget = usePetPackageWidgetState;