Skip to content

Commit

Permalink
feat: save file system on create update delete actions (#214)
Browse files Browse the repository at this point in the history
* feat: save file system option on create, update, delete files

* feat: test interactions

* fix: cheats and saves not persisting properly automatically

- fixes issues with bulk upload of cheats/saves not persisting properly
  • Loading branch information
thenick775 authored Nov 19, 2024
1 parent 36a06c5 commit 1c1068b
Show file tree
Hide file tree
Showing 26 changed files with 395 additions and 29 deletions.
21 changes: 21 additions & 0 deletions gbajs3/src/components/controls/virtual-controls.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { VirtualControls } from './virtual-controls.tsx';
import { renderWithContext } from '../../../test/render-with-context.tsx';
import { GbaDarkTheme } from '../../context/theme/theme.tsx';
import * as contextHooks from '../../hooks/context.tsx';
import * as addCallbackHooks from '../../hooks/emulator/use-add-callbacks.tsx';
import * as quickReloadHooks from '../../hooks/emulator/use-quick-reload.tsx';
import { UploadSaveToServerModal } from '../modals/upload-save-to-server.tsx';

Expand Down Expand Up @@ -270,9 +271,13 @@ describe('<VirtualControls />', () => {

it('creates save state', async () => {
const createSaveStateSpy: (slot: number) => boolean = vi.fn(() => true);
const syncActionIfEnabledSpy = vi.fn();
const { useEmulatorContext: original } = await vi.importActual<
typeof contextHooks
>('../../hooks/context.tsx');
const { useAddCallbacks: originalCallbacks } = await vi.importActual<
typeof addCallbackHooks
>('../../hooks/emulator/use-add-callbacks.tsx');

vi.spyOn(contextHooks, 'useEmulatorContext').mockImplementation(() => ({
...original(),
Expand All @@ -281,6 +286,11 @@ describe('<VirtualControls />', () => {
} as GBAEmulator
}));

vi.spyOn(addCallbackHooks, 'useAddCallbacks').mockImplementation(() => ({
...originalCallbacks(),
syncActionIfEnabled: syncActionIfEnabledSpy
}));

const toastSuccessSpy = vi.spyOn(toast.default, 'success');

localStorage.setItem(saveStateSlotLocalStorageKey, '2');
Expand All @@ -291,16 +301,21 @@ describe('<VirtualControls />', () => {

expect(createSaveStateSpy).toHaveBeenCalledOnce();
expect(createSaveStateSpy).toHaveBeenCalledWith(2);
expect(syncActionIfEnabledSpy).toHaveBeenCalledOnce();
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 syncActionIfEnabledSpy = vi.fn();
const { useEmulatorContext: original } = await vi.importActual<
typeof contextHooks
>('../../hooks/context.tsx');
const { useAddCallbacks: originalCallbacks } = await vi.importActual<
typeof addCallbackHooks
>('../../hooks/emulator/use-add-callbacks.tsx');

vi.spyOn(contextHooks, 'useEmulatorContext').mockImplementation(() => ({
...original(),
Expand All @@ -309,6 +324,11 @@ describe('<VirtualControls />', () => {
} as GBAEmulator
}));

vi.spyOn(addCallbackHooks, 'useAddCallbacks').mockImplementation(() => ({
...originalCallbacks(),
syncActionIfEnabled: syncActionIfEnabledSpy
}));

const toastErrorSpy = vi.spyOn(toast.default, 'error');

localStorage.setItem(saveStateSlotLocalStorageKey, '2');
Expand All @@ -319,6 +339,7 @@ describe('<VirtualControls />', () => {

expect(createSaveStateSpy).toHaveBeenCalledOnce();
expect(createSaveStateSpy).toHaveBeenCalledWith(2);
expect(syncActionIfEnabledSpy).not.toHaveBeenCalled();
expect(toastErrorSpy).toHaveBeenCalledWith('Failed to save slot: 2', {
id: expect.anything()
});
Expand Down
4 changes: 4 additions & 0 deletions gbajs3/src/components/controls/virtual-controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
useModalContext,
useRunningContext
} from '../../hooks/context.tsx';
import { useAddCallbacks } from '../../hooks/emulator/use-add-callbacks.tsx';
import { useQuickReload } from '../../hooks/emulator/use-quick-reload.tsx';
import { UploadSaveToServerModal } from '../modals/upload-save-to-server.tsx';

Expand Down Expand Up @@ -62,6 +63,7 @@ export const VirtualControls = () => {
const { layouts } = useLayoutContext();
const virtualControlToastId = useId();
const quickReload = useQuickReload();
const { syncActionIfEnabled } = useAddCallbacks();
const [currentSaveStateSlot] = useLocalStorage(
saveStateSlotLocalStorageKey,
0
Expand Down Expand Up @@ -379,6 +381,8 @@ export const VirtualControls = () => {
onClick: () => {
const wasSuccessful = emulator?.createSaveState(currentSaveStateSlot);

if (wasSuccessful) syncActionIfEnabled({ withToast: false });

toastOnCondition(
!!wasSuccessful,
`Saved slot: ${currentSaveStateSlot}`,
Expand Down
21 changes: 21 additions & 0 deletions gbajs3/src/components/modals/cheats.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { describe, expect, it, vi } from 'vitest';
import { CheatsModal } from './cheats.tsx';
import { renderWithContext } from '../../../test/render-with-context.tsx';
import * as contextHooks from '../../hooks/context.tsx';
import * as addCallbackHooks from '../../hooks/emulator/use-add-callbacks.tsx';
import { productTourLocalStorageKey } from '../product-tour/consts.tsx';

import type {
Expand Down Expand Up @@ -129,7 +130,11 @@ describe('<CheatsModal />', () => {
const { useEmulatorContext: original } = await vi.importActual<
typeof contextHooks
>('../../hooks/context.tsx');
const { useAddCallbacks: originalCallbacks } = await vi.importActual<
typeof addCallbackHooks
>('../../hooks/emulator/use-add-callbacks.tsx');
const autoLoadCheatsSpy: () => boolean = vi.fn(() => true);
const syncActionIfEnabledSpy = vi.fn();

vi.spyOn(contextHooks, 'useEmulatorContext').mockImplementation(() => ({
...original(),
Expand All @@ -143,6 +148,11 @@ describe('<CheatsModal />', () => {
} as GBAEmulator
}));

vi.spyOn(addCallbackHooks, 'useAddCallbacks').mockImplementation(() => ({
...originalCallbacks(),
syncActionIfEnabled: syncActionIfEnabledSpy
}));

renderWithContext(<CheatsModal />);

await userEvent.type(screen.getByLabelText('Name'), '_edited');
Expand All @@ -159,6 +169,7 @@ describe('<CheatsModal />', () => {
enable: true
}
]);
expect(syncActionIfEnabledSpy).toHaveBeenCalledOnce();
expect(uploadCheatsSpy).toHaveBeenCalledOnce();
expect(autoLoadCheatsSpy).toHaveBeenCalledOnce();
});
Expand All @@ -174,7 +185,11 @@ describe('<CheatsModal />', () => {
const { useEmulatorContext: original } = await vi.importActual<
typeof contextHooks
>('../../hooks/context.tsx');
const { useAddCallbacks: originalCallbacks } = await vi.importActual<
typeof addCallbackHooks
>('../../hooks/emulator/use-add-callbacks.tsx');
const autoLoadCheatsSpy: () => boolean = vi.fn(() => true);
const syncActionIfEnabledSpy = vi.fn();

vi.spyOn(contextHooks, 'useEmulatorContext').mockImplementation(() => ({
...original(),
Expand All @@ -188,6 +203,11 @@ describe('<CheatsModal />', () => {
} as GBAEmulator
}));

vi.spyOn(addCallbackHooks, 'useAddCallbacks').mockImplementation(() => ({
...originalCallbacks(),
syncActionIfEnabled: syncActionIfEnabledSpy
}));

renderWithContext(<CheatsModal />);

await userEvent.click(screen.getByRole('button', { name: 'Raw' }));
Expand All @@ -200,6 +220,7 @@ describe('<CheatsModal />', () => {

await userEvent.click(screen.getByRole('button', { name: 'Submit' }));

expect(syncActionIfEnabledSpy).toHaveBeenCalledOnce();
expect(uploadCheatsSpy).toHaveBeenCalledOnce();
expect(autoLoadCheatsSpy).toHaveBeenCalledOnce();
expect(getCurrentCheatsFileNameSpy).toHaveBeenCalledOnce();
Expand Down
3 changes: 3 additions & 0 deletions gbajs3/src/components/modals/cheats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ModalBody } from './modal-body.tsx';
import { ModalFooter } from './modal-footer.tsx';
import { ModalHeader } from './modal-header.tsx';
import { useEmulatorContext, useModalContext } from '../../hooks/context.tsx';
import { useAddCallbacks } from '../../hooks/emulator/use-add-callbacks.tsx';
import {
EmbeddedProductTour,
type TourSteps
Expand Down Expand Up @@ -85,6 +86,7 @@ export const CheatsModal = () => {
const { setIsModalOpen } = useModalContext();
const { emulator } = useEmulatorContext();
const [viewRawCheats, setViewRawCheats] = useState(false);
const { syncActionIfEnabled } = useAddCallbacks();
const baseId = useId();
const defaultCheat = { desc: '', code: '', enable: false };

Expand Down Expand Up @@ -197,6 +199,7 @@ export const CheatsModal = () => {

if (cheatsFile)
emulator?.uploadCheats(cheatsFile, () => {
syncActionIfEnabled();
emulator.autoLoadCheats();
refreshForm();
});
Expand Down
11 changes: 11 additions & 0 deletions gbajs3/src/components/modals/file-system.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { describe, expect, it, vi } from 'vitest';
import { FileSystemModal } from './file-system.tsx';
import { renderWithContext } from '../../../test/render-with-context.tsx';
import * as contextHooks from '../../hooks/context.tsx';
import * as addCallbackHooks from '../../hooks/emulator/use-add-callbacks.tsx';
import { productTourLocalStorageKey } from '../product-tour/consts.tsx';

import type {
Expand Down Expand Up @@ -60,10 +61,14 @@ describe('<FileSystemModal />', () => {

it('deletes file from the tree', async () => {
const deleteFileSpy: (p: string) => void = vi.fn();
const syncActionIfEnabledSpy = vi.fn();
const listAllFilesSpy = vi.fn(() => defaultFSData);
const { useEmulatorContext: original } = await vi.importActual<
typeof contextHooks
>('../../hooks/context.tsx');
const { useAddCallbacks: originalCallbacks } = await vi.importActual<
typeof addCallbackHooks
>('../../hooks/emulator/use-add-callbacks.tsx');

vi.spyOn(contextHooks, 'useEmulatorContext').mockImplementation(() => {
return {
Expand All @@ -75,6 +80,11 @@ describe('<FileSystemModal />', () => {
};
});

vi.spyOn(addCallbackHooks, 'useAddCallbacks').mockImplementation(() => ({
...originalCallbacks(),
syncActionIfEnabled: syncActionIfEnabledSpy
}));

renderWithContext(<FileSystemModal />);

listAllFilesSpy.mockClear(); // clear calls from initial render
Expand All @@ -85,6 +95,7 @@ describe('<FileSystemModal />', () => {
expect(deleteFileSpy).toHaveBeenCalledOnce();
expect(deleteFileSpy).toHaveBeenCalledWith('/data/games/rom1.gba');
expect(listAllFilesSpy).toHaveBeenCalledOnce();
expect(syncActionIfEnabledSpy).toHaveBeenCalledOnce();
});

it('downloads file from the tree', async () => {
Expand Down
5 changes: 4 additions & 1 deletion gbajs3/src/components/modals/file-system.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ModalBody } from './modal-body.tsx';
import { ModalFooter } from './modal-footer.tsx';
import { ModalHeader } from './modal-header.tsx';
import { useEmulatorContext, useModalContext } from '../../hooks/context.tsx';
import { useAddCallbacks } from '../../hooks/emulator/use-add-callbacks.tsx';
import {
EmbeddedProductTour,
type TourSteps
Expand All @@ -25,15 +26,17 @@ const FlexModalBody = styled(ModalBody)`
export const FileSystemModal = () => {
const { setIsModalOpen } = useModalContext();
const { emulator } = useEmulatorContext();
const { syncActionIfEnabled } = useAddCallbacks();
const [allFiles, setAllFiles] = useState<FileNode | undefined>();
const baseId = useId();

const deleteFile = useCallback(
(path: string) => {
emulator?.deleteFile(path);
setAllFiles(emulator?.listAllFiles());
syncActionIfEnabled();
},
[emulator]
[emulator, syncActionIfEnabled]
);

const downloadFile = (path: string) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ describe('<FileSystemOptionsForm />', () => {
// form and form fields
expect(screen.getByLabelText('File System Options Form')).toBeVisible();
expect(screen.getByText('Save file system on in-game save')).toBeVisible();
expect(
screen.getByText('Save file system on creates / updates / deletes')
).toBeVisible();
});

it('saves file system options', async () => {
Expand All @@ -37,7 +40,8 @@ describe('<FileSystemOptionsForm />', () => {

vi.spyOn(addCallbacksHooks, 'useAddCallbacks').mockImplementation(() => ({
addCallbacks: vi.fn(),
addCallbacksAndSaveSettings: addCallbacksAndSaveSettingsSpy
addCallbacksAndSaveSettings: addCallbacksAndSaveSettingsSpy,
syncActionIfEnabled: vi.fn()
}));

renderWithContext(<FileSystemOptionsForm id="testId" />);
Expand All @@ -50,6 +54,9 @@ describe('<FileSystemOptionsForm />', () => {
await userEvent.click(
screen.getByLabelText('Save file system on in-game save')
);
await userEvent.click(
screen.getByLabelText('Save file system on creates / updates / deletes')
);
await userEvent.click(screen.getByLabelText('Enable Notifications'));
// submit form
await userEvent.click(submitButton);
Expand All @@ -58,7 +65,8 @@ describe('<FileSystemOptionsForm />', () => {
expect(addCallbacksAndSaveSettingsSpy).toHaveBeenCalledWith(
{
saveFileSystemOnInGameSave: true,
notificationsEnabled: true
notificationsEnabled: true,
saveFileSystemOnCreateUpdateDelete: true
},
expect.anything()
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ export const FileSystemOptionsForm = ({ id }: FileSystemOptionsFormProps) => {
watcher={watch('saveFileSystemOnInGameSave')}
{...register('saveFileSystemOnInGameSave')}
/>
<ManagedCheckbox
label="Save file system on creates / updates / deletes"
watcher={watch('saveFileSystemOnCreateUpdateDelete')}
{...register('saveFileSystemOnCreateUpdateDelete')}
/>
<ManagedSwitch
label="Enable Notifications"
watcher={watch('notificationsEnabled')}
Expand Down
11 changes: 11 additions & 0 deletions gbajs3/src/components/modals/load-rom.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { describe, expect, it, vi } from 'vitest';
import { LoadRomModal } from './load-rom.tsx';
import { renderWithContext } from '../../../test/render-with-context.tsx';
import * as contextHooks from '../../hooks/context.tsx';
import * as addCallbackHooks from '../../hooks/emulator/use-add-callbacks.tsx';
import * as runGameHooks from '../../hooks/emulator/use-run-game.tsx';
import * as listRomHooks from '../../hooks/use-list-roms.tsx';
import * as loadRomHooks from '../../hooks/use-load-rom.tsx';
Expand All @@ -25,9 +26,13 @@ describe('<LoadRomModal />', () => {
(_file, cb) => cb && cb()
);
const runGameSpy = vi.fn();
const syncActionIfEnabledSpy = vi.fn();
const { useEmulatorContext: originalEmulator } = await vi.importActual<
typeof contextHooks
>('../../hooks/context.tsx');
const { useAddCallbacks: originalCallbacks } = await vi.importActual<
typeof addCallbackHooks
>('../../hooks/emulator/use-add-callbacks.tsx');

vi.spyOn(contextHooks, 'useEmulatorContext').mockImplementation(() => ({
...originalEmulator(),
Expand All @@ -39,6 +44,11 @@ describe('<LoadRomModal />', () => {
} as GBAEmulator
}));

vi.spyOn(addCallbackHooks, 'useAddCallbacks').mockImplementation(() => ({
...originalCallbacks(),
syncActionIfEnabled: syncActionIfEnabledSpy
}));

vi.spyOn(runGameHooks, 'useRunGame').mockReturnValue(runGameSpy);

renderWithContext(<LoadRomModal />);
Expand All @@ -54,6 +64,7 @@ describe('<LoadRomModal />', () => {
await waitForElementToBeRemoved(screen.queryByText(/Loading rom:/));

expect(uploadRomSpy).toHaveBeenCalledOnce();
expect(syncActionIfEnabledSpy).toHaveBeenCalledOnce();
expect(runGameSpy).toHaveBeenCalledOnce();
expect(runGameSpy).toHaveBeenCalledWith('/games/rom1.gba');
});
Expand Down
5 changes: 4 additions & 1 deletion gbajs3/src/components/modals/load-rom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ModalBody } from './modal-body.tsx';
import { ModalFooter } from './modal-footer.tsx';
import { ModalHeader } from './modal-header.tsx';
import { useEmulatorContext, useModalContext } from '../../hooks/context.tsx';
import { useAddCallbacks } from '../../hooks/emulator/use-add-callbacks.tsx';
import { useRunGame } from '../../hooks/emulator/use-run-game.tsx';
import { useListRoms } from '../../hooks/use-list-roms.tsx';
import { useLoadRom } from '../../hooks/use-load-rom.tsx';
Expand Down Expand Up @@ -94,19 +95,21 @@ export const LoadRomModal = () => {
const [currentRomLoading, setCurrentRomLoading] = useState<string | null>(
null
);
const { syncActionIfEnabled } = useAddCallbacks();

const shouldUploadRom = !romLoading && !!romFile && !!currentRomLoading;

useEffect(() => {
if (shouldUploadRom) {
const runCallback = () => {
syncActionIfEnabled();
runGame(emulator?.filePaths().gamePath + '/' + romFile.name);
};

emulator?.uploadRom(romFile, runCallback);
setCurrentRomLoading(null);
}
}, [emulator, shouldUploadRom, romFile, runGame]);
}, [emulator, shouldUploadRom, romFile, runGame, syncActionIfEnabled]);

const tourSteps: TourSteps = [
{
Expand Down
Loading

0 comments on commit 1c1068b

Please sign in to comment.