From 61faa9f45601da3507378714c8709b86dc98cd40 Mon Sep 17 00:00:00 2001 From: Matan Borenkraout Date: Tue, 17 Dec 2024 15:26:02 +0200 Subject: [PATCH 1/4] fix: support async act --- src/pure.ts | 28 ++++++++++++++-------------- test/render.test.tsx | 4 ++-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/pure.ts b/src/pure.ts index 2b47455..862b10f 100644 --- a/src/pure.ts +++ b/src/pure.ts @@ -7,7 +7,7 @@ import ReactDOMClient from 'react-dom/client' // we call act only when rendering to flush any possible effects // usually the async nature of Vitest browser mode ensures consistency, // but rendering is sync and controlled by React directly -function act(cb: () => unknown) { +async function act(cb: () => unknown) { // @ts-expect-error unstable_act is not typed, but exported const _act = React.act || React.unstable_act if (typeof _act !== 'function') { @@ -15,7 +15,7 @@ function act(cb: () => unknown) { } else { (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true - _act(cb) + await _act(cb) ;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = false } } @@ -28,8 +28,8 @@ export interface RenderResult extends LocatorSelectors { maxLength?: number, options?: PrettyDOMOptions ) => void - unmount: () => void - rerender: (ui: React.ReactNode) => void + unmount: () => Promise + rerender: (ui: React.ReactNode) => Promise asFragment: () => DocumentFragment } @@ -47,10 +47,10 @@ const mountedRootEntries: { root: ReturnType }[] = [] -export function render( +export async function render( ui: React.ReactNode, { container, baseElement, wrapper: WrapperComponent }: ComponentRenderOptions = {}, -): RenderResult { +): Promise { if (!baseElement) { // default to document.body instead of documentElement to avoid output of potentially-large // head elements (such as JSS style blocks) in debug output @@ -83,7 +83,7 @@ export function render( }) } - act(() => { + await act(() => { root!.render( strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)), ) @@ -93,13 +93,13 @@ export function render( container, baseElement, debug: (el, maxLength, options) => debug(el, maxLength, options), - unmount: () => { - act(() => { + unmount: async () => { + await act(() => { root.unmount() }) }, - rerender: (newUi: React.ReactNode) => { - act(() => { + rerender: async (newUi: React.ReactNode) => { + await act(() => { root.render( strictModeIfNeeded(wrapUiIfNeeded(newUi, WrapperComponent)), ) @@ -112,9 +112,9 @@ export function render( } } -export function cleanup(): void { - mountedRootEntries.forEach(({ root, container }) => { - act(() => { +export async function cleanup(): Promise { + mountedRootEntries.forEach(async ({ root, container }) => { + await act(() => { root.unmount() }) if (container.parentNode === document.body) { diff --git a/test/render.test.tsx b/test/render.test.tsx index b2b54b9..0d2f451 100644 --- a/test/render.test.tsx +++ b/test/render.test.tsx @@ -5,13 +5,13 @@ import { HelloWorld } from './fixtures/HelloWorld' import { Counter } from './fixtures/Counter' test('renders simple component', async () => { - const screen = render() + const screen = await render() await expect.element(page.getByText('Hello World')).toBeVisible() expect(screen.container).toMatchSnapshot() }) test('renders counter', async () => { - const screen = render() + const screen = await render() await expect.element(screen.getByText('Count is 1')).toBeVisible() await screen.getByRole('button', { name: 'Increment' }).click() From 6c9238b811a7c58bd1aa35e745f3a90a5421de92 Mon Sep 17 00:00:00 2001 From: Matan Borenkraout Date: Tue, 17 Dec 2024 15:51:40 +0200 Subject: [PATCH 2/4] test: add test to check for suspended component --- test/fixtures/HelloWorld.tsx | 8 ++++++-- test/render.test.tsx | 10 ++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/test/fixtures/HelloWorld.tsx b/test/fixtures/HelloWorld.tsx index 204fff0..365a35e 100644 --- a/test/fixtures/HelloWorld.tsx +++ b/test/fixtures/HelloWorld.tsx @@ -1,3 +1,7 @@ -export function HelloWorld(): React.ReactElement { - return
Hello World
+export function HelloWorld({ + name = 'World', +}: { + name?: string +}): React.ReactElement { + return
{`Hello ${name}`}
} diff --git a/test/render.test.tsx b/test/render.test.tsx index 0d2f451..a2a0e16 100644 --- a/test/render.test.tsx +++ b/test/render.test.tsx @@ -1,3 +1,4 @@ +import { Suspense } from 'react' import { expect, test } from 'vitest' import { page } from '@vitest/browser/context' import { render } from '../src/index' @@ -17,3 +18,12 @@ test('renders counter', async () => { await screen.getByRole('button', { name: 'Increment' }).click() await expect.element(screen.getByText('Count is 2')).toBeVisible() }) + +test('renders a suspended component', async () => { + const { getByText } = await render(, { + wrapper: ({ children }) => ( + Suspended!}>{children} + ), + }) + await expect.element(getByText('Hello Vitest')).toBeInTheDocument() +}) From 8716744ec83f10ce0b18986c7f5456345cb354bd Mon Sep 17 00:00:00 2001 From: Matan Borenkraout Date: Tue, 17 Dec 2024 22:42:30 +0200 Subject: [PATCH 3/4] rename test --- test/render.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/render.test.tsx b/test/render.test.tsx index a2a0e16..5a7360e 100644 --- a/test/render.test.tsx +++ b/test/render.test.tsx @@ -19,7 +19,7 @@ test('renders counter', async () => { await expect.element(screen.getByText('Count is 2')).toBeVisible() }) -test('renders a suspended component', async () => { +test('renders child component on mount for suspense boundary which is not suspending', async () => { const { getByText } = await render(, { wrapper: ({ children }) => ( Suspended!}>{children} From 9999e1e4b64373a63fd66c803ff70da98c0e0e10 Mon Sep 17 00:00:00 2001 From: Matan Borenkraout Date: Sun, 22 Dec 2024 23:03:58 +0200 Subject: [PATCH 4/4] fix component to suspend --- package.json | 8 +-- pnpm-lock.yaml | 87 +++++++++++---------------- test/fixtures/SuspendedHelloWorld.tsx | 19 ++++++ test/render.test.tsx | 6 +- 4 files changed, 62 insertions(+), 58 deletions(-) create mode 100644 test/fixtures/SuspendedHelloWorld.tsx diff --git a/package.json b/package.json index fdf0ad7..d3940f7 100644 --- a/package.json +++ b/package.json @@ -69,16 +69,16 @@ }, "devDependencies": { "@antfu/eslint-config": "^2.24.1", - "@types/react": "^18.0.0", - "@types/react-dom": "^18.3.0", + "@types/react": "^19.0.1", + "@types/react-dom": "^19.0.2", "@vitejs/plugin-react": "^4.3.3", "@vitest/browser": "^2.1.0", "bumpp": "^9.4.2", "changelogithub": "^0.13.9", "eslint": "^9.8.0", "playwright": "^1.46.0", - "react": "^18.0.0", - "react-dom": "^18.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", "tsup": "^8.2.4", "tsx": "^4.17.0", "typescript": "^5.5.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 30f60be..1d87f33 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,13 +10,13 @@ importers: devDependencies: '@antfu/eslint-config': specifier: ^2.24.1 - version: 2.24.1(@vue/compiler-sfc@3.4.35)(eslint@9.8.0)(typescript@5.5.4)(vitest@2.1.3(@types/node@22.1.0)(@vitest/browser@2.1.3)(msw@2.3.5(typescript@5.5.4))) + version: 2.24.1(@vue/compiler-sfc@3.4.35)(eslint@9.8.0)(typescript@5.5.4)(vitest@2.1.3) '@types/react': - specifier: ^18.0.0 - version: 18.3.3 + specifier: ^19.0.1 + version: 19.0.1 '@types/react-dom': - specifier: ^18.3.0 - version: 18.3.0 + specifier: ^19.0.2 + version: 19.0.2(@types/react@19.0.1) '@vitejs/plugin-react': specifier: ^4.3.3 version: 4.3.3(vite@5.3.5(@types/node@22.1.0)) @@ -36,11 +36,11 @@ importers: specifier: ^1.46.0 version: 1.46.0 react: - specifier: ^18.0.0 - version: 18.3.1 + specifier: ^19.0.0 + version: 19.0.0 react-dom: - specifier: ^18.0.0 - version: 18.3.1(react@18.3.1) + specifier: ^19.0.0 + version: 19.0.0(react@19.0.0) tsup: specifier: ^8.2.4 version: 8.2.4(jiti@1.21.6)(postcss@8.4.41)(tsx@4.17.0)(typescript@5.5.4)(yaml@2.5.0) @@ -817,14 +817,13 @@ packages: '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} - '@types/prop-types@15.7.12': - resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} - - '@types/react-dom@18.3.0': - resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} + '@types/react-dom@19.0.2': + resolution: {integrity: sha512-c1s+7TKFaDRRxr1TxccIX2u7sfCnc3RxkVyBIUA2lCpyqCF+QoAwQ/CBg7bsMdVwP120HEH143VQezKtef5nCg==} + peerDependencies: + '@types/react': ^19.0.0 - '@types/react@18.3.3': - resolution: {integrity: sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==} + '@types/react@19.0.1': + resolution: {integrity: sha512-YW6614BDhqbpR5KtUYzTA+zlA7nayzJRA9ljz9CQoxthR0sDisYZLuvSMsil36t4EH/uAt8T52Xb4sVw17G+SQ==} '@types/statuses@2.0.5': resolution: {integrity: sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==} @@ -1953,10 +1952,6 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - loose-envify@1.4.0: - resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} - hasBin: true - loupe@3.1.1: resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==} @@ -2304,10 +2299,10 @@ packages: rc9@2.1.2: resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} - react-dom@18.3.1: - resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + react-dom@19.0.0: + resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==} peerDependencies: - react: ^18.3.1 + react: ^19.0.0 react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} @@ -2316,8 +2311,8 @@ packages: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} - react@18.3.1: - resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + react@19.0.0: + resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} engines: {node: '>=0.10.0'} read-pkg-up@7.0.1: @@ -2389,8 +2384,8 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - scheduler@0.23.2: - resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + scheduler@0.25.0: + resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} scslre@0.3.0: resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} @@ -2874,7 +2869,7 @@ snapshots: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 - '@antfu/eslint-config@2.24.1(@vue/compiler-sfc@3.4.35)(eslint@9.8.0)(typescript@5.5.4)(vitest@2.1.3(@types/node@22.1.0)(@vitest/browser@2.1.3)(msw@2.3.5(typescript@5.5.4)))': + '@antfu/eslint-config@2.24.1(@vue/compiler-sfc@3.4.35)(eslint@9.8.0)(typescript@5.5.4)(vitest@2.1.3)': dependencies: '@antfu/install-pkg': 0.3.3 '@clack/prompts': 0.7.0 @@ -2899,7 +2894,7 @@ snapshots: eslint-plugin-toml: 0.11.1(eslint@9.8.0) eslint-plugin-unicorn: 55.0.0(eslint@9.8.0) eslint-plugin-unused-imports: 4.0.1(@typescript-eslint/eslint-plugin@8.0.1(@typescript-eslint/parser@8.0.1(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0) - eslint-plugin-vitest: 0.5.4(@typescript-eslint/eslint-plugin@8.0.1(@typescript-eslint/parser@8.0.1(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4)(vitest@2.1.3(@types/node@22.1.0)(@vitest/browser@2.1.3)(msw@2.3.5(typescript@5.5.4))) + eslint-plugin-vitest: 0.5.4(@typescript-eslint/eslint-plugin@8.0.1(@typescript-eslint/parser@8.0.1(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4)(vitest@2.1.3) eslint-plugin-vue: 9.27.0(eslint@9.8.0) eslint-plugin-yml: 1.14.0(eslint@9.8.0) eslint-processor-vue-blocks: 0.1.2(@vue/compiler-sfc@3.4.35)(eslint@9.8.0) @@ -3567,15 +3562,12 @@ snapshots: '@types/normalize-package-data@2.4.4': {} - '@types/prop-types@15.7.12': {} - - '@types/react-dom@18.3.0': + '@types/react-dom@19.0.2(@types/react@19.0.1)': dependencies: - '@types/react': 18.3.3 + '@types/react': 19.0.1 - '@types/react@18.3.3': + '@types/react@19.0.1': dependencies: - '@types/prop-types': 15.7.12 csstype: 3.1.3 '@types/statuses@2.0.5': {} @@ -3780,7 +3772,7 @@ snapshots: '@vue/compiler-core@3.4.35': dependencies: - '@babel/parser': 7.25.3 + '@babel/parser': 7.25.8 '@vue/shared': 3.4.35 entities: 4.5.0 estree-walker: 2.0.2 @@ -3793,7 +3785,7 @@ snapshots: '@vue/compiler-sfc@3.4.35': dependencies: - '@babel/parser': 7.25.3 + '@babel/parser': 7.25.8 '@vue/compiler-core': 3.4.35 '@vue/compiler-dom': 3.4.35 '@vue/compiler-ssr': 3.4.35 @@ -4392,7 +4384,7 @@ snapshots: optionalDependencies: '@typescript-eslint/eslint-plugin': 8.0.1(@typescript-eslint/parser@8.0.1(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4) - eslint-plugin-vitest@0.5.4(@typescript-eslint/eslint-plugin@8.0.1(@typescript-eslint/parser@8.0.1(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4)(vitest@2.1.3(@types/node@22.1.0)(@vitest/browser@2.1.3)(msw@2.3.5(typescript@5.5.4))): + eslint-plugin-vitest@0.5.4(@typescript-eslint/eslint-plugin@8.0.1(@typescript-eslint/parser@8.0.1(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4))(eslint@9.8.0)(typescript@5.5.4)(vitest@2.1.3): dependencies: '@typescript-eslint/utils': 7.18.0(eslint@9.8.0)(typescript@5.5.4) eslint: 9.8.0 @@ -4848,10 +4840,6 @@ snapshots: lodash@4.17.21: {} - loose-envify@1.4.0: - dependencies: - js-tokens: 4.0.0 - loupe@3.1.1: dependencies: get-func-name: 2.0.2 @@ -5190,19 +5178,16 @@ snapshots: defu: 6.1.4 destr: 2.0.3 - react-dom@18.3.1(react@18.3.1): + react-dom@19.0.0(react@19.0.0): dependencies: - loose-envify: 1.4.0 - react: 18.3.1 - scheduler: 0.23.2 + react: 19.0.0 + scheduler: 0.25.0 react-is@17.0.2: {} react-refresh@0.14.2: {} - react@18.3.1: - dependencies: - loose-envify: 1.4.0 + react@19.0.0: {} read-pkg-up@7.0.1: dependencies: @@ -5286,9 +5271,7 @@ snapshots: dependencies: queue-microtask: 1.2.3 - scheduler@0.23.2: - dependencies: - loose-envify: 1.4.0 + scheduler@0.25.0: {} scslre@0.3.0: dependencies: diff --git a/test/fixtures/SuspendedHelloWorld.tsx b/test/fixtures/SuspendedHelloWorld.tsx new file mode 100644 index 0000000..915516b --- /dev/null +++ b/test/fixtures/SuspendedHelloWorld.tsx @@ -0,0 +1,19 @@ +import { use } from 'react' + +let fakeCacheLoaded = false +const fakeCacheLoadPromise = new Promise((resolve) => { + setTimeout(() => { + fakeCacheLoaded = true + resolve() + }, 100) +}) + +export function SuspendedHelloWorld({ name }: { name: string }): React.ReactElement { + if (!fakeCacheLoaded) { + use(fakeCacheLoadPromise) + } + + return ( +
{`Hello ${name}`}
+ ) +} diff --git a/test/render.test.tsx b/test/render.test.tsx index 5a7360e..842de25 100644 --- a/test/render.test.tsx +++ b/test/render.test.tsx @@ -4,6 +4,7 @@ import { page } from '@vitest/browser/context' import { render } from '../src/index' import { HelloWorld } from './fixtures/HelloWorld' import { Counter } from './fixtures/Counter' +import { SuspendedHelloWorld } from './fixtures/SuspendedHelloWorld' test('renders simple component', async () => { const screen = await render() @@ -19,11 +20,12 @@ test('renders counter', async () => { await expect.element(screen.getByText('Count is 2')).toBeVisible() }) -test('renders child component on mount for suspense boundary which is not suspending', async () => { - const { getByText } = await render(, { +test('waits for suspended boundaries', async () => { + const { getByText } = await render(, { wrapper: ({ children }) => ( Suspended!}>{children} ), }) + await expect.element(getByText('Suspended!')).toBeInTheDocument() await expect.element(getByText('Hello Vitest')).toBeInTheDocument() })