diff --git a/.changeset/twelve-needles-worry.md b/.changeset/twelve-needles-worry.md new file mode 100644 index 0000000000..db0c6af595 --- /dev/null +++ b/.changeset/twelve-needles-worry.md @@ -0,0 +1,7 @@ +--- +"blitz": patch +"@blitzjs/next": patch +"@blitzjs/generator": patch +--- + +Add client testing utilities and a sample test to a new blitz app template diff --git a/apps/toolkit-app/test/index.test.tsx b/apps/toolkit-app/test/index.test.tsx new file mode 100644 index 0000000000..5ebacfa027 --- /dev/null +++ b/apps/toolkit-app/test/index.test.tsx @@ -0,0 +1,27 @@ +import { useCurrentUser } from "app/users/hooks/useCurrentUser" +import { render } from "test/utils" + +import Home from "../pages/index" + +jest.mock("app/users/hooks/useCurrentUser") +const mockUseCurrentUser = useCurrentUser as jest.MockedFunction + +describe("renders blitz documentation link", () => { + it("test", () => { + // This is an example of how to ensure a specific item is in the document + // But it's disabled by default (by test.skip) so the test doesn't fail + // when you remove the the default content from the page + + // This is an example on how to mock api hooks when testing + mockUseCurrentUser.mockReturnValue({ + id: 1, + name: "User", + email: "user@email.com", + role: "user", + }) + + const { getByText } = render() + const linkElement = getByText(/Documentation/i) + expect(linkElement).toBeInTheDocument() + }) +}) diff --git a/apps/toolkit-app/test/utils.tsx b/apps/toolkit-app/test/utils.tsx new file mode 100644 index 0000000000..e63e205a31 --- /dev/null +++ b/apps/toolkit-app/test/utils.tsx @@ -0,0 +1,104 @@ +import { render as defaultRender } from "@testing-library/react" +import { renderHook as defaultRenderHook } from "@testing-library/react-hooks" +import { NextRouter } from "next/router" +import { BlitzProvider, RouterContext } from "@blitzjs/next" +import { QueryClient } from "@blitzjs/rpc" + +export * from "@testing-library/react" + +// -------------------------------------------------------------------------------- +// This file customizes the render() and renderHook() test functions provided +// by React testing library. It adds a router context wrapper with a mocked router. +// +// You should always import `render` and `renderHook` from this file +// +// This is the place to add any other context providers you need while testing. +// -------------------------------------------------------------------------------- + +// -------------------------------------------------- +// render() +// -------------------------------------------------- +// Override the default test render with our own +// +// You can override the router mock like this: +// +// const { baseElement } = render(, { +// router: { pathname: '/my-custom-pathname' }, +// }); +// -------------------------------------------------- + +const queryClient = new QueryClient() +export function render( + ui: RenderUI, + { wrapper, router, dehydratedState, ...options }: RenderOptions = {} +) { + if (!wrapper) { + // Add a default context wrapper if one isn't supplied from the test + wrapper = ({ children }: { children: React.ReactNode }) => ( + + + {children} + + + ) + } + return defaultRender(ui, { wrapper, ...options }) +} + +// -------------------------------------------------- +// renderHook() +// -------------------------------------------------- +// Override the default test renderHook with our own +// +// You can override the router mock like this: +// +// const result = renderHook(() => myHook(), { +// router: { pathname: '/my-custom-pathname' }, +// }); +// -------------------------------------------------- +export function renderHook( + hook: RenderHook, + { wrapper, router, dehydratedState, ...options }: RenderOptions = {} +) { + if (!wrapper) { + // Add a default context wrapper if one isn't supplied from the test + wrapper = ({ children }: { children: React.ReactNode }) => ( + + + {children} + + + ) + } + return defaultRenderHook(hook, { wrapper, ...options }) +} + +export const mockRouter: NextRouter = { + basePath: "", + pathname: "/", + route: "/", + asPath: "/", + query: {}, + isReady: true, + isLocaleDomain: false, + isPreview: false, + push: jest.fn(), + replace: jest.fn(), + reload: jest.fn(), + back: jest.fn(), + prefetch: jest.fn(), + beforePopState: jest.fn(), + events: { + on: jest.fn(), + off: jest.fn(), + emit: jest.fn(), + }, + isFallback: false, +} + +type DefaultParams = Parameters +type RenderUI = DefaultParams[0] +type RenderOptions = DefaultParams[1] & { router?: Partial; dehydratedState?: unknown } + +type DefaultHookParams = Parameters +type RenderHook = DefaultHookParams[0] diff --git a/integration-tests/utils/blitz-test-utils.tsx b/integration-tests/utils/blitz-test-utils.tsx index e111fd5fc6..60b31f24a6 100644 --- a/integration-tests/utils/blitz-test-utils.tsx +++ b/integration-tests/utils/blitz-test-utils.tsx @@ -57,6 +57,7 @@ const BlitzProvider = ({ return children } + const compose = (...rest) => (x: React.ComponentType) => diff --git a/packages/blitz-next/src/index-browser.tsx b/packages/blitz-next/src/index-browser.tsx index 46f242d86c..f24e4ea0ea 100644 --- a/packages/blitz-next/src/index-browser.tsx +++ b/packages/blitz-next/src/index-browser.tsx @@ -1,10 +1,5 @@ import "./global" -import type { - ClientPlugin, - BlitzProvider as BlitzProviderType, - UnionToIntersection, - Simplify, -} from "blitz" +import type {ClientPlugin, BlitzProviderComponentType, UnionToIntersection, Simplify} from "blitz" import Head from "next/head" import React, {ReactNode} from "react" import {QueryClient, QueryClientProvider, Hydrate, HydrateOptions} from "@tanstack/react-query" @@ -22,14 +17,14 @@ export * from "./router-context" export {Routes} from ".blitz" const compose = - (...rest: BlitzProviderType[]) => + (...rest: BlitzProviderComponentType[]) => (x: React.ComponentType) => rest.reduceRight((y, f) => f(y), x) const buildWithBlitz = []>(plugins: TPlugins) => { const providers = plugins.reduce((acc, plugin) => { return plugin.withProvider ? acc.concat(plugin.withProvider) : acc - }, [] as BlitzProviderType[]) + }, [] as BlitzProviderComponentType[]) const withPlugins = compose(...providers) diff --git a/packages/blitz/src/index-browser.tsx b/packages/blitz/src/index-browser.tsx index 74f8ed3a98..1bb6fc85dd 100644 --- a/packages/blitz/src/index-browser.tsx +++ b/packages/blitz/src/index-browser.tsx @@ -3,7 +3,7 @@ import {ComponentType} from "react" import {IncomingMessage, ServerResponse} from "http" import {AuthenticationError, AuthorizationError, NotFoundError, RedirectError} from "./errors" -export type BlitzProvider = ( +export type BlitzProviderComponentType = ( component: ComponentType, ) => { (props: TProps): JSX.Element @@ -29,7 +29,7 @@ export interface ClientPlugin { ) => void } exports: () => Exports - withProvider?: BlitzProvider + withProvider?: BlitzProviderComponentType } export function createClientPlugin( diff --git a/packages/generator/templates/app/package.js.json b/packages/generator/templates/app/package.js.json index 185293b3ca..b2a9a2f791 100644 --- a/packages/generator/templates/app/package.js.json +++ b/packages/generator/templates/app/package.js.json @@ -36,6 +36,8 @@ "devDependencies": { "@next/bundle-analyzer": "12.0.8", "@testing-library/jest-dom": "5.16.3", + "@testing-library/react": "13.4.0", + "@testing-library/react-hooks": "8.0.1", "@types/jest": "27.4.1", "@types/node": "17.0.16", "@types/preview-email": "2.0.1", diff --git a/packages/generator/templates/app/package.ts.json b/packages/generator/templates/app/package.ts.json index f3e04ff40f..72d798fd59 100644 --- a/packages/generator/templates/app/package.ts.json +++ b/packages/generator/templates/app/package.ts.json @@ -36,6 +36,8 @@ "devDependencies": { "@next/bundle-analyzer": "12.0.8", "@testing-library/jest-dom": "5.16.3", + "@testing-library/react": "13.4.0", + "@testing-library/react-hooks": "8.0.1", "@types/jest": "27.4.1", "@types/node": "17.0.16", "@types/preview-email": "2.0.1", diff --git a/packages/generator/templates/app/test/index.test.tsx b/packages/generator/templates/app/test/index.test.tsx new file mode 100644 index 0000000000..a623312f6c --- /dev/null +++ b/packages/generator/templates/app/test/index.test.tsx @@ -0,0 +1,26 @@ +import { useCurrentUser } from "app/users/hooks/useCurrentUser" +import { render } from "test/utils" + +import Home from "../pages/index" + +jest.mock("app/users/hooks/useCurrentUser") +const mockUseCurrentUser = useCurrentUser as jest.MockedFunction + +test.skip("renders blitz documentation link", () => { + // This is an example of how to ensure a specific item is in the document + // But it's disabled by default (by test.skip) so the test doesn't fail + // when you remove the the default content from the page + + // This is an example on how to mock api hooks when testing + mockUseCurrentUser.mockReturnValue({ + id: 1, + name: "User", + email: "user@email.com", + role: "user", + }) + + const { getByText } = render() + const linkElement = getByText(/Documentation/i) + expect(linkElement).toBeInTheDocument() +}) + diff --git a/packages/generator/templates/app/test/utils.tsx b/packages/generator/templates/app/test/utils.tsx new file mode 100644 index 0000000000..6c4aad4cf6 --- /dev/null +++ b/packages/generator/templates/app/test/utils.tsx @@ -0,0 +1,105 @@ + +import { render as defaultRender } from "@testing-library/react" +import { renderHook as defaultRenderHook } from "@testing-library/react-hooks" +import { NextRouter } from "next/router" +import {BlitzProvider, RouterContext} from "@blitzjs/next" +import { QueryClient } from "@blitzjs/rpc" + +export * from "@testing-library/react" + +// -------------------------------------------------------------------------------- +// This file customizes the render() and renderHook() test functions provided +// by React testing library. It adds a router context wrapper with a mocked router. +// +// You should always import `render` and `renderHook` from this file +// +// This is the place to add any other context providers you need while testing. +// -------------------------------------------------------------------------------- + +// -------------------------------------------------- +// render() +// -------------------------------------------------- +// Override the default test render with our own +// +// You can override the router mock like this: +// +// const { baseElement } = render(, { +// router: { pathname: '/my-custom-pathname' }, +// }); +// -------------------------------------------------- + +const queryClient = new QueryClient() +export function render( + ui: RenderUI, + { wrapper, router, dehydratedState, ...options }: RenderOptions = {} +) { + if (!wrapper) { + // Add a default context wrapper if one isn't supplied from the test + wrapper = ({ children }: { children: React.ReactNode }) => ( + + + {children} + + + ) + } + return defaultRender(ui, { wrapper, ...options }) +} + +// -------------------------------------------------- +// renderHook() +// -------------------------------------------------- +// Override the default test renderHook with our own +// +// You can override the router mock like this: +// +// const result = renderHook(() => myHook(), { +// router: { pathname: '/my-custom-pathname' }, +// }); +// -------------------------------------------------- +export function renderHook( + hook: RenderHook, + { wrapper, router, dehydratedState, ...options }: RenderOptions = {} +) { + if (!wrapper) { + // Add a default context wrapper if one isn't supplied from the test + wrapper = ({ children }: { children: React.ReactNode }) => ( + + + {children} + + + ) + } + return defaultRenderHook(hook, { wrapper, ...options }) +} + +export const mockRouter: NextRouter = { + basePath: "", + pathname: "/", + route: "/", + asPath: "/", + query: {}, + isReady: true, + isLocaleDomain: false, + isPreview: false, + push: jest.fn(), + replace: jest.fn(), + reload: jest.fn(), + back: jest.fn(), + prefetch: jest.fn(), + beforePopState: jest.fn(), + events: { + on: jest.fn(), + off: jest.fn(), + emit: jest.fn(), + }, + isFallback: false, +} + +type DefaultParams = Parameters +type RenderUI = DefaultParams[0] +type RenderOptions = DefaultParams[1] & { router?: Partial; dehydratedState?: unknown } + +type DefaultHookParams = Parameters +type RenderHook = DefaultHookParams[0]