diff --git a/gbajs3/src/components/controls/o-pad.tsx b/gbajs3/src/components/controls/o-pad.tsx index a6b80c64..f0299fe2 100644 --- a/gbajs3/src/components/controls/o-pad.tsx +++ b/gbajs3/src/components/controls/o-pad.tsx @@ -243,6 +243,7 @@ export const OPad = ({ initialPosition }: OPadProps) => { } > ', () => { + beforeEach(async () => { + const { useLayoutContext: original } = await vi.importActual< + typeof contextHooks + >('../../hooks/context.tsx'); + + vi.spyOn(contextHooks, 'useLayoutContext').mockImplementation(() => ({ + ...original(), + layouts: { + ...original().layouts, + controlPanel: { initialBounds: { left: 0, bottom: 0 } as DOMRect } + } + })); + }); + + it('renders dpad and default virtual controls on mobile', () => { + renderWithContext(); + + expect(screen.getByLabelText('A Button')).toBeVisible(); + expect(screen.getByLabelText('B Button')).toBeVisible(); + expect(screen.getByLabelText('Start Button')).toBeVisible(); + expect(screen.getByLabelText('Select Button')).toBeVisible(); + expect(screen.getByLabelText('L Button')).toBeVisible(); + expect(screen.getByLabelText('R Button')).toBeVisible(); + expect(screen.getByLabelText('O-Pad')).toBeVisible(); + }); + + it('renders no virtual controls by default on desktop', () => { + vi.spyOn(window, 'matchMedia').mockImplementation((query) => ({ + matches: query === GbaDarkTheme.isLargerThanPhone, + media: '', + addListener: () => {}, + removeListener: () => {}, + onchange: () => {}, + addEventListener: () => {}, + removeEventListener: () => {}, + dispatchEvent: () => true + })); + + renderWithContext(); + + expect(screen.queryByLabelText('A Button')).not.toBeInTheDocument(); + expect(screen.queryByLabelText('B Button')).not.toBeInTheDocument(); + expect(screen.queryByLabelText('Start Button')).not.toBeInTheDocument(); + expect(screen.queryByLabelText('Select Button')).not.toBeInTheDocument(); + expect(screen.queryByLabelText('L Button')).not.toBeInTheDocument(); + expect(screen.queryByLabelText('R Button')).not.toBeInTheDocument(); + expect(screen.queryByLabelText('O-Pad')).not.toBeInTheDocument(); + }); + + describe('Additional Controls', () => { + beforeEach(() => { + localStorage.setItem( + virtualControlsLocalStorageKey, + '{"DPadAndButtons":true,"NotificationsEnabled":true,"SaveState":true,"LoadState":true,"QuickReload":true,"SendSaveToServer":true}' + ); + }); + + it('quick reloads game', async () => { + const quickReloadSpy: () => void = vi.fn(); + const { useEmulatorContext: original } = await vi.importActual< + typeof contextHooks + >('../../hooks/context.tsx'); + + vi.spyOn(contextHooks, 'useEmulatorContext').mockImplementation(() => ({ + ...original(), + emulator: { + quickReload: quickReloadSpy, + getCurrentGameName: () => 'some_rom.gba' + } as GBAEmulator + })); + + const toastErrorSpy = vi.spyOn(toast.default, 'error'); + + renderWithContext(); + + await userEvent.click(screen.getByLabelText('Quickreload Button')); + + expect(quickReloadSpy).toHaveBeenCalledOnce(); + expect(toastErrorSpy).not.toHaveBeenCalled(); + }); + + it('quick reload renders error toast', async () => { + const quickReloadSpy: () => void = vi.fn(); + const { useEmulatorContext: original } = await vi.importActual< + typeof contextHooks + >('../../hooks/context.tsx'); + + vi.spyOn(contextHooks, 'useEmulatorContext').mockImplementation(() => ({ + ...original(), + emulator: { + quickReload: quickReloadSpy, + getCurrentGameName: () => undefined + } as GBAEmulator + })); + + const toastErrorSpy = vi.spyOn(toast.default, 'error'); + + renderWithContext(); + + await userEvent.click(screen.getByLabelText('Quickreload Button')); + + expect(quickReloadSpy).toHaveBeenCalledOnce(); + expect(toastErrorSpy).toHaveBeenCalledWith( + 'Load a game to quick reload', + { id: expect.anything() } + ); + }); + + it('upload save opens modal if authenticated and running a game', async () => { + const setIsModalOpenSpy = vi.fn(); + const setModalContextSpy = vi.fn(); + const { + useAuthContext: originalAuth, + useEmulatorContext: originalEmulator, + useModalContext: originalContext + } = await vi.importActual('../../hooks/context.tsx'); + + vi.spyOn(contextHooks, 'useAuthContext').mockImplementation(() => ({ + ...originalAuth(), + isAuthenticated: () => true + })); + + vi.spyOn(contextHooks, 'useEmulatorContext').mockImplementation(() => ({ + ...originalEmulator(), + isEmulatorRunning: true + })); + + vi.spyOn(contextHooks, 'useModalContext').mockImplementation(() => ({ + ...originalContext(), + setModalContent: setModalContextSpy, + setIsModalOpen: setIsModalOpenSpy + })); + + renderWithContext(); + + await userEvent.click(screen.getByLabelText('Uploadsave Button')); + + expect(setModalContextSpy).toHaveBeenCalledWith( + + ); + expect(setIsModalOpenSpy).toHaveBeenCalledWith(true); + }); + + it('upload save renders error toast', async () => { + const setIsModalOpenSpy = vi.fn(); + const setModalContextSpy = vi.fn(); + const toastErrorSpy = vi.spyOn(toast.default, 'error'); + + const { useModalContext: original } = await vi.importActual< + typeof contextHooks + >('../../hooks/context.tsx'); + + vi.spyOn(contextHooks, 'useModalContext').mockImplementation(() => ({ + ...original(), + setModalContent: setModalContextSpy, + setIsModalOpen: setIsModalOpenSpy + })); + + renderWithContext(); + + await userEvent.click(screen.getByLabelText('Uploadsave Button')); + + expect(toastErrorSpy).toHaveBeenCalledWith( + 'Please log in and load a game', + { id: expect.anything() } + ); + expect(setModalContextSpy).not.toHaveBeenCalled(); + expect(setIsModalOpenSpy).not.toHaveBeenCalled(); + }); + + it('loads save state', async () => { + const loadSaveStateSpy: (slot: number) => boolean = vi.fn((_) => true); + const { useEmulatorContext: original } = await vi.importActual< + typeof contextHooks + >('../../hooks/context.tsx'); + + vi.spyOn(contextHooks, 'useEmulatorContext').mockImplementation(() => ({ + ...original(), + emulator: { + loadSaveState: loadSaveStateSpy + } as GBAEmulator + })); + + const toastSuccessSpy = vi.spyOn(toast.default, 'success'); + + localStorage.setItem(saveStateSlotLocalStorageKey, '2'); + + renderWithContext(); + + await userEvent.click(screen.getByLabelText('Loadstate Button')); + + expect(loadSaveStateSpy).toHaveBeenCalledOnce(); + expect(loadSaveStateSpy).toHaveBeenCalledWith(2); + expect(toastSuccessSpy).toHaveBeenCalledWith('Loaded slot: 2', { + id: expect.anything() + }); + }); + + it('load save state renders error toast', async () => { + const loadSaveStateSpy: (slot: number) => boolean = vi.fn((_) => false); + const { useEmulatorContext: original } = await vi.importActual< + typeof contextHooks + >('../../hooks/context.tsx'); + + vi.spyOn(contextHooks, 'useEmulatorContext').mockImplementation(() => ({ + ...original(), + emulator: { + loadSaveState: loadSaveStateSpy + } as GBAEmulator + })); + + const toastErrorSpy = vi.spyOn(toast.default, 'error'); + + localStorage.setItem(saveStateSlotLocalStorageKey, '2'); + + renderWithContext(); + + await userEvent.click(screen.getByLabelText('Loadstate Button')); + + expect(loadSaveStateSpy).toHaveBeenCalledOnce(); + expect(loadSaveStateSpy).toHaveBeenCalledWith(2); + expect(toastErrorSpy).toHaveBeenCalledWith('Failed to load slot: 2', { + id: expect.anything() + }); + }); + + it('creates save state', async () => { + const createSaveStateSpy: (slot: number) => boolean = vi.fn((_) => true); + const { useEmulatorContext: original } = await vi.importActual< + typeof contextHooks + >('../../hooks/context.tsx'); + + vi.spyOn(contextHooks, 'useEmulatorContext').mockImplementation(() => ({ + ...original(), + emulator: { + createSaveState: createSaveStateSpy + } as GBAEmulator + })); + + const toastSuccessSpy = vi.spyOn(toast.default, 'success'); + + localStorage.setItem(saveStateSlotLocalStorageKey, '2'); + + renderWithContext(); + + await userEvent.click(screen.getByLabelText('Savestate Button')); + + expect(createSaveStateSpy).toHaveBeenCalledOnce(); + expect(createSaveStateSpy).toHaveBeenCalledWith(2); + expect(toastSuccessSpy).toHaveBeenCalledWith('Saved slot: 2', { + id: expect.anything() + }); + }); + + it('create save state renders error toast', async () => { + const createSaveStateSpy: (slot: number) => boolean = vi.fn((_) => false); + const { useEmulatorContext: original } = await vi.importActual< + typeof contextHooks + >('../../hooks/context.tsx'); + + vi.spyOn(contextHooks, 'useEmulatorContext').mockImplementation(() => ({ + ...original(), + emulator: { + createSaveState: createSaveStateSpy + } as GBAEmulator + })); + + const toastErrorSpy = vi.spyOn(toast.default, 'error'); + + localStorage.setItem(saveStateSlotLocalStorageKey, '2'); + + renderWithContext(); + + await userEvent.click(screen.getByLabelText('Savestate Button')); + + expect(createSaveStateSpy).toHaveBeenCalledOnce(); + expect(createSaveStateSpy).toHaveBeenCalledWith(2); + expect(toastErrorSpy).toHaveBeenCalledWith('Failed to save slot: 2', { + id: expect.anything() + }); + }); + }); +});