diff --git a/src/__tests__/renderToRenderStream.test.tsx b/src/__tests__/renderToRenderStream.test.tsx
index e224944fd..195ed6ad4 100644
--- a/src/__tests__/renderToRenderStream.test.tsx
+++ b/src/__tests__/renderToRenderStream.test.tsx
@@ -58,6 +58,37 @@ describe('snapshotDOM', () => {
expect(input.value).toBe('2')
}
})
+
+ test('queries option', async () => {
+ function Component() {
+ return null
+ }
+ const queries = {
+ foo: (_: any) => {
+ return null
+ },
+ }
+ const {takeRender, renderResultPromise} = renderToRenderStream(
+ ,
+ {
+ queries,
+ snapshotDOM: true,
+ },
+ )
+ const utils = await renderResultPromise
+ expect(utils.foo()).toBe(null)
+ const {withinDOM} = await takeRender()
+ expect(withinDOM().foo()).toBe(null)
+ function _typeTest() {
+ // @ts-expect-error should not be present
+ utils.getByText
+ // @ts-expect-error should not be present
+ withinDOM().getByText
+ utils.debug()
+ withinDOM().debug()
+ const _str: string = withinDOM().logTestingPlaygroundURL()
+ }
+ })
})
// for more tests, see the `createRenderStream` test suite, as `renderToRenderStream` is just a wrapper around that
diff --git a/src/assertable.ts b/src/assertable.ts
index b7de2d222..fdafef904 100644
--- a/src/assertable.ts
+++ b/src/assertable.ts
@@ -13,12 +13,12 @@ export const assertableSymbol = Symbol.for(
```
*/
export type Assertable = {
- [assertableSymbol]: RenderStream
+ [assertableSymbol]: RenderStream
}
export function markAssertable(
assertable: T,
- stream: RenderStream,
+ stream: RenderStream,
): T & Assertable {
return Object.assign(assertable, {
[assertableSymbol]: stream,
diff --git a/src/renderHookToSnapshotStream.tsx b/src/renderHookToSnapshotStream.tsx
index b1eb48874..3d8f6edb7 100644
--- a/src/renderHookToSnapshotStream.tsx
+++ b/src/renderHookToSnapshotStream.tsx
@@ -11,7 +11,7 @@ export interface SnapshotStream extends Assertable {
* Errors thrown during component render will be captured here, too.
*/
renders: Array<
- | Render<{value: Snapshot}>
+ | Render<{value: Snapshot}, never>
| {phase: 'snapshotError'; count: number; error: unknown}
>
/**
@@ -49,7 +49,7 @@ export function renderHookToSnapshotStream(
renderCallback: (props: Props) => ReturnValue,
{initialProps, ...renderOptions}: RenderHookOptions = {},
): SnapshotStream {
- const {render, ...stream} = createRenderStream<{value: ReturnValue}>()
+ const {render, ...stream} = createRenderStream<{value: ReturnValue}, never>()
const HookComponent: React.FC<{arg: Props}> = props => {
stream.replaceSnapshot({value: renderCallback(props.arg)})
diff --git a/src/renderStream/Render.tsx b/src/renderStream/Render.tsx
index 078e85654..456fbc699 100644
--- a/src/renderStream/Render.tsx
+++ b/src/renderStream/Render.tsx
@@ -1,16 +1,10 @@
-/* istanbul ignore file */
-
-/*
-Something in this file does not compile correctly while measuring code coverage
-and will lead to a
- Uncaught [ReferenceError: cov_1zb8w312au is not defined]
-if we do not ignore this file in code coverage.
-
-As we only use this file in our internal tests, we can safely ignore it.
-*/
-
-import {within, screen} from '@testing-library/dom'
+import {screen, getQueriesForElement, Screen} from '@testing-library/dom'
import {JSDOM, VirtualConsole} from 'jsdom'
+import {
+ BoundSyncFunctions,
+ type Queries,
+ type SyncQueries,
+} from './syncQueries.js'
export interface BaseRender {
id: string
@@ -25,18 +19,11 @@ export interface BaseRender {
count: number
}
-type Screen = typeof screen
+export type SyncScreen =
+ BoundSyncFunctions & Pick
-export type SyncScreen = {
- [K in keyof Screen]: K extends `find${string}`
- ? {
- /** @deprecated A snapshot is static, so avoid async queries! */
- (...args: Parameters): ReturnType
- }
- : Screen[K]
-}
-
-export interface Render extends BaseRender {
+export interface Render
+ extends BaseRender {
/**
* The snapshot, as returned by the `takeSnapshot` option of `createRenderStream`.
*/
@@ -57,12 +44,14 @@ export interface Render extends BaseRender {
* +expect(withinDOM().getByText("foo")).toBeInTheDocument();
* ```
*/
- withinDOM: () => SyncScreen
+ withinDOM: () => SyncScreen
renderedComponents: Array
}
-export class RenderInstance implements Render {
+export class RenderInstance
+ implements Render
+{
id: string
phase: 'mount' | 'update' | 'nested-update'
actualDuration: number
@@ -73,12 +62,14 @@ export class RenderInstance implements Render {
public snapshot: Snapshot
private stringifiedDOM: string | undefined
public renderedComponents: Array
+ private queries: Q
constructor(
baseRender: BaseRender,
snapshot: Snapshot,
stringifiedDOM: string | undefined,
renderedComponents: Array,
+ queries: Q,
) {
this.snapshot = snapshot
this.stringifiedDOM = stringifiedDOM
@@ -90,6 +81,7 @@ export class RenderInstance implements Render {
this.startTime = baseRender.startTime
this.commitTime = baseRender.commitTime
this.count = baseRender.count
+ this.queries = queries
}
private _domSnapshot: HTMLElement | undefined
@@ -124,17 +116,23 @@ export class RenderInstance implements Render {
return (this._domSnapshot = body)
}
- get withinDOM(): () => SyncScreen {
- const snapScreen = Object.assign(within(this.domSnapshot), {
- debug: (
- ...[dom = this.domSnapshot, ...rest]: Parameters
- ) => screen.debug(dom, ...rest),
- logTestingPlaygroundURL: (
- ...[dom = this.domSnapshot, ...rest]: Parameters<
- typeof screen.logTestingPlaygroundURL
- >
- ) => screen.logTestingPlaygroundURL(dom, ...rest),
- })
+ get withinDOM(): () => SyncScreen {
+ const snapScreen = Object.assign(
+ getQueriesForElement(
+ this.domSnapshot,
+ this.queries,
+ ) as any as BoundSyncFunctions,
+ {
+ debug: (
+ ...[dom = this.domSnapshot, ...rest]: Parameters
+ ) => screen.debug(dom, ...rest),
+ logTestingPlaygroundURL: (
+ ...[dom = this.domSnapshot, ...rest]: Parameters<
+ typeof screen.logTestingPlaygroundURL
+ >
+ ) => screen.logTestingPlaygroundURL(dom, ...rest),
+ },
+ )
return () => snapScreen
}
}
diff --git a/src/renderStream/__tests__/createRenderStream.test.tsx b/src/renderStream/__tests__/createRenderStream.test.tsx
index 5c7fc6fb5..3218f5406 100644
--- a/src/renderStream/__tests__/createRenderStream.test.tsx
+++ b/src/renderStream/__tests__/createRenderStream.test.tsx
@@ -87,6 +87,32 @@ describe('snapshotDOM', () => {
}
}
})
+
+ test('queries option', async () => {
+ function Component() {
+ return null
+ }
+ const queries = {
+ foo: (_: any) => {
+ return null
+ },
+ }
+
+ const {takeRender, render} = createRenderStream({
+ snapshotDOM: true,
+ queries,
+ })
+ render()
+
+ const {withinDOM} = await takeRender()
+ expect(withinDOM().foo()).toBe(null)
+ function _typeTest() {
+ // @ts-expect-error should not be present
+ withinDOM().getByText
+ withinDOM().debug()
+ const _str: string = withinDOM().logTestingPlaygroundURL()
+ }
+ })
})
describe('replaceSnapshot', () => {
diff --git a/src/renderStream/createRenderStream.tsx b/src/renderStream/createRenderStream.tsx
index d536cac34..2c87ffe46 100644
--- a/src/renderStream/createRenderStream.tsx
+++ b/src/renderStream/createRenderStream.tsx
@@ -6,6 +6,7 @@ import {RenderInstance, type Render, type BaseRender} from './Render.js'
import {type RenderStreamContextValue} from './context.js'
import {RenderStreamContextProvider} from './context.js'
import {disableActWarnings} from './disableActWarnings.js'
+import {syncQueries, type Queries, type SyncQueries} from './syncQueries.js'
export type ValidSnapshot =
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
@@ -29,7 +30,10 @@ interface MergeSnapshot {
): void
}
-export interface RenderStream {
+export interface RenderStream<
+ Snapshot extends ValidSnapshot,
+ Q extends Queries = SyncQueries,
+> {
// Allows for partial updating of the snapshot by shallow merging the results
mergeSnapshot: MergeSnapshot
// Performs a full replacement of the snapshot
@@ -39,21 +43,22 @@ export interface RenderStream {
* Errors thrown during component render will be captured here, too.
*/
renders: Array<
- Render | {phase: 'snapshotError'; count: number; error: unknown}
+ | Render
+ | {phase: 'snapshotError'; count: number; error: unknown}
>
/**
* Peeks the next render from the current iterator position, without advancing the iterator.
* If no render has happened yet, it will wait for the next render to happen.
* @throws {WaitForRenderTimeoutError} if no render happens within the timeout
*/
- peekRender: (options?: NextRenderOptions) => Promise>
+ peekRender: (options?: NextRenderOptions) => Promise>
/**
* Iterates to the next render and returns it.
* If no render has happened yet, it will wait for the next render to happen.
* @throws {WaitForRenderTimeoutError} if no render happens within the timeout
*/
takeRender: Assertable &
- ((options?: NextRenderOptions) => Promise>)
+ ((options?: NextRenderOptions) => Promise>)
/**
* Returns the total number of renders.
*/
@@ -62,20 +67,27 @@ export interface RenderStream {
* Returns the current render.
* @throws {Error} if no render has happened yet
*/
- getCurrentRender: () => Render
+ getCurrentRender: () => Render
/**
* Waits for the next render to happen.
* Does not advance the render iterator.
*/
- waitForNextRender: (options?: NextRenderOptions) => Promise>
+ waitForNextRender: (
+ options?: NextRenderOptions,
+ ) => Promise>
}
-export interface RenderStreamWithRenderFn
- extends RenderStream {
+export interface RenderStreamWithRenderFn<
+ Snapshot extends ValidSnapshot,
+ Q extends Queries = SyncQueries,
+> extends RenderStream {
render: typeof baseRender
}
-export type RenderStreamOptions = {
+export type RenderStreamOptions<
+ Snapshot extends ValidSnapshot,
+ Q extends Queries = SyncQueries,
+> = {
onRender?: (
info: BaseRender & {
snapshot: Snapshot
@@ -90,6 +102,7 @@ export type RenderStreamOptions = {
* `useTrackRenders` occured.
*/
skipNonTrackingRenders?: boolean
+ queries?: Q
}
export class WaitForRenderTimeoutError extends Error {
@@ -100,19 +113,26 @@ export class WaitForRenderTimeoutError extends Error {
}
}
-export function createRenderStream({
+export function createRenderStream<
+ Snapshot extends ValidSnapshot = void,
+ Q extends Queries = SyncQueries,
+>({
onRender,
snapshotDOM = false,
initialSnapshot,
skipNonTrackingRenders,
-}: RenderStreamOptions = {}): RenderStreamWithRenderFn {
+ queries = syncQueries as any as Q,
+}: RenderStreamOptions = {}): RenderStreamWithRenderFn<
+ Snapshot,
+ Q
+> {
// creating the object first and then assigning in all the properties
// allows keeping the object instance for reference while the members are
// created, which is important for the `markAssertable` function
- const stream = {} as any as RenderStreamWithRenderFn
+ const stream = {} as any as RenderStreamWithRenderFn
- let nextRender: Promise> | undefined,
- resolveNextRender: ((render: Render) => void) | undefined,
+ let nextRender: Promise> | undefined,
+ resolveNextRender: ((render: Render) => void) | undefined,
rejectNextRender: ((error: unknown) => void) | undefined
function resetNextRender() {
nextRender = undefined
@@ -199,6 +219,7 @@ export function createRenderStream({
snapshot,
domSnapshot,
renderStreamContext.renderedComponents,
+ queries,
)
renderStreamContext.renderedComponents = []
stream.renders.push(render)
@@ -247,7 +268,8 @@ export function createRenderStream({
replaceSnapshot,
mergeSnapshot,
renders: new Array<
- Render | {phase: 'snapshotError'; count: number; error: unknown}
+ | Render
+ | {phase: 'snapshotError'; count: number; error: unknown}
>(),
totalRenderCount() {
return stream.renders.length
@@ -316,12 +338,12 @@ export function createRenderStream({
},
waitForNextRender({timeout = 1000}: NextRenderOptions = {}) {
if (!nextRender) {
- nextRender = Promise.race>([
- new Promise>((resolve, reject) => {
+ nextRender = Promise.race>([
+ new Promise>((resolve, reject) => {
resolveNextRender = resolve
rejectNextRender = reject
}),
- new Promise>((_, reject) =>
+ new Promise>((_, reject) =>
setTimeout(() => {
const error = new WaitForRenderTimeoutError()
Error.captureStackTrace(error, stream.waitForNextRender)
diff --git a/src/renderStream/syncQueries.ts b/src/renderStream/syncQueries.ts
new file mode 100644
index 000000000..1055ddd90
--- /dev/null
+++ b/src/renderStream/syncQueries.ts
@@ -0,0 +1,135 @@
+import {queries, Query, Queries} from '@testing-library/dom'
+
+export {Query, Queries}
+
+type OriginalQueries = typeof queries
+
+export type SyncQueries = {
+ [K in keyof OriginalQueries as K extends `${'query'}${string}`
+ ? never
+ : K]: OriginalQueries[K]
+}
+
+export const syncQueries = Object.fromEntries(
+ Object.entries(queries).filter(
+ ([key]) => key.startsWith('get') || key.startsWith('find'),
+ ),
+) as any as SyncQueries
+
+export type BoundFunction = T extends (
+ container: HTMLElement,
+ ...args: infer P
+) => infer R
+ ? (...args: P) => R
+ : never
+
+export type BoundSyncFunctions = Q extends typeof syncQueries
+ ? {
+ getByLabelText(
+ ...args: Parameters>>
+ ): ReturnType>
+ getAllByLabelText(
+ ...args: Parameters>>
+ ): ReturnType>
+ findByLabelText(
+ ...args: Parameters>>
+ ): ReturnType>
+ findAllByLabelText(
+ ...args: Parameters>>
+ ): ReturnType>
+ getByPlaceholderText(
+ ...args: Parameters>>
+ ): ReturnType>
+ getAllByPlaceholderText(
+ ...args: Parameters>>
+ ): ReturnType>
+ findByPlaceholderText(
+ ...args: Parameters>>
+ ): ReturnType>
+ findAllByPlaceholderText(
+ ...args: Parameters>>
+ ): ReturnType>
+ getByText(
+ ...args: Parameters>>
+ ): ReturnType>
+ getAllByText(
+ ...args: Parameters>>
+ ): ReturnType>
+ findByText(
+ ...args: Parameters>>
+ ): ReturnType>
+ findAllByText(
+ ...args: Parameters>>
+ ): ReturnType>
+ getByAltText(
+ ...args: Parameters>>
+ ): ReturnType>
+ getAllByAltText(
+ ...args: Parameters>>
+ ): ReturnType>
+ findByAltText(
+ ...args: Parameters>>
+ ): ReturnType>
+ findAllByAltText(
+ ...args: Parameters>>
+ ): ReturnType>
+ getByTitle(
+ ...args: Parameters>>
+ ): ReturnType>
+ getAllByTitle(
+ ...args: Parameters>>
+ ): ReturnType>
+ findByTitle(
+ ...args: Parameters>>
+ ): ReturnType>
+ findAllByTitle(
+ ...args: Parameters>>
+ ): ReturnType>
+ getByDisplayValue(
+ ...args: Parameters>>
+ ): ReturnType>
+ getAllByDisplayValue(
+ ...args: Parameters>>
+ ): ReturnType>
+ findByDisplayValue(
+ ...args: Parameters>>
+ ): ReturnType>
+ findAllByDisplayValue(
+ ...args: Parameters>>
+ ): ReturnType>
+ getByRole(
+ ...args: Parameters>>
+ ): ReturnType>
+ getAllByRole(
+ ...args: Parameters>>
+ ): ReturnType>
+ findByRole(
+ ...args: Parameters>>
+ ): ReturnType>
+ findAllByRole(
+ ...args: Parameters>>
+ ): ReturnType>
+ getByTestId(
+ ...args: Parameters>>
+ ): ReturnType>
+ getAllByTestId(
+ ...args: Parameters>>
+ ): ReturnType>
+ queryByTestId(
+ ...args: Parameters>>
+ ): ReturnType>
+ queryAllByTestId(
+ ...args: Parameters>>
+ ): ReturnType>
+ findByTestId(
+ ...args: Parameters>>
+ ): ReturnType>
+ findAllByTestId(
+ ...args: Parameters>>
+ ): ReturnType>
+ } & {
+ [P in keyof Q]: BoundFunction
+ }
+ : {
+ [P in keyof Q]: BoundFunction
+ }
diff --git a/src/renderToRenderStream.ts b/src/renderToRenderStream.ts
index 34431d391..9692918f3 100644
--- a/src/renderToRenderStream.ts
+++ b/src/renderToRenderStream.ts
@@ -1,4 +1,5 @@
import {
+ Queries,
type RenderOptions as BaseOptions,
type RenderResult as BaseResult,
} from '@testing-library/react'
@@ -8,39 +9,49 @@ import {
type RenderStream,
type ValidSnapshot,
} from './renderStream/createRenderStream.js'
+import {SyncQueries} from './renderStream/syncQueries.js'
-type RenderOptions = BaseOptions &
- RenderStreamOptions
+type RenderOptions<
+ Snapshot extends ValidSnapshot = void,
+ Q extends Queries = SyncQueries,
+> = BaseOptions & RenderStreamOptions
export interface RenderStreamWithRenderResult<
Snapshot extends ValidSnapshot = void,
-> extends RenderStream {
- renderResultPromise: Promise
+ Q extends Queries = SyncQueries,
+> extends RenderStream {
+ renderResultPromise: Promise>
}
/**
* Render into a container which is appended to document.body. It should be used with cleanup.
*/
-export function renderToRenderStream(
+export function renderToRenderStream<
+ Snapshot extends ValidSnapshot = void,
+ Q extends Queries = SyncQueries,
+>(
ui: React.ReactNode,
- // TODO: add `queries`
{
onRender,
snapshotDOM,
initialSnapshot,
skipNonTrackingRenders,
+ queries,
...options
- }: RenderOptions = {},
-): RenderStreamWithRenderResult {
- const {render, ...stream} = createRenderStream({
+ }: RenderOptions = {},
+): RenderStreamWithRenderResult {
+ const {render, ...stream} = createRenderStream({
onRender,
snapshotDOM,
initialSnapshot,
skipNonTrackingRenders,
+ queries,
})
// `render` needs to be called asynchronously here, because the definition of `ui`
// might contain components that reference the return value of `renderToRenderStream`
// itself, e.g. `replaceSnapshot` or `mergeSnapshot`.
- const renderResultPromise = Promise.resolve().then(() => render(ui, options))
+ const renderResultPromise = Promise.resolve().then(() =>
+ render(ui, {...options, queries}),
+ )
return {...stream, renderResultPromise}
}