Skip to content

Commit 090958a

Browse files
committed
first bunch of tests
1 parent 3983beb commit 090958a

File tree

10 files changed

+475
-7
lines changed

10 files changed

+475
-7
lines changed

jest.config.cjs

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const {jest: jestConfig} = require('kcd-scripts/config')
2+
3+
module.exports = Object.assign(jestConfig, {
4+
resolver: 'ts-jest-resolver',
5+
prettierPath: null,
6+
})

package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"devDependencies": {
5757
"@arethetypeswrong/cli": "^0.16.4",
5858
"@jest/globals": "^29.7.0",
59+
"@testing-library/user-event": "^14.5.2",
5960
"@tsconfig/recommended": "^1.0.7",
6061
"@types/jsdom": "^21.1.7",
6162
"@types/react": "^18",
@@ -67,6 +68,8 @@
6768
"publint": "^0.2.11",
6869
"react": "^18.3.1",
6970
"react-dom": "^18.3.1",
71+
"react-error-boundary": "^4.0.13",
72+
"ts-jest-resolver": "^2.0.1",
7073
"tsup": "^8.3.0",
7174
"typescript": "^5.6.2"
7275
},
@@ -82,7 +85,7 @@
8285
"prepack": "yarn build",
8386
"format": "kcd-scripts format",
8487
"lint": "kcd-scripts lint --config .eslintrc.cjs",
85-
"test": "kcd-scripts test --passWithNoTests",
88+
"test": "kcd-scripts test --config jest.config.cjs",
8689
"verify": "attw --pack . && publint",
8790
"typecheck": "kcd-scripts typecheck --build",
8891
"validate": "CI=true kcd-scripts validate verify,lint,typecheck,test"

src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ export type {
66
} from './renderStream/createRenderStream.js'
77
export {
88
createRenderStream,
9-
useTrackRenders,
109
WaitForRenderTimeoutError,
1110
} from './renderStream/createRenderStream.js'
11+
export {useTrackRenders} from './renderStream/useTrackRenders.js'
1212

1313
export type {SyncScreen} from './renderStream/Render.js'
1414

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/* eslint-disable default-case */
2+
/* eslint-disable consistent-return */
3+
function isStatefulPromise(promise) {
4+
return 'status' in promise
5+
}
6+
function wrapPromiseWithState(promise) {
7+
if (isStatefulPromise(promise)) {
8+
return promise
9+
}
10+
const pendingPromise = promise
11+
pendingPromise.status = 'pending'
12+
pendingPromise.then(
13+
value => {
14+
if (pendingPromise.status === 'pending') {
15+
const fulfilledPromise = pendingPromise
16+
fulfilledPromise.status = 'fulfilled'
17+
fulfilledPromise.value = value
18+
}
19+
},
20+
reason => {
21+
if (pendingPromise.status === 'pending') {
22+
const rejectedPromise = pendingPromise
23+
rejectedPromise.status = 'rejected'
24+
rejectedPromise.reason = reason
25+
}
26+
},
27+
)
28+
return promise
29+
}
30+
31+
/**
32+
* @template T
33+
* @param {Promise<T>} promise
34+
* @returns {T}
35+
*/
36+
export function __use(promise) {
37+
const statefulPromise = wrapPromiseWithState(promise)
38+
switch (statefulPromise.status) {
39+
case 'pending':
40+
throw statefulPromise
41+
case 'rejected':
42+
throw statefulPromise.reason
43+
case 'fulfilled':
44+
return statefulPromise.value
45+
}
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/* eslint-disable @typescript-eslint/no-use-before-define */
2+
import {jest, describe, test, expect} from '@jest/globals'
3+
import {createRenderStream} from '@testing-library/react-render-stream'
4+
import {userEvent} from '@testing-library/user-event'
5+
import * as React from 'react'
6+
import {ErrorBoundary} from 'react-error-boundary'
7+
8+
function CounterForm({
9+
value,
10+
onIncrement,
11+
}: {
12+
value: number
13+
onIncrement: () => void
14+
}) {
15+
return (
16+
<form>
17+
<button type="button" onClick={() => onIncrement()}>
18+
Increment
19+
</button>
20+
<label>
21+
Value
22+
<input type="number" value={value} readOnly />
23+
</label>
24+
</form>
25+
)
26+
}
27+
28+
describe('snapshotDOM', () => {
29+
test('basic functionality', async () => {
30+
function Counter() {
31+
const [value, setValue] = React.useState(0)
32+
return (
33+
<CounterForm value={value} onIncrement={() => setValue(v => v + 1)} />
34+
)
35+
}
36+
37+
const {takeRender, render} = createRenderStream({
38+
snapshotDOM: true,
39+
})
40+
const utils = render(<Counter />)
41+
const incrementButton = utils.getByText('Increment')
42+
await userEvent.click(incrementButton)
43+
await userEvent.click(incrementButton)
44+
{
45+
const {withinDOM} = await takeRender()
46+
const input = withinDOM().getByLabelText<HTMLInputElement>('Value')
47+
expect(input.value).toBe('0')
48+
}
49+
{
50+
const {withinDOM} = await takeRender()
51+
const input = withinDOM().getByLabelText<HTMLInputElement>('Value')
52+
expect(input.value).toBe('1')
53+
}
54+
{
55+
const {withinDOM} = await takeRender()
56+
const input = withinDOM().getByLabelText<HTMLInputElement>('Value')
57+
expect(input.value).toBe('2')
58+
}
59+
})
60+
61+
test('errors when triggering events on rendered elemenst', async () => {
62+
function Counter() {
63+
const [value, setValue] = React.useState(0)
64+
return (
65+
<CounterForm value={value} onIncrement={() => setValue(v => v + 1)} />
66+
)
67+
}
68+
69+
const {takeRender, render} = createRenderStream({
70+
snapshotDOM: true,
71+
})
72+
render(<Counter />)
73+
{
74+
const {withinDOM} = await takeRender()
75+
const snapshotIncrementButton = withinDOM().getByText('Increment')
76+
try {
77+
await userEvent.click(snapshotIncrementButton)
78+
} catch (error) {
79+
expect(error).toMatchInlineSnapshot(`
80+
[Error: Uncaught [Error:
81+
DOM interaction with a snapshot detected in test.
82+
Please don't interact with the DOM you get from \`withinDOM\`,
83+
but still use \`screen\` to get elements for simulating user interaction.
84+
]]
85+
`)
86+
}
87+
}
88+
})
89+
})
90+
91+
describe('replaceSnapshot', () => {
92+
test('basic functionality', async () => {
93+
function Counter() {
94+
const [value, setValue] = React.useState(0)
95+
replaceSnapshot({value})
96+
return (
97+
<CounterForm value={value} onIncrement={() => setValue(v => v + 1)} />
98+
)
99+
}
100+
101+
const {takeRender, replaceSnapshot, render} = createRenderStream<{
102+
value: number
103+
}>()
104+
const utils = render(<Counter />)
105+
const incrementButton = utils.getByText('Increment')
106+
await userEvent.click(incrementButton)
107+
await userEvent.click(incrementButton)
108+
{
109+
const {snapshot} = await takeRender()
110+
expect(snapshot).toEqual({value: 0})
111+
}
112+
{
113+
const {snapshot} = await takeRender()
114+
expect(snapshot).toEqual({value: 1})
115+
}
116+
{
117+
const {snapshot} = await takeRender()
118+
expect(snapshot).toEqual({value: 2})
119+
}
120+
})
121+
describe('callback notation', () => {
122+
test('basic functionality', async () => {
123+
function Counter() {
124+
const [value, setValue] = React.useState(0)
125+
replaceSnapshot(oldSnapshot => ({...oldSnapshot, value}))
126+
return (
127+
<CounterForm value={value} onIncrement={() => setValue(v => v + 1)} />
128+
)
129+
}
130+
131+
const {takeRender, replaceSnapshot, render} = createRenderStream({
132+
initialSnapshot: {unrelatedValue: 'unrelated', value: -1},
133+
})
134+
const utils = render(<Counter />)
135+
const incrementButton = utils.getByText('Increment')
136+
await userEvent.click(incrementButton)
137+
await userEvent.click(incrementButton)
138+
{
139+
const {snapshot} = await takeRender()
140+
expect(snapshot).toEqual({unrelatedValue: 'unrelated', value: 0})
141+
}
142+
{
143+
const {snapshot} = await takeRender()
144+
expect(snapshot).toEqual({unrelatedValue: 'unrelated', value: 1})
145+
}
146+
{
147+
const {snapshot} = await takeRender()
148+
expect(snapshot).toEqual({unrelatedValue: 'unrelated', value: 2})
149+
}
150+
})
151+
test('requires initialSnapshot', async () => {
152+
function Counter() {
153+
const [value, setValue] = React.useState(0)
154+
replaceSnapshot(() => ({value}))
155+
return (
156+
<CounterForm value={value} onIncrement={() => setValue(v => v + 1)} />
157+
)
158+
}
159+
160+
const {replaceSnapshot, render} = createRenderStream<{
161+
value: number
162+
}>()
163+
let caughtError: Error
164+
165+
const spy = jest.spyOn(console, 'error')
166+
spy.mockImplementation(() => {})
167+
render(
168+
<ErrorBoundary
169+
fallbackRender={({error}) => {
170+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
171+
caughtError = error
172+
return null
173+
}}
174+
>
175+
<Counter />
176+
</ErrorBoundary>,
177+
)
178+
spy.mockRestore()
179+
180+
expect(caughtError!).toMatchInlineSnapshot(
181+
`[Error: Cannot use a function to update the snapshot if no initial snapshot was provided.]`,
182+
)
183+
})
184+
})
185+
})

0 commit comments

Comments
 (0)