From 271cb7d32171f4b48e949ab3a4a7cd7eb1039e96 Mon Sep 17 00:00:00 2001 From: Stephen Hanson Date: Fri, 3 May 2024 12:27:33 -0500 Subject: [PATCH] Move React Query setup into main app boilerplate --- src/cli.ts | 6 +--- src/commands/createApp.ts | 1 + src/commands/reactQuery.ts | 22 ------------- templates/boilerplate/jest.setup.js | 14 ++++++++ templates/boilerplate/package.json | 4 +++ .../boilerplate/src/__tests__/App.test.tsx | 7 ++++ .../src/components/ExampleCoffees.tsx | 2 +- .../src/test/mock.ts | 0 templates/boilerplate/src/test/render.tsx | 33 +++++++++++++++++-- .../src/test/server.ts | 0 templates/boilerplate/src/test/sleep.ts | 5 +++ .../boilerplate/src/test/waitForUpdates.ts | 12 +++++++ .../src/util/api/api.ts | 2 +- .../src/util/api/queryClient.ts | 0 templates/createApp/src/test/render.tsx | 4 +++ 15 files changed, 81 insertions(+), 31 deletions(-) delete mode 100644 src/commands/reactQuery.ts rename templates/{reactQuery => boilerplate}/src/components/ExampleCoffees.tsx (97%) rename templates/{reactQuery => boilerplate}/src/test/mock.ts (100%) rename templates/{reactQuery => boilerplate}/src/test/server.ts (100%) create mode 100644 templates/boilerplate/src/test/sleep.ts create mode 100644 templates/boilerplate/src/test/waitForUpdates.ts rename templates/{reactQuery => boilerplate}/src/util/api/api.ts (96%) rename templates/{reactQuery => boilerplate}/src/util/api/queryClient.ts (100%) diff --git a/src/cli.ts b/src/cli.ts index f70998a..09daa43 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -60,11 +60,7 @@ export default function runCli() { '--no-interactive', 'Pass true to skip all prompts and use default values', ) - .action(buildAction(import('./commands/notifications'))) - - .command('react-query') - .description('Add React Query') - .action(buildAction(import('./commands/reactQuery'))); + .action(buildAction(import('./commands/notifications'))); printWelcome(); program.parse(); diff --git a/src/commands/createApp.ts b/src/commands/createApp.ts index 641b010..d670dd6 100644 --- a/src/commands/createApp.ts +++ b/src/commands/createApp.ts @@ -106,6 +106,7 @@ async function printIntro(appName: string) { - ESLint - Jest, React Native Testing Library - React Navigation + - TanStack React Query - Intuitive directory structure `); diff --git a/src/commands/reactQuery.ts b/src/commands/reactQuery.ts deleted file mode 100644 index 56b288e..0000000 --- a/src/commands/reactQuery.ts +++ /dev/null @@ -1,22 +0,0 @@ -import addDependency from '../util/addDependency'; -import appendToFile from '../util/appendToFile'; -import copyTemplateDirectory from '../util/copyTemplateDirectory'; -import prependToFile from '../util/prependToFile'; - -export default async function addReactQuery() { - await addDependency('@tanstack/react-query axios msw'); - await copyTemplateDirectory({ templateDir: 'reactQuery' }); - await prependToFile('jest.setup.js', "import server from 'src/test/server';"); - await appendToFile( - 'jest.setup.js', - ` -// listen with MSW server. Individual tests can pass mocks to 'render' function -beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); -afterAll(() => server.close()); - -beforeEach(() => { - server.resetHandlers() -}); -`, - ); -} diff --git a/templates/boilerplate/jest.setup.js b/templates/boilerplate/jest.setup.js index 7d448c7..00da6e6 100644 --- a/templates/boilerplate/jest.setup.js +++ b/templates/boilerplate/jest.setup.js @@ -2,6 +2,8 @@ import '@testing-library/jest-native/extend-expect'; import { configure } from '@testing-library/react-native'; import mockSafeAreaContext from 'react-native-safe-area-context/jest/mock'; import mockBackHandler from 'react-native/Libraries/Utilities/__mocks__/BackHandler.js'; +import server from 'src/test/server'; +import queryClient from 'src/util/api/queryClient'; beforeEach(() => { jest.clearAllMocks(); @@ -35,6 +37,18 @@ jest.mock('@react-native-async-storage/async-storage', () => jest.mock('react-native-keyboard-aware-scroll-view'); +// listen with MSW server. Individual tests can pass mocks to 'render' function +beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); +afterAll(() => server.close()); + +beforeEach(() => { + server.resetHandlers(); +}); + +afterEach(() => { + queryClient.clear(); +}); + // configure debug output for RN Testing Library // is way too verbose by default. Only include common // props that might affect test failure. diff --git a/templates/boilerplate/package.json b/templates/boilerplate/package.json index fc062a1..a9d5598 100644 --- a/templates/boilerplate/package.json +++ b/templates/boilerplate/package.json @@ -23,10 +23,13 @@ "@react-navigation/bottom-tabs": "^6.5.20", "@react-navigation/native": "^6.1.10", "@react-navigation/native-stack": "^6.9.18", + "@tanstack/react-query": "^5.32.1", + "axios": "^1.6.8", "expo": "^50.0.17", "expo-status-bar": "~1.11.1", "jest": "^29.3.1", "jest-expo": "~50.0.2", + "msw": "^2.2.14", "react": "18.2.0", "react-native": "0.73.6", "react-native-keyboard-aware-scrollview": "^2.1.0", @@ -40,6 +43,7 @@ "@thoughtbot/eslint-config": "^1.0.2", "@types/jest": "^29.5.12", "@types/react": "~18.2.73", + "@types/react-test-renderer": "^18.0.7", "babel-jest": "^29.7.0", "create-belt-app": "^0.4.0", "eslint": "^8.56.0", diff --git a/templates/boilerplate/src/__tests__/App.test.tsx b/templates/boilerplate/src/__tests__/App.test.tsx index 6bed27f..61ebeca 100644 --- a/templates/boilerplate/src/__tests__/App.test.tsx +++ b/templates/boilerplate/src/__tests__/App.test.tsx @@ -4,6 +4,13 @@ import mock from 'src/test/mock'; import { renderApplication } from 'src/test/render'; test('renders', async () => { + // We would not normally recommend fake timers, but the tests are currently + // throwing a "not wrapped in act" warning after this test finishes. One + // option is to put a `await waitForUpdates()` at the end of the test, but + // fake timers also work here until we find a better solution. The stack trace + // seems to point to React Navigation bottom tabs. + jest.useFakeTimers(); + const mocks = [mockCoffees()]; renderApplication({ mocks }); diff --git a/templates/reactQuery/src/components/ExampleCoffees.tsx b/templates/boilerplate/src/components/ExampleCoffees.tsx similarity index 97% rename from templates/reactQuery/src/components/ExampleCoffees.tsx rename to templates/boilerplate/src/components/ExampleCoffees.tsx index 46da8b4..bf1a228 100644 --- a/templates/reactQuery/src/components/ExampleCoffees.tsx +++ b/templates/boilerplate/src/components/ExampleCoffees.tsx @@ -2,7 +2,7 @@ import { useQuery } from '@tanstack/react-query'; import { FlatList, Image, Text, View } from 'react-native'; import api, { Coffee as CoffeeType } from 'src/util/api/api'; -// TODO: sample component, remove +// TODO: sample data, remove export default function ExampleCoffees() { const { data } = useQuery({ queryKey: ['coffee'], queryFn: api.coffee }); diff --git a/templates/reactQuery/src/test/mock.ts b/templates/boilerplate/src/test/mock.ts similarity index 100% rename from templates/reactQuery/src/test/mock.ts rename to templates/boilerplate/src/test/mock.ts diff --git a/templates/boilerplate/src/test/render.tsx b/templates/boilerplate/src/test/render.tsx index 2a7c909..0a346f7 100644 --- a/templates/boilerplate/src/test/render.tsx +++ b/templates/boilerplate/src/test/render.tsx @@ -1,15 +1,44 @@ import { NavigationContainer } from '@react-navigation/native'; +import { QueryClientProvider } from '@tanstack/react-query'; import { RenderAPI, // eslint-disable-next-line no-restricted-imports render as TestingLibraryRender, } from '@testing-library/react-native'; +import { RequestHandler } from 'msw'; import { ReactElement } from 'react'; +import Providers, { Provider } from 'src/components/Providers'; +import RootNavigator from 'src/navigators/RootNavigator'; +import queryClient from 'src/util/api/queryClient'; +import server from './server'; + +export type RenderOptions = { + mocks?: Array; +}; // TODO: this will become customized as the codebase progresses, so our // tests can be wrapped with appropriate providers, mocks can be supplied, etc -export default function render(element: ReactElement): RenderAPI { +export default function render( + element: ReactElement, + { mocks }: RenderOptions = {}, +): RenderAPI { + if (mocks) { + server.use(...mocks); + } + + const providers: Provider[] = [ + (children) => ( + {children} + ), + (children) => {children}, + // CODEGEN:BELT:PROVIDERS - do not remove + ]; + return TestingLibraryRender( - {element}, + {element}, ); } + +export function renderApplication(options: RenderOptions = {}) { + return render(, options); +} diff --git a/templates/reactQuery/src/test/server.ts b/templates/boilerplate/src/test/server.ts similarity index 100% rename from templates/reactQuery/src/test/server.ts rename to templates/boilerplate/src/test/server.ts diff --git a/templates/boilerplate/src/test/sleep.ts b/templates/boilerplate/src/test/sleep.ts new file mode 100644 index 0000000..6172333 --- /dev/null +++ b/templates/boilerplate/src/test/sleep.ts @@ -0,0 +1,5 @@ +export default function sleep(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} diff --git a/templates/boilerplate/src/test/waitForUpdates.ts b/templates/boilerplate/src/test/waitForUpdates.ts new file mode 100644 index 0000000..20000bd --- /dev/null +++ b/templates/boilerplate/src/test/waitForUpdates.ts @@ -0,0 +1,12 @@ +import { act } from '@testing-library/react-native'; +import sleep from './sleep'; + +/** + * Wait a specified time, wrapped in act + * Usually, it is better to use waitFor or a findBy* matcher, + * but this is sometimes required + * @param time + */ +export default async function waitForUpdates(time = 2) { + return act(() => sleep(time)); +} diff --git a/templates/reactQuery/src/util/api/api.ts b/templates/boilerplate/src/util/api/api.ts similarity index 96% rename from templates/reactQuery/src/util/api/api.ts rename to templates/boilerplate/src/util/api/api.ts index b25ce11..13d8203 100644 --- a/templates/reactQuery/src/util/api/api.ts +++ b/templates/boilerplate/src/util/api/api.ts @@ -36,7 +36,7 @@ const api = { export type Coffee = { title: string; description: string; - /** url */ + /** the url to the image */ image: string; id: number; }; diff --git a/templates/reactQuery/src/util/api/queryClient.ts b/templates/boilerplate/src/util/api/queryClient.ts similarity index 100% rename from templates/reactQuery/src/util/api/queryClient.ts rename to templates/boilerplate/src/util/api/queryClient.ts diff --git a/templates/createApp/src/test/render.tsx b/templates/createApp/src/test/render.tsx index 0a346f7..8ab04d4 100644 --- a/templates/createApp/src/test/render.tsx +++ b/templates/createApp/src/test/render.tsx @@ -39,6 +39,10 @@ export default function render( ); } +/** + * Render the full RootNavigator. Use this function for integration tests + * that need to be able to navigate between screens. + */ export function renderApplication(options: RenderOptions = {}) { return render(, options); }