diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9bc937e29..3ec3a6626 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,11 +56,26 @@ jobs: uses: ./.github/actions/setup-deps - name: Test - run: yarn test:ci + run: yarn test:ci:coverage - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 + + test-concurrent: + needs: [install-cache-deps] + runs-on: ubuntu-latest + name: Test (concurrent mode) + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js and deps + uses: ./.github/actions/setup-deps + + - name: Test in concurrent mode + run: CONCURRENT_MODE=1 yarn test:ci + test-website: runs-on: ubuntu-latest name: Test Website diff --git a/jest-setup.ts b/jest-setup.ts index a4d893a18..9ed60181d 100644 --- a/jest-setup.ts +++ b/jest-setup.ts @@ -1,6 +1,9 @@ -import { resetToDefaults } from './src/pure'; +import { resetToDefaults, configure } from './src/pure'; import './src/matchers/extend-expect'; beforeEach(() => { resetToDefaults(); + if (process.env.CONCURRENT_MODE === '1') { + configure({ concurrentRoot: true }); + } }); diff --git a/package.json b/package.json index f3ab481d1..dfce46bef 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "scripts": { "clean": "del build", "test": "jest", - "test:ci": "jest --maxWorkers=2 --collectCoverage=true --coverage-provider=v8", + "test:ci": "jest --maxWorkers=2", + "test:ci:coverage": "jest --maxWorkers=2 --collectCoverage=true --coverage-provider=v8", "typecheck": "tsc", "copy-flowtypes": "cp typings/index.flow.js build", "lint": "eslint src --cache", diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index aca62f304..b3d2a7ed1 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -1,5 +1,9 @@ import { getConfig, configure, resetToDefaults, configureInternal } from '../config'; +beforeEach(() => { + resetToDefaults(); +}); + test('getConfig() returns existing configuration', () => { expect(getConfig().asyncUtilTimeout).toEqual(1000); expect(getConfig().defaultIncludeHiddenElements).toEqual(false); @@ -12,6 +16,7 @@ test('configure() overrides existing config values', () => { asyncUtilTimeout: 5000, defaultDebugOptions: { message: 'debug message' }, defaultIncludeHiddenElements: false, + concurrentRoot: false, }); }); diff --git a/src/__tests__/render.test.tsx b/src/__tests__/render.test.tsx index 0c09b3904..3127963d7 100644 --- a/src/__tests__/render.test.tsx +++ b/src/__tests__/render.test.tsx @@ -241,3 +241,13 @@ test('render calls detects host component names', () => { render(); expect(getConfig().hostComponentNames).not.toBeUndefined(); }); + +test('supports legacy rendering', () => { + render(, { concurrentRoot: false }); + expect(screen.root).toBeDefined(); +}); + +test('supports concurrent rendering', () => { + render(, { concurrentRoot: true }); + expect(screen.root).toBeDefined(); +}); diff --git a/src/config.ts b/src/config.ts index c343a3e15..388933cdd 100644 --- a/src/config.ts +++ b/src/config.ts @@ -13,6 +13,12 @@ export type Config = { /** Default options for `debug` helper. */ defaultDebugOptions?: Partial; + + /** + * Set to `true` to enable concurrent rendering. + * Otherwise `render` will default to legacy synchronous rendering. + */ + concurrentRoot: boolean; }; export type ConfigAliasOptions = { @@ -37,6 +43,7 @@ export type InternalConfig = Config & { const defaultConfig: InternalConfig = { asyncUtilTimeout: 1000, defaultIncludeHiddenElements: false, + concurrentRoot: false, }; let config = { ...defaultConfig }; diff --git a/src/render.tsx b/src/render.tsx index 5f31dcb2a..e4a6e22e4 100644 --- a/src/render.tsx +++ b/src/render.tsx @@ -1,4 +1,8 @@ -import type { ReactTestInstance, ReactTestRenderer } from 'react-test-renderer'; +import type { + ReactTestInstance, + ReactTestRenderer, + TestRendererOptions, +} from 'react-test-renderer'; import * as React from 'react'; import { Profiler } from 'react'; import act from './act'; @@ -14,7 +18,18 @@ import { setRenderResult } from './screen'; import { getQueriesForElement } from './within'; export interface RenderOptions { + /** + * Pass a React Component as the wrapper option to have it rendered around the inner element. This is most useful for creating + * reusable custom render functions for common data providers. + */ wrapper?: React.ComponentType; + + /** + * Set to `true` to enable concurrent rendering. + * Otherwise `render` will default to legacy synchronous rendering. + */ + concurrentRoot?: boolean | undefined; + createNodeMock?: (element: React.ReactElement) => unknown; unstable_validateStringsRenderedWithinText?: boolean; } @@ -39,11 +54,18 @@ export function renderInternal( ) { const { wrapper: Wrapper, + concurrentRoot, detectHostComponentNames = true, unstable_validateStringsRenderedWithinText, - ...testRendererOptions + ...rest } = options || {}; + const testRendererOptions: TestRendererOptions = { + ...rest, + // @ts-expect-error incomplete typing on RTR package + unstable_isConcurrent: concurrentRoot ?? getConfig().concurrentRoot, + }; + if (detectHostComponentNames) { configureHostComponentNamesIfNeeded(); } diff --git a/website/docs/12.x/docs/api/misc/config.mdx b/website/docs/12.x/docs/api/misc/config.mdx index 049e82760..bdaabd107 100644 --- a/website/docs/12.x/docs/api/misc/config.mdx +++ b/website/docs/12.x/docs/api/misc/config.mdx @@ -7,6 +7,7 @@ type Config = { asyncUtilTimeout: number; defaultHidden: boolean; defaultDebugOptions: Partial; + concurrentRoot: boolean; }; function configure(options: Partial) {} @@ -26,6 +27,10 @@ This option is also available as `defaultHidden` alias for compatibility with [R Default [debug options](#debug) to be used when calling `debug()`. These default options will be overridden by the ones you specify directly when calling `debug()`. +### `concurrentRoot` option + +Set to `true` to enable concurrent rendering used in the React Native New Architecture. Otherwise `render` will default to legacy synchronous rendering. + ## `resetToDefaults()` ```ts diff --git a/website/docs/12.x/docs/api/render.mdx b/website/docs/12.x/docs/api/render.mdx index 0763e1dfd..ee38878ff 100644 --- a/website/docs/12.x/docs/api/render.mdx +++ b/website/docs/12.x/docs/api/render.mdx @@ -32,6 +32,10 @@ wrapper?: React.ComponentType, This option allows you to wrap the tested component, passed as the first option to the `render()` function, in an additional wrapper component. This is useful for creating reusable custom render functions for common React Context providers. +#### `concurrentRoot` option + +Set to `true` to enable concurrent rendering used in the React Native New Architecture. Otherwise `render` will default to legacy synchronous rendering. + #### `createNodeMock` option ```ts