useSoundboard.test.tsx 10.1 Ko
Télécharger le fichier
/* @vitest-environment jsdom */
import { SoundboardPlayDeniedEvent, SoundboardPlayEvent, SoundboardSettingsEvent } from '@octane/renderer';
import { act, renderHook, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { NotificationBubbleType } from '../../api/notification/NotificationBubbleType';
import { useSoundboardFeedStore } from './soundboardFeedStore';
import { useSoundboardState } from './useSoundboard';
const mocks = vi.hoisted(() => ({
handlers: new Map<unknown, (event: any) => void>(),
playSoundboard: vi.fn().mockResolvedValue(true),
stopSoundboard: vi.fn(),
sendMessage: vi.fn(),
showSingleBubble: vi.fn(),
loadGamedata: vi.fn().mockResolvedValue({}),
manifest: { categories: [] as unknown[], byClassname: new Map<string, unknown>() }
}));
vi.mock('@octane/renderer', () => {
class OctaneEvent {
constructor(public type: string) {}
}
class SoundboardPlayComposer {
constructor(public id: number) {}
}
class SoundboardRequestSettingsComposer {}
class SoundboardSetEnabledComposer {
constructor(public mode: number) {}
}
class SoundboardPlayEvent {}
class SoundboardPlayDeniedEvent {}
class SoundboardSettingsEvent {}
return {
GetSessionDataManager: () => ({ getUserDataSnapshot: () => ({ userId: 42 }) }),
GetSoundManager: () => ({ playSoundboard: mocks.playSoundboard, stopSoundboard: mocks.stopSoundboard }),
loadGamedata: mocks.loadGamedata,
OctaneEvent,
SoundboardPlayComposer,
SoundboardRequestSettingsComposer,
SoundboardSetEnabledComposer,
SoundboardPlayEvent,
SoundboardPlayDeniedEvent,
SoundboardSettingsEvent
};
});
vi.mock('../../api', () => ({
DispatchUiEvent: vi.fn(),
GetConfigurationValue: vi.fn().mockReturnValue(''),
LocalizeText: (key: string, names?: string[], values?: string[]) => `${key}:${values?.join(',') ?? ''}`,
NotificationBubbleType: { SOUNDBOARD: 'soundboard' },
SendMessageComposer: mocks.sendMessage,
setSoundboardRoomEnabled: vi.fn()
}));
// The manifest is its own shared source; this suite renders the soundboard
// hook directly, without a SharedHookRegistry host to mount it.
vi.mock('./useSoundboardManifest', () => ({
useSoundboardManifest: () => ({ manifest: mocks.manifest, manifestRef: { current: mocks.manifest }, classnames: [], loaded: true })
}));
vi.mock('../events', () => ({
useMessageEvent: (type: unknown, handler: (event: any) => void) => mocks.handlers.set(type, handler)
}));
vi.mock('../notification', () => ({
useNotificationActions: () => ({ showSingleBubble: mocks.showSingleBubble })
}));
describe('useSoundboardState', () => {
beforeEach(() => {
mocks.handlers.clear();
mocks.playSoundboard.mockClear();
mocks.stopSoundboard.mockClear();
mocks.sendMessage.mockClear();
mocks.showSingleBubble.mockClear();
mocks.loadGamedata.mockReset();
mocks.loadGamedata.mockResolvedValue({});
});
it('blocks a locally known cooldown and shows one personal bubble', () => {
const { result } = renderHook(() => useSoundboardState());
act(() =>
mocks.handlers.get(SoundboardSettingsEvent)?.({
getParser: () => ({ enabled: true, roomMode: 1, cooldownSeconds: 30, sounds: [] })
})
);
act(() =>
mocks.handlers.get(SoundboardPlayDeniedEvent)?.({
getParser: () => ({ reason: 1, remainingSeconds: 12 })
})
);
act(() => result.current.play({ id: 7 } as any));
expect(result.current.isCoolingDown).toBe(true);
expect(mocks.sendMessage).not.toHaveBeenCalled();
expect(mocks.showSingleBubble).toHaveBeenCalledWith(expect.stringContaining('12'), NotificationBubbleType.SOUNDBOARD);
});
it('sends refresh and play composers when allowed', () => {
const { result } = renderHook(() => useSoundboardState());
act(() => result.current.refresh());
act(() => result.current.play({ id: 7 } as any));
expect(mocks.sendMessage).toHaveBeenCalledTimes(2);
expect((mocks.sendMessage.mock.calls[1][0] as any).id).toBe(7);
});
describe('the room mode', () => {
const settings = (roomMode: number) =>
act(() =>
mocks.handlers.get(SoundboardSettingsEvent)?.({
getParser: () => ({ enabled: roomMode > 0, roomMode, cooldownSeconds: 30, sounds: [] })
})
);
it('is on for both everyone and rights, and off for nobody', () => {
const { result } = renderHook(() => useSoundboardState());
settings(2);
expect(result.current.roomMode).toBe(2);
expect(result.current.enabled).toBe(true);
settings(0);
expect(result.current.enabled).toBe(false);
});
it('sends the chosen mode to the server', () => {
const { result } = renderHook(() => useSoundboardState());
act(() => result.current.setRoomMode(2));
expect(result.current.roomMode).toBe(2);
expect((mocks.sendMessage.mock.calls[0][0] as any).mode).toBe(2);
});
it('tells the player when somebody else changes it, but not on the first packet or on their own change', () => {
const { result } = renderHook(() => useSoundboardState());
settings(1);
expect(mocks.showSingleBubble).not.toHaveBeenCalled();
settings(2);
expect(mocks.showSingleBubble).toHaveBeenCalledWith('soundboard.notice.mode.rights:', NotificationBubbleType.SOUNDBOARD);
mocks.showSingleBubble.mockClear();
act(() => result.current.setRoomMode(0));
settings(0);
expect(mocks.showSingleBubble).not.toHaveBeenCalled();
});
it('tells the player a pad is held without locking the panel', () => {
const { result } = renderHook(() => useSoundboardState());
settings(1);
act(() => mocks.handlers.get(SoundboardPlayDeniedEvent)?.({ getParser: () => ({ reason: 5, remainingSeconds: 8 }) }));
expect(mocks.showSingleBubble).toHaveBeenCalledWith('soundboard.error.pad_cooldown:8', NotificationBubbleType.SOUNDBOARD);
expect(result.current.isCoolingDown).toBe(false);
});
it('explains a denial that needs rights', () => {
renderHook(() => useSoundboardState());
act(() => mocks.handlers.get(SoundboardPlayDeniedEvent)?.({ getParser: () => ({ reason: 4, remainingSeconds: 0 }) }));
expect(mocks.showSingleBubble).toHaveBeenCalledWith('soundboard.error.rights_required:', NotificationBubbleType.SOUNDBOARD);
});
});
it('keeps the JSON and JSONC catalog fallback local-only', async () => {
mocks.loadGamedata.mockImplementation(async (url: string) =>
url.includes('layout')
? {}
: {
sounds: [{ id: 11, name: 'Local click', url: 'sounds/soundboard/click.ogg' }]
}
);
const { result } = renderHook(() => useSoundboardState());
act(() =>
mocks.handlers.get(SoundboardSettingsEvent)?.({
getParser: () => ({ enabled: true, roomMode: 1, cooldownSeconds: 30, sounds: [] })
})
);
await waitFor(() => expect(result.current.sounds).toEqual([expect.objectContaining({ id: 11, name: 'Local click', local: true })]));
act(() => result.current.play(result.current.sounds[0]));
expect(mocks.playSoundboard).toHaveBeenCalledWith('sounds/soundboard/click.ogg', { gain: 1 });
expect(mocks.sendMessage).toHaveBeenCalledTimes(0);
});
describe('a play announced by the server', () => {
const announce = (classname: string) =>
act(() =>
mocks.handlers.get(SoundboardPlayEvent)?.({
getParser: () => ({ soundId: 3, classname, url: '', soundName: 'Bell', username: 'tester', actorUserId: 7, actorRoomIndex: 1 })
})
);
beforeEach(() => {
mocks.manifest.byClassname.clear();
window.localStorage.clear();
useSoundboardFeedStore.getState().clear();
});
it('plays a pad with the group and the gain its manifest entry carries', () => {
mocks.manifest.byClassname.set('bell', { classname: 'bell', name: 'Bell', file: 'bell.ogg', group: 'bells', gain: 0.5 });
renderHook(() => useSoundboardState());
announce('bell');
expect(mocks.playSoundboard).toHaveBeenCalledWith(expect.stringContaining('bell.ogg'), { group: 'bells', gain: 0.5 });
});
it('neither plays nor shows the pads of a user the player muted', () => {
const { result } = renderHook(() => useSoundboardState());
act(() => result.current.silenceUser(7));
announce('bell');
expect(mocks.playSoundboard).not.toHaveBeenCalled();
expect(useSoundboardFeedStore.getState().entries).toEqual([]);
expect(result.current.silencedUserIds).toEqual([7]);
});
it('never mutes the player own pads and unmutes everybody on request', () => {
const { result } = renderHook(() => useSoundboardState());
act(() => result.current.silenceUser(42));
act(() =>
mocks.handlers.get(SoundboardPlayEvent)?.({
getParser: () => ({ soundId: 3, classname: 'bell', url: '', soundName: 'Bell', username: 'me', actorUserId: 42, actorRoomIndex: 1 })
})
);
expect(mocks.playSoundboard).toHaveBeenCalledOnce();
act(() => result.current.restoreSilencedUsers());
expect(result.current.silencedUserIds).toEqual([]);
});
it('plays a pad the manifest does not know without options', () => {
renderHook(() => useSoundboardState());
announce('unknown');
expect(mocks.playSoundboard).toHaveBeenCalledWith(expect.any(String), undefined);
});
});
});
