Skip to content

Commit

Permalink
feat: wrap all context access, more tests, mocking strategy
Browse files Browse the repository at this point in the history
- not sure if im sold on how I'm mocking so far, but this is food for thought

- seems like this may be the best way for me to interact with the global context in tests
  • Loading branch information
Nick VanCise authored and Nick VanCise committed Jan 4, 2024
1 parent ecdfdca commit 3f12872
Show file tree
Hide file tree
Showing 46 changed files with 315 additions and 204 deletions.
15 changes: 4 additions & 11 deletions gbajs3/src/components/controls/control-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import { Slider, useMediaQuery } from '@mui/material';
import { useLocalStorage } from '@uidotdev/usehooks';
import {
useCallback,
useContext,
useId,
useState,
type ReactNode
} from 'react';
import { useCallback, useId, useState, type ReactNode } from 'react';
import { IconContext } from 'react-icons';
import { AiOutlineFastForward, AiOutlineForward } from 'react-icons/ai';
import {
Expand All @@ -22,8 +16,7 @@ import { Rnd } from 'react-rnd';
import { css, styled, useTheme } from 'styled-components';

import { emulatorVolumeLocalStorageKey } from '../../context/emulator/consts.tsx';
import { EmulatorContext } from '../../context/emulator/emulator.tsx';
import { LayoutContext } from '../../context/layout/layout.tsx';
import { useEmulatorContext, useLayoutContext } from '../../hooks/context.tsx';
import {
EmbeddedProductTour,
type TourSteps
Expand Down Expand Up @@ -133,8 +126,8 @@ export const ControlPanel = () => {
setAreItemsDraggable,
areItemsResizable,
setAreItemsResizable
} = useContext(EmulatorContext);
const { layouts, setLayout } = useContext(LayoutContext);
} = useEmulatorContext();
const { layouts, setLayout } = useLayoutContext();
const [isFastForwardOn, setIsFastForwardOn] = useState(false);
const theme = useTheme();
const isLargerThanPhone = useMediaQuery(theme.isLargerThanPhone);
Expand Down
15 changes: 4 additions & 11 deletions gbajs3/src/components/controls/o-pad.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,8 @@
import {
useState,
useRef,
useContext,
useCallback,
type PointerEvent
} from 'react';
import { useState, useRef, useCallback, type PointerEvent } from 'react';
import Draggable from 'react-draggable';
import { styled } from 'styled-components';

import { EmulatorContext } from '../../context/emulator/emulator.tsx';
import { LayoutContext } from '../../context/layout/layout.tsx';
import { useEmulatorContext, useLayoutContext } from '../../hooks/context.tsx';

import type { Position } from 'react-rnd';

Expand Down Expand Up @@ -122,8 +115,8 @@ const RightArrow = styled(DirectionArrow)`
`;

export const OPad = ({ initialPosition }: OPadProps) => {
const { emulator, areItemsDraggable } = useContext(EmulatorContext);
const { layouts, setLayout } = useContext(LayoutContext);
const { emulator, areItemsDraggable } = useEmulatorContext();
const { layouts, setLayout } = useLayoutContext();
const [position, setPosition] = useState({ x: 0, y: 0 });
const [isControlled, setIsControlled] = useState(true);
const containerDragRef = useRef<HTMLDivElement>(null);
Expand Down
9 changes: 4 additions & 5 deletions gbajs3/src/components/controls/virtual-button.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { useContext, useRef, type ReactNode } from 'react';
import { useRef, type ReactNode } from 'react';
import Draggable from 'react-draggable';
import { styled } from 'styled-components';

import { EmulatorContext } from '../../context/emulator/emulator.tsx';
import { LayoutContext } from '../../context/layout/layout.tsx';
import { useEmulatorContext, useLayoutContext } from '../../hooks/context.tsx';
import { ButtonBase } from '../shared/custom-button-base.tsx';

type VirtualButtonProps = {
Expand Down Expand Up @@ -93,8 +92,8 @@ export const VirtualButton = ({
enabled = false,
ariaLabel
}: VirtualButtonProps) => {
const { emulator, areItemsDraggable } = useContext(EmulatorContext);
const { layouts, setLayout } = useContext(LayoutContext);
const { emulator, areItemsDraggable } = useEmulatorContext();
const { layouts, setLayout } = useLayoutContext();
const dragRef = useRef<HTMLButtonElement | null>(null);

if (!enabled) return null;
Expand Down
19 changes: 10 additions & 9 deletions gbajs3/src/components/controls/virtual-controls.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useMediaQuery } from '@mui/material';
import { useLocalStorage } from '@uidotdev/usehooks';
import { useContext } from 'react';
import { IconContext } from 'react-icons';
import {
BiRefresh,
Expand All @@ -16,10 +15,12 @@ import {
} from './consts.tsx';
import { OPad } from './o-pad.tsx';
import { VirtualButton } from './virtual-button.tsx';
import { AuthContext } from '../../context/auth/auth.tsx';
import { EmulatorContext } from '../../context/emulator/emulator.tsx';
import { LayoutContext } from '../../context/layout/layout.tsx';
import { ModalContext } from '../../context/modal/modal.tsx';
import {
useEmulatorContext,
useLayoutContext,
useAuthContext,
useModalContext
} from '../../hooks/context.tsx';
import { UploadSaveToServerModal } from '../modals/upload-save-to-server.tsx';

import type { AreVirtualControlsEnabledProps } from '../modals/controls/virtual-controls-form.tsx';
Expand Down Expand Up @@ -50,10 +51,10 @@ export const VirtualControls = () => {
const theme = useTheme();
const isLargerThanPhone = useMediaQuery(theme.isLargerThanPhone);
const isMobileWithUrlBar = useMediaQuery(theme.isMobileWithUrlBar);
const { emulator, isEmulatorRunning } = useContext(EmulatorContext);
const { isAuthenticated } = useContext(AuthContext);
const { setModalContent, setIsModalOpen } = useContext(ModalContext);
const { layouts } = useContext(LayoutContext);
const { emulator, isEmulatorRunning } = useEmulatorContext();
const { isAuthenticated } = useAuthContext();
const { setModalContent, setIsModalOpen } = useModalContext();
const { layouts } = useLayoutContext();
const [currentSaveStateSlot] = useLocalStorage(
saveStateSlotLocalStorageKey,
0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
exports[`<ModalBody /> > container matches snapshot 1`] = `
<div
class="sc-bcXGCL kmdKjB"
data-testid="modal-body:wrapper"
>
Test Footer Content
</div>
Expand Down
54 changes: 54 additions & 0 deletions gbajs3/src/components/modals/about.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { describe, expect, it, vi } from 'vitest';

import { AboutModal } from './about.tsx';
import { renderWithContext } from '../../../test/render-with-context.tsx';
import * as contextHooks from '../../hooks/context.tsx';
import { productTourLocalStorageKey } from '../product-tour/consts.tsx';

describe('<AboutModal>', () => {
it('triggers product tour and closes modal when taking a tour', async () => {
const setIsModalOpenSpy = vi.fn();
const setItemSpy = vi.spyOn(Storage.prototype, 'setItem');
const { useModalContext: original } = await vi.importActual<
typeof contextHooks
>('../../hooks/context.tsx');

vi.spyOn(contextHooks, 'useModalContext').mockImplementation(() => ({
...original(),
setIsModalOpen: setIsModalOpenSpy
}));

renderWithContext(<AboutModal />);

// click the close button
const closeButton = screen.getByText('Take a tour', { selector: 'button' });
expect(closeButton).toBeInTheDocument();
await userEvent.click(closeButton);

expect(setItemSpy).toHaveBeenCalledWith(productTourLocalStorageKey, '{}');
expect(setIsModalOpenSpy).toHaveBeenCalledWith(false);
});

it('closes modal using the close button', async () => {
const setIsModalOpenSpy = vi.fn();
const { useModalContext: original } = await vi.importActual<
typeof contextHooks
>('../../hooks/context.tsx');

vi.spyOn(contextHooks, 'useModalContext').mockImplementation(() => ({
...original(),
setIsModalOpen: setIsModalOpenSpy
}));

renderWithContext(<AboutModal />);

// click the close button
const closeButton = screen.getByText('Close', { selector: 'button' });
expect(closeButton).toBeInTheDocument();
await userEvent.click(closeButton);

expect(setIsModalOpenSpy).toHaveBeenCalledWith(false);
});
});
5 changes: 2 additions & 3 deletions gbajs3/src/components/modals/about.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import { Button } from '@mui/material';
import { useLocalStorage } from '@uidotdev/usehooks';
import { useContext } from 'react';

import { ModalBody } from './modal-body.tsx';
import { ModalFooter } from './modal-footer.tsx';
import { ModalHeader } from './modal-header.tsx';
import { ModalContext } from '../../context/modal/modal.tsx';
import { useModalContext } from '../../hooks/context.tsx';
import { productTourLocalStorageKey } from '../product-tour/consts.tsx';

import type { CompletedProductTourSteps } from '../product-tour/product-tour-intro.tsx';

export const AboutModal = () => {
const { setIsModalOpen } = useContext(ModalContext);
const { setIsModalOpen } = useModalContext();
const [, setHasCompletedProductTourSteps] = useLocalStorage<
CompletedProductTourSteps | undefined
>(productTourLocalStorageKey);
Expand Down
9 changes: 4 additions & 5 deletions gbajs3/src/components/modals/cheats.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Button, TextField, useMediaQuery } from '@mui/material';
import { useCallback, useContext, useId, useMemo, useState } from 'react';
import { useCallback, useId, useMemo, useState } from 'react';
import { useFieldArray, useForm } from 'react-hook-form';
import { BiPlus } from 'react-icons/bi';
import { CiSquareRemove } from 'react-icons/ci';
Expand All @@ -8,8 +8,7 @@ import { styled, useTheme } from 'styled-components';
import { ModalBody } from './modal-body.tsx';
import { ModalFooter } from './modal-footer.tsx';
import { ModalHeader } from './modal-header.tsx';
import { EmulatorContext } from '../../context/emulator/emulator.tsx';
import { ModalContext } from '../../context/modal/modal.tsx';
import { useEmulatorContext, useModalContext } from '../../hooks/context.tsx';
import {
EmbeddedProductTour,
type TourSteps
Expand Down Expand Up @@ -88,8 +87,8 @@ const HelpText = styled.p<HelpTextProps>`
export const CheatsModal = () => {
const theme = useTheme();
const isLargerThanPhone = useMediaQuery(theme.isLargerThanPhone);
const { setIsModalOpen } = useContext(ModalContext);
const { emulator } = useContext(EmulatorContext);
const { setIsModalOpen } = useModalContext();
const { emulator } = useEmulatorContext();
const [viewRawCheats, setViewRawCheats] = useState(false);
const cheatsFormId = useId();
const addCheatButtonId = useId();
Expand Down
9 changes: 4 additions & 5 deletions gbajs3/src/components/modals/controls.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { Button, Tabs, Tab } from '@mui/material';
import { useContext, useId, useState, type ReactNode } from 'react';
import { useId, useState, type ReactNode } from 'react';
import { styled } from 'styled-components';

import { KeyBindingsForm } from './controls/key-bindings-form.tsx';
import { VirtualControlsForm } from './controls/virtual-controls-form.tsx';
import { ModalBody } from './modal-body.tsx';
import { ModalFooter } from './modal-footer.tsx';
import { ModalHeader } from './modal-header.tsx';
import { LayoutContext } from '../../context/layout/layout.tsx';
import { ModalContext } from '../../context/modal/modal.tsx';
import { useLayoutContext, useModalContext } from '../../hooks/context.tsx';
import {
EmbeddedProductTour,
type TourSteps
Expand Down Expand Up @@ -62,7 +61,7 @@ const ControlTabs = ({
keyBindingsFormId,
resetPositionsButtonId
}: ControlTabsProps) => {
const { clearLayouts } = useContext(LayoutContext);
const { clearLayouts } = useLayoutContext();
const [value, setValue] = useState(0);

const handleChange = (_: React.SyntheticEvent, newValue: number) => {
Expand Down Expand Up @@ -98,7 +97,7 @@ const ControlTabs = ({
};

export const ControlsModal = () => {
const { setIsModalOpen } = useContext(ModalContext);
const { setIsModalOpen } = useModalContext();
const virtualControlsFormId = useId();
const keyBindingsFormId = useId();
const saveChangesButtonId = useId();
Expand Down
5 changes: 2 additions & 3 deletions gbajs3/src/components/modals/controls/key-bindings-form.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { TextField } from '@mui/material';
import { useLocalStorage } from '@uidotdev/usehooks';
import { useContext } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { styled } from 'styled-components';

import { emulatorKeyBindingsLocalStorageKey } from '../../../context/emulator/consts.tsx';
import { EmulatorContext } from '../../../context/emulator/emulator.tsx';
import { useEmulatorContext } from '../../../hooks/context.tsx';

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

Expand All @@ -24,7 +23,7 @@ const StyledForm = styled.form`
`;

export const KeyBindingsForm = ({ id }: KeyBindingsFormProps) => {
const { emulator, isEmulatorRunning } = useContext(EmulatorContext);
const { emulator, isEmulatorRunning } = useEmulatorContext();
const {
handleSubmit,
setValue,
Expand Down
9 changes: 4 additions & 5 deletions gbajs3/src/components/modals/download-save.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { Button } from '@mui/material';
import { useContext, useId, useState } from 'react';
import { useId, useState } from 'react';
import { BiError } from 'react-icons/bi';
import { useTheme } from 'styled-components';

import { ModalBody } from './modal-body.tsx';
import { ModalFooter } from './modal-footer.tsx';
import { ModalHeader } from './modal-header.tsx';
import { EmulatorContext } from '../../context/emulator/emulator.tsx';
import { ModalContext } from '../../context/modal/modal.tsx';
import { useEmulatorContext, useModalContext } from '../../hooks/context.tsx';
import {
EmbeddedProductTour,
type TourSteps
Expand All @@ -17,8 +16,8 @@ import { CenteredText } from '../shared/styled.tsx';

export const DownloadSaveModal = () => {
const theme = useTheme();
const { setIsModalOpen } = useContext(ModalContext);
const { emulator } = useContext(EmulatorContext);
const { setIsModalOpen } = useModalContext();
const { emulator } = useEmulatorContext();
const downloadSaveButtonId = useId();
const [error, setError] = useState(false);

Expand Down
9 changes: 4 additions & 5 deletions gbajs3/src/components/modals/file-system.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ import {
treeItemClasses,
type TreeItemProps
} from '@mui/x-tree-view';
import { useCallback, useContext, useEffect, useId, useState } from 'react';
import { useCallback, useEffect, useId, useState } from 'react';
import { BiTrash } from 'react-icons/bi';
import { styled } from 'styled-components';

import { ModalBody } from './modal-body.tsx';
import { ModalFooter } from './modal-footer.tsx';
import { ModalHeader } from './modal-header.tsx';
import { EmulatorContext } from '../../context/emulator/emulator.tsx';
import { ModalContext } from '../../context/modal/modal.tsx';
import { useEmulatorContext, useModalContext } from '../../hooks/context.tsx';
import {
EmbeddedProductTour,
type TourSteps
Expand Down Expand Up @@ -122,8 +121,8 @@ const EmulatorFS = ({
};

export const FileSystemModal = () => {
const { setIsModalOpen } = useContext(ModalContext);
const { emulator } = useContext(EmulatorContext);
const { setIsModalOpen } = useModalContext();
const { emulator } = useEmulatorContext();
const [allFiles, setAllFiles] = useState<FileNode | undefined>();
const emulatorFsId = useId();
const saveFileSystemButtonId = useId();
Expand Down
38 changes: 38 additions & 0 deletions gbajs3/src/components/modals/legal.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { describe, expect, it, vi } from 'vitest';

import { renderWithContext } from '../../../test/render-with-context.tsx';
import * as contextHooks from '../../hooks/context.tsx';
import { LegalModal } from './legal.tsx';

Check failure on line 7 in gbajs3/src/components/modals/legal.spec.tsx

View workflow job for this annotation

GitHub Actions / gbajs3 / build

`./legal.tsx` import should occur before import of `../../../test/render-with-context.tsx`

describe('<LegalModal>', () => {
it('closes modal using the close button', async () => {
const setIsModalOpenSpy = vi.fn();
const { useModalContext: original } = await vi.importActual<
typeof contextHooks
>('../../hooks/context.tsx');

vi.spyOn(contextHooks, 'useModalContext').mockImplementation(() => ({
...original(),
setIsModalOpen: setIsModalOpenSpy
}));

renderWithContext(<LegalModal />);

// click the close button
const closeButton = screen.getByText('Close', { selector: 'button' });
expect(closeButton).toBeInTheDocument();
await userEvent.click(closeButton);

expect(setIsModalOpenSpy).toHaveBeenCalledWith(false);
});

it('renders with current year in copywright', () => {
vi.setSystemTime(new Date(2023, 0));

renderWithContext(<LegalModal />);

expect(screen.getByText(/© 2023,/)).toBeInTheDocument();
});
});
Loading

0 comments on commit 3f12872

Please sign in to comment.