Skip to content

Commit

Permalink
feat: keybindings form spec
Browse files Browse the repository at this point in the history
  • Loading branch information
Nick VanCise authored and Nick VanCise committed Jan 12, 2024
1 parent bf3884a commit 26fb79a
Show file tree
Hide file tree
Showing 3 changed files with 184 additions and 2 deletions.
175 changes: 175 additions & 0 deletions gbajs3/src/components/modals/controls/key-bindings-form.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { afterEach, describe, expect, it, vi } from 'vitest';

import { KeyBindingsForm } from './key-bindings-form.tsx';
import { renderWithContext } from '../../../../test/render-with-context.tsx';
import { emulatorKeyBindingsLocalStorageKey } from '../../../context/emulator/consts.tsx';
import * as contextHooks from '../../../hooks/context.tsx';

import type {
GBAEmulator,
KeyBinding
} from '../../../emulator/mgba/mgba-emulator.tsx';

describe('<KeyBindingsForm />', () => {
afterEach(() => {
localStorage.clear();
});

it('renders if emulator is null', () => {
renderWithContext(<KeyBindingsForm id="testId" />);

expect(screen.getByRole('form', { name: 'Key Bindings Form' }));
});

it('renders form with provided id', () => {
renderWithContext(<KeyBindingsForm id="testId" />);

expect(
screen.getByRole('form', { name: 'Key Bindings Form' })
).toHaveAttribute('id', 'testId');
});

it('renders with default keybindings', async () => {
const { useEmulatorContext: original } = await vi.importActual<
typeof contextHooks
>('../../../hooks/context.tsx');

vi.spyOn(contextHooks, 'useEmulatorContext').mockImplementation(() => ({
...original(),
emulator: {
defaultKeyBindings: () => [
{ gbaInput: 'A', key: 'X', location: 0 },
{ gbaInput: 'B', key: 'Z', location: 0 }
]
} as GBAEmulator
}));

renderWithContext(<KeyBindingsForm id="testId" />);

expect(screen.getByLabelText('A')).toBeInTheDocument();
expect(screen.getByDisplayValue('X')).toBeInTheDocument();

expect(screen.getByLabelText('B')).toBeInTheDocument();
expect(screen.getByDisplayValue('Z')).toBeInTheDocument();
});

it('renders form validation errors', async () => {
const errorPostfix = ' is reserved for accessibility requirements';
const { useEmulatorContext: original } = await vi.importActual<
typeof contextHooks
>('../../../hooks/context.tsx');

vi.spyOn(contextHooks, 'useEmulatorContext').mockImplementation(() => ({
...original(),
emulator: {
defaultKeyBindings: () => [
{ gbaInput: 'A', key: 'X', location: 0 },
// users are no longer able to naturally type tab due to form focus
{ gbaInput: 'B', key: 'Tab', location: 0 }
]
} as GBAEmulator
}));

renderWithContext(
<>
<KeyBindingsForm id="testId" />{' '}
<button form="testId" type="submit">
submit
</button>
</>
);

const submitButton = screen.getByRole('button', { name: 'submit' });

await userEvent.type(screen.getByLabelText('A'), ' ');

await userEvent.click(submitButton);

expect(screen.getByText('Space' + errorPostfix)).toBeInTheDocument();
expect(screen.getByText('Tab' + errorPostfix)).toBeInTheDocument();
});

it('form values can be changed and properly persisted', async () => {
const setItemSpy = vi.spyOn(Storage.prototype, 'setItem');
const remapBindingsSpy: (keyBindings: KeyBinding[]) => void = vi.fn();
const { useEmulatorContext: original } = await vi.importActual<
typeof contextHooks
>('../../../hooks/context.tsx');

vi.spyOn(contextHooks, 'useEmulatorContext').mockImplementation(() => ({
...original(),
emulator: {
defaultKeyBindings: () => [
{ gbaInput: 'A', key: 'X', location: 0 },
{ gbaInput: 'B', key: 'Z', location: 0 }
],
remapKeyBindings: remapBindingsSpy
} as GBAEmulator,
isEmulatorRunning: true
}));

renderWithContext(
<>
<KeyBindingsForm id="testId" />{' '}
<button form="testId" type="submit">
submit
</button>
</>
);

const submitButton = screen.getByRole('button', { name: 'submit' });

await userEvent.type(screen.getByLabelText('A'), 'T');
await userEvent.type(screen.getByLabelText('B'), '{delete}');

await userEvent.click(submitButton);

expect(setItemSpy).toHaveBeenCalledWith(
emulatorKeyBindingsLocalStorageKey,
'[{"gbaInput":"A","key":"T","location":0},{"gbaInput":"B","key":"Delete","location":0}]'
);
expect(remapBindingsSpy).toHaveBeenCalledWith([
{ gbaInput: 'A', key: 'T', location: 0 },
{ gbaInput: 'B', key: 'Delete', location: 0 }
]);
});

it('does not accept tab keys', async () => {
const { useEmulatorContext: original } = await vi.importActual<
typeof contextHooks
>('../../../hooks/context.tsx');

vi.spyOn(contextHooks, 'useEmulatorContext').mockImplementation(() => ({
...original(),
emulator: {
defaultKeyBindings: () => [{ gbaInput: 'A', key: 'X', location: 0 }]
} as GBAEmulator
}));

renderWithContext(
<>
<KeyBindingsForm id="testId" />
</>
);

await userEvent.type(screen.getByLabelText('A'), '{tab}');

expect(screen.getByDisplayValue('X')).toBeInTheDocument();
});

it('renders initial values from storage', () => {
vi.spyOn(Storage.prototype, 'getItem').mockReturnValue(
'[{ "gbaInput": "L", "key": "A", "location": 0 },{ "gbaInput": "R", "key": "S", "location": 0 }]'
);

renderWithContext(<KeyBindingsForm id="testId" />);

expect(screen.getByLabelText('L')).toBeInTheDocument();
expect(screen.getByDisplayValue('A')).toBeInTheDocument();

expect(screen.getByLabelText('R')).toBeInTheDocument();
expect(screen.getByDisplayValue('S')).toBeInTheDocument();
});
});
6 changes: 5 additions & 1 deletion gbajs3/src/components/modals/controls/key-bindings-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ export const KeyBindingsForm = ({ id }: KeyBindingsFormProps) => {
const renderedBindings = currentKeyBindings ?? defaultKeyBindings;

return (
<StyledForm id={id} onSubmit={handleSubmit(onSubmit)}>
<StyledForm
aria-label="Key Bindings Form"
id={id}
onSubmit={handleSubmit(onSubmit)}
>
{renderedBindings?.map((keyBinding) => (
<Controller
key={`gba_input_${keyBinding.gbaInput.toLowerCase()}`}
Expand Down
5 changes: 4 additions & 1 deletion gbajs3/test/render-with-context.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { render } from '@testing-library/react';
import { ThemeProvider } from 'styled-components';

import { EmulatorProvider } from '../src/context/emulator/emulator.tsx';
import { ModalProvider } from '../src/context/modal/modal.tsx';
import { GbaDarkTheme } from '../src/context/theme/theme.tsx';

Expand All @@ -9,7 +10,9 @@ import type { ReactNode } from 'react';
export const renderWithContext = (testNode: ReactNode) => {
return render(
<ThemeProvider theme={GbaDarkTheme}>
<ModalProvider>{testNode}</ModalProvider>
<EmulatorProvider>
<ModalProvider>{testNode}</ModalProvider>
</EmulatorProvider>
</ThemeProvider>
);
};

0 comments on commit 26fb79a

Please sign in to comment.