Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

apply queries to DOM snapshots, add generic parameter #3

Merged
merged 3 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/__tests__/renderToRenderStream.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<Component />,
{
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
4 changes: 2 additions & 2 deletions src/assertable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ export const assertableSymbol = Symbol.for(
```
*/
export type Assertable = {
[assertableSymbol]: RenderStream<any>
[assertableSymbol]: RenderStream<any, any>
}

export function markAssertable<T extends {}>(
assertable: T,
stream: RenderStream<any>,
stream: RenderStream<any, any>,
): T & Assertable {
return Object.assign(assertable, {
[assertableSymbol]: stream,
Expand Down
4 changes: 2 additions & 2 deletions src/renderHookToSnapshotStream.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export interface SnapshotStream<Snapshot, Props> 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}
>
/**
Expand Down Expand Up @@ -49,7 +49,7 @@ export function renderHookToSnapshotStream<ReturnValue, Props>(
renderCallback: (props: Props) => ReturnValue,
{initialProps, ...renderOptions}: RenderHookOptions<Props> = {},
): SnapshotStream<ReturnValue, Props> {
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)})
Expand Down
70 changes: 34 additions & 36 deletions src/renderStream/Render.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -25,18 +19,11 @@ export interface BaseRender {
count: number
}

type Screen = typeof screen
export type SyncScreen<Q extends Queries = SyncQueries> =
BoundSyncFunctions<Q> & Pick<Screen, 'debug' | 'logTestingPlaygroundURL'>

export type SyncScreen = {
[K in keyof Screen]: K extends `find${string}`
? {
/** @deprecated A snapshot is static, so avoid async queries! */
(...args: Parameters<Screen[K]>): ReturnType<Screen[K]>
}
: Screen[K]
}

export interface Render<Snapshot> extends BaseRender {
export interface Render<Snapshot, Q extends Queries = SyncQueries>
extends BaseRender {
/**
* The snapshot, as returned by the `takeSnapshot` option of `createRenderStream`.
*/
Expand All @@ -57,12 +44,14 @@ export interface Render<Snapshot> extends BaseRender {
* +expect(withinDOM().getByText("foo")).toBeInTheDocument();
* ```
*/
withinDOM: () => SyncScreen
withinDOM: () => SyncScreen<Q>

renderedComponents: Array<string | React.ComponentType>
}

export class RenderInstance<Snapshot> implements Render<Snapshot> {
export class RenderInstance<Snapshot, Q extends Queries = SyncQueries>
implements Render<Snapshot, Q>
{
id: string
phase: 'mount' | 'update' | 'nested-update'
actualDuration: number
Expand All @@ -73,12 +62,14 @@ export class RenderInstance<Snapshot> implements Render<Snapshot> {
public snapshot: Snapshot
private stringifiedDOM: string | undefined
public renderedComponents: Array<string | React.ComponentType>
private queries: Q

constructor(
baseRender: BaseRender,
snapshot: Snapshot,
stringifiedDOM: string | undefined,
renderedComponents: Array<string | React.ComponentType>,
queries: Q,
) {
this.snapshot = snapshot
this.stringifiedDOM = stringifiedDOM
Expand All @@ -90,6 +81,7 @@ export class RenderInstance<Snapshot> implements Render<Snapshot> {
this.startTime = baseRender.startTime
this.commitTime = baseRender.commitTime
this.count = baseRender.count
this.queries = queries
}

private _domSnapshot: HTMLElement | undefined
Expand Down Expand Up @@ -124,17 +116,23 @@ export class RenderInstance<Snapshot> implements Render<Snapshot> {
return (this._domSnapshot = body)
}

get withinDOM(): () => SyncScreen {
const snapScreen = Object.assign(within(this.domSnapshot), {
debug: (
...[dom = this.domSnapshot, ...rest]: Parameters<typeof screen.debug>
) => screen.debug(dom, ...rest),
logTestingPlaygroundURL: (
...[dom = this.domSnapshot, ...rest]: Parameters<
typeof screen.logTestingPlaygroundURL
>
) => screen.logTestingPlaygroundURL(dom, ...rest),
})
get withinDOM(): () => SyncScreen<Q> {
const snapScreen = Object.assign(
getQueriesForElement<Q>(
this.domSnapshot,
this.queries,
) as any as BoundSyncFunctions<Q>,
{
debug: (
...[dom = this.domSnapshot, ...rest]: Parameters<typeof screen.debug>
) => screen.debug(dom, ...rest),
logTestingPlaygroundURL: (
...[dom = this.domSnapshot, ...rest]: Parameters<
typeof screen.logTestingPlaygroundURL
>
) => screen.logTestingPlaygroundURL(dom, ...rest),
},
)
return () => snapScreen
}
}
Expand Down
26 changes: 26 additions & 0 deletions src/renderStream/__tests__/createRenderStream.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<Component />)

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', () => {
Expand Down
58 changes: 40 additions & 18 deletions src/renderStream/createRenderStream.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -29,7 +30,10 @@ interface MergeSnapshot<Snapshot> {
): void
}

export interface RenderStream<Snapshot extends ValidSnapshot> {
export interface RenderStream<
Snapshot extends ValidSnapshot,
Q extends Queries = SyncQueries,
> {
// Allows for partial updating of the snapshot by shallow merging the results
mergeSnapshot: MergeSnapshot<Snapshot>
// Performs a full replacement of the snapshot
Expand All @@ -39,21 +43,22 @@ export interface RenderStream<Snapshot extends ValidSnapshot> {
* Errors thrown during component render will be captured here, too.
*/
renders: Array<
Render<Snapshot> | {phase: 'snapshotError'; count: number; error: unknown}
| Render<Snapshot, Q>
| {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<Render<Snapshot>>
peekRender: (options?: NextRenderOptions) => Promise<Render<Snapshot, Q>>
/**
* 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<Render<Snapshot>>)
((options?: NextRenderOptions) => Promise<Render<Snapshot, Q>>)
/**
* Returns the total number of renders.
*/
Expand All @@ -62,20 +67,27 @@ export interface RenderStream<Snapshot extends ValidSnapshot> {
* Returns the current render.
* @throws {Error} if no render has happened yet
*/
getCurrentRender: () => Render<Snapshot>
getCurrentRender: () => Render<Snapshot, Q>
/**
* Waits for the next render to happen.
* Does not advance the render iterator.
*/
waitForNextRender: (options?: NextRenderOptions) => Promise<Render<Snapshot>>
waitForNextRender: (
options?: NextRenderOptions,
) => Promise<Render<Snapshot, Q>>
}

export interface RenderStreamWithRenderFn<Snapshot extends ValidSnapshot>
extends RenderStream<Snapshot> {
export interface RenderStreamWithRenderFn<
Snapshot extends ValidSnapshot,
Q extends Queries = SyncQueries,
> extends RenderStream<Snapshot, Q> {
render: typeof baseRender
}

export type RenderStreamOptions<Snapshot extends ValidSnapshot> = {
export type RenderStreamOptions<
Snapshot extends ValidSnapshot,
Q extends Queries = SyncQueries,
> = {
onRender?: (
info: BaseRender & {
snapshot: Snapshot
Expand All @@ -90,6 +102,7 @@ export type RenderStreamOptions<Snapshot extends ValidSnapshot> = {
* `useTrackRenders` occured.
*/
skipNonTrackingRenders?: boolean
queries?: Q
}

export class WaitForRenderTimeoutError extends Error {
Expand All @@ -100,19 +113,26 @@ export class WaitForRenderTimeoutError extends Error {
}
}

export function createRenderStream<Snapshot extends ValidSnapshot = void>({
export function createRenderStream<
Snapshot extends ValidSnapshot = void,
Q extends Queries = SyncQueries,
>({
onRender,
snapshotDOM = false,
initialSnapshot,
skipNonTrackingRenders,
}: RenderStreamOptions<Snapshot> = {}): RenderStreamWithRenderFn<Snapshot> {
queries = syncQueries as any as Q,
}: RenderStreamOptions<Snapshot, Q> = {}): 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<Snapshot>
const stream = {} as any as RenderStreamWithRenderFn<Snapshot, Q>

let nextRender: Promise<Render<Snapshot>> | undefined,
resolveNextRender: ((render: Render<Snapshot>) => void) | undefined,
let nextRender: Promise<Render<Snapshot, Q>> | undefined,
resolveNextRender: ((render: Render<Snapshot, Q>) => void) | undefined,
rejectNextRender: ((error: unknown) => void) | undefined
function resetNextRender() {
nextRender = undefined
Expand Down Expand Up @@ -199,6 +219,7 @@ export function createRenderStream<Snapshot extends ValidSnapshot = void>({
snapshot,
domSnapshot,
renderStreamContext.renderedComponents,
queries,
)
renderStreamContext.renderedComponents = []
stream.renders.push(render)
Expand Down Expand Up @@ -247,7 +268,8 @@ export function createRenderStream<Snapshot extends ValidSnapshot = void>({
replaceSnapshot,
mergeSnapshot,
renders: new Array<
Render<Snapshot> | {phase: 'snapshotError'; count: number; error: unknown}
| Render<Snapshot, Q>
| {phase: 'snapshotError'; count: number; error: unknown}
>(),
totalRenderCount() {
return stream.renders.length
Expand Down Expand Up @@ -316,12 +338,12 @@ export function createRenderStream<Snapshot extends ValidSnapshot = void>({
},
waitForNextRender({timeout = 1000}: NextRenderOptions = {}) {
if (!nextRender) {
nextRender = Promise.race<Render<Snapshot>>([
new Promise<Render<Snapshot>>((resolve, reject) => {
nextRender = Promise.race<Render<Snapshot, Q>>([
new Promise<Render<Snapshot, Q>>((resolve, reject) => {
resolveNextRender = resolve
rejectNextRender = reject
}),
new Promise<Render<Snapshot>>((_, reject) =>
new Promise<Render<Snapshot, Q>>((_, reject) =>
setTimeout(() => {
const error = new WaitForRenderTimeoutError()
Error.captureStackTrace(error, stream.waitForNextRender)
Expand Down
Loading