Skip to content

Commit cce10ef

Browse files
committed
more improved tests
1 parent 02bd183 commit cce10ef

File tree

11 files changed

+494
-40
lines changed

11 files changed

+494
-40
lines changed

exercises/06.rerenders/01.problem.memo/tests/memoized.test.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,3 @@ test('Only ListItem should not rerender when clicking force rerender', async ({
9191
'🚨 The ListItem component was rendered when clicking force render. Use the `memo` utility from React on the ListItem component to prevent this.',
9292
).not.toContain('ListItem')
9393
})
94-
95-
declare global {
96-
interface Window {
97-
__REACT_DEVTOOLS_GLOBAL_HOOK__?: any
98-
}
99-
}

exercises/06.rerenders/01.solution.memo/tests/memoized.test.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,3 @@ test('Only ListItem should not rerender when clicking force rerender', async ({
9191
'🚨 The ListItem component was rendered when clicking force render. Use the `memo` utility from React on the ListItem component to prevent this.',
9292
).not.toContain('ListItem')
9393
})
94-
95-
declare global {
96-
interface Window {
97-
__REACT_DEVTOOLS_GLOBAL_HOOK__?: any
98-
}
99-
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { test, expect } from '@playwright/test'
2+
3+
test('Only two ListItems should not rerender when the highlighted item changes', async ({
4+
page,
5+
}) => {
6+
await page.route('/', async (route) => {
7+
const request = route.request()
8+
if (request.resourceType() !== 'document') return route.continue()
9+
const response = await route.fetch()
10+
11+
let html = await response.text()
12+
const scriptToInject = `
13+
<script>
14+
let internals
15+
16+
function enhanceExistingHook(existingHook) {
17+
const originalInject = existingHook.inject
18+
19+
existingHook.inject = (injectedInternals) => {
20+
internals = injectedInternals
21+
22+
// Returning a number as React expects a renderer ID
23+
return originalInject?.call(existingHook, injectedInternals) ?? 1
24+
}
25+
26+
return existingHook
27+
}
28+
29+
function createMinimalHook() {
30+
return {
31+
renderers: [],
32+
supportsFiber: true,
33+
inject: (injectedInternals) => {
34+
internals = injectedInternals
35+
return 1 // Returning a number as React expects a renderer ID
36+
},
37+
onCommitFiberRoot: () => {},
38+
onCommitFiberUnmount: () => {},
39+
}
40+
}
41+
42+
async function getComponentCalls(cb) {
43+
const componentNames = []
44+
if (!internals) {
45+
throw new Error('🚨 React DevTools is not available')
46+
}
47+
48+
internals.enableProfilerTimer = true
49+
internals.enableProfilerCommitHooks = true
50+
internals.injectProfilingHooks({
51+
markComponentRenderStarted: (fiber) => {
52+
componentNames.push(fiber.type.name || 'Anonymous')
53+
},
54+
})
55+
56+
await cb()
57+
58+
internals.enableProfilerTimer = false
59+
internals.enableProfilerCommitHooks = false
60+
internals.injectProfilingHooks(null)
61+
62+
return componentNames
63+
}
64+
65+
window.getComponentCalls = getComponentCalls
66+
67+
if (window.__REACT_DEVTOOLS_GLOBAL_HOOK__) {
68+
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = enhanceExistingHook(
69+
window.__REACT_DEVTOOLS_GLOBAL_HOOK__,
70+
)
71+
} else {
72+
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = createMinimalHook()
73+
}
74+
</script>
75+
`
76+
html = html.replace('<head>', `<head>${scriptToInject}`)
77+
route.fulfill({ body: html, headers: { 'content-type': 'text/html' } })
78+
})
79+
80+
await page.goto('/')
81+
await page.waitForLoadState('networkidle')
82+
83+
// get the first item highlighted
84+
await page.evaluate(() => {
85+
const input = document.querySelector('input')
86+
if (!input) {
87+
throw new Error('🚨 could not find the input')
88+
}
89+
input.dispatchEvent(
90+
new KeyboardEvent('keydown', {
91+
key: 'ArrowDown',
92+
keyCode: 40,
93+
bubbles: true,
94+
}),
95+
)
96+
})
97+
98+
// go to the next item, we should now have two that render, the old one to unhighlight it and the new one to highlight it
99+
const calledComponents: Array<string> = await page.evaluate(() =>
100+
(window as any).getComponentCalls(() => {
101+
const input = document.querySelector('input')
102+
if (!input) {
103+
throw new Error('🚨 could not find the input')
104+
}
105+
input.dispatchEvent(
106+
new KeyboardEvent('keydown', {
107+
key: 'ArrowDown',
108+
keyCode: 40,
109+
bubbles: true,
110+
}),
111+
)
112+
}),
113+
)
114+
115+
// memo can change the name of the components, so we'll be more generous with a regex
116+
const listItemRenders = calledComponents.filter((c) => /ListItem/i.test(c))
117+
118+
expect(
119+
listItemRenders,
120+
'🚨 Only two ListItems should render when changing the highlighted item. The first is rerendered to un-highlight it and the second is rerendered to highlight it. Make sure your comparator is correct.',
121+
).toHaveLength(2)
122+
})

exercises/06.rerenders/02.problem.comparator/tests/todo.test.js

Lines changed: 0 additions & 7 deletions
This file was deleted.
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { test, expect } from '@playwright/test'
2+
3+
test('Only two ListItems should not rerender when the highlighted item changes', async ({
4+
page,
5+
}) => {
6+
await page.route('/', async (route) => {
7+
const request = route.request()
8+
if (request.resourceType() !== 'document') return route.continue()
9+
const response = await route.fetch()
10+
11+
let html = await response.text()
12+
const scriptToInject = `
13+
<script>
14+
let internals
15+
16+
function enhanceExistingHook(existingHook) {
17+
const originalInject = existingHook.inject
18+
19+
existingHook.inject = (injectedInternals) => {
20+
internals = injectedInternals
21+
22+
// Returning a number as React expects a renderer ID
23+
return originalInject?.call(existingHook, injectedInternals) ?? 1
24+
}
25+
26+
return existingHook
27+
}
28+
29+
function createMinimalHook() {
30+
return {
31+
renderers: [],
32+
supportsFiber: true,
33+
inject: (injectedInternals) => {
34+
internals = injectedInternals
35+
return 1 // Returning a number as React expects a renderer ID
36+
},
37+
onCommitFiberRoot: () => {},
38+
onCommitFiberUnmount: () => {},
39+
}
40+
}
41+
42+
async function getComponentCalls(cb) {
43+
const componentNames = []
44+
if (!internals) {
45+
throw new Error('🚨 React DevTools is not available')
46+
}
47+
48+
internals.enableProfilerTimer = true
49+
internals.enableProfilerCommitHooks = true
50+
internals.injectProfilingHooks({
51+
markComponentRenderStarted: (fiber) => {
52+
componentNames.push(fiber.type.name || 'Anonymous')
53+
},
54+
})
55+
56+
await cb()
57+
58+
internals.enableProfilerTimer = false
59+
internals.enableProfilerCommitHooks = false
60+
internals.injectProfilingHooks(null)
61+
62+
return componentNames
63+
}
64+
65+
window.getComponentCalls = getComponentCalls
66+
67+
if (window.__REACT_DEVTOOLS_GLOBAL_HOOK__) {
68+
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = enhanceExistingHook(
69+
window.__REACT_DEVTOOLS_GLOBAL_HOOK__,
70+
)
71+
} else {
72+
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = createMinimalHook()
73+
}
74+
</script>
75+
`
76+
html = html.replace('<head>', `<head>${scriptToInject}`)
77+
route.fulfill({ body: html, headers: { 'content-type': 'text/html' } })
78+
})
79+
80+
await page.goto('/')
81+
await page.waitForLoadState('networkidle')
82+
83+
// get the first item highlighted
84+
await page.evaluate(() => {
85+
const input = document.querySelector('input')
86+
if (!input) {
87+
throw new Error('🚨 could not find the input')
88+
}
89+
input.dispatchEvent(
90+
new KeyboardEvent('keydown', {
91+
key: 'ArrowDown',
92+
keyCode: 40,
93+
bubbles: true,
94+
}),
95+
)
96+
})
97+
98+
// go to the next item, we should now have two that render, the old one to unhighlight it and the new one to highlight it
99+
const calledComponents: Array<string> = await page.evaluate(() =>
100+
(window as any).getComponentCalls(() => {
101+
const input = document.querySelector('input')
102+
if (!input) {
103+
throw new Error('🚨 could not find the input')
104+
}
105+
input.dispatchEvent(
106+
new KeyboardEvent('keydown', {
107+
key: 'ArrowDown',
108+
keyCode: 40,
109+
bubbles: true,
110+
}),
111+
)
112+
}),
113+
)
114+
115+
// memo can change the name of the components, so we'll be more generous with a regex
116+
const listItemRenders = calledComponents.filter((c) => /ListItem/i.test(c))
117+
118+
expect(
119+
listItemRenders,
120+
'🚨 Only two ListItems should render when changing the highlighted item. The first is rerendered to un-highlight it and the second is rerendered to highlight it. Make sure your comparator is correct.',
121+
).toHaveLength(2)
122+
})

exercises/06.rerenders/02.solution.comparator/tests/todo.test.js

Lines changed: 0 additions & 7 deletions
This file was deleted.

exercises/06.rerenders/03.problem.primitives/README.mdx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,9 @@ because a simple check with `===` will be enough.
4545
Let's do this for our `ListItem`.
4646

4747
And make sure to check the before/after of your work!
48+
49+
<callout-info>
50+
🚨 There is no change in how many times the ListItem renders with this change
51+
so the tests will be passing from the start. But you'll want to make sure the
52+
tests continue to pass when you're finished.
53+
</callout-info>

0 commit comments

Comments
 (0)