Skip to content

Commit

Permalink
more improved tests
Browse files Browse the repository at this point in the history
  • Loading branch information
kentcdodds committed Oct 15, 2024
1 parent 02bd183 commit cce10ef
Show file tree
Hide file tree
Showing 11 changed files with 494 additions and 40 deletions.
6 changes: 0 additions & 6 deletions exercises/06.rerenders/01.problem.memo/tests/memoized.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,3 @@ test('Only ListItem should not rerender when clicking force rerender', async ({
'🚨 The ListItem component was rendered when clicking force render. Use the `memo` utility from React on the ListItem component to prevent this.',
).not.toContain('ListItem')
})

declare global {
interface Window {
__REACT_DEVTOOLS_GLOBAL_HOOK__?: any
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,3 @@ test('Only ListItem should not rerender when clicking force rerender', async ({
'🚨 The ListItem component was rendered when clicking force render. Use the `memo` utility from React on the ListItem component to prevent this.',
).not.toContain('ListItem')
})

declare global {
interface Window {
__REACT_DEVTOOLS_GLOBAL_HOOK__?: any
}
}
122 changes: 122 additions & 0 deletions exercises/06.rerenders/02.problem.comparator/tests/memoized.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { test, expect } from '@playwright/test'

test('Only two ListItems should not rerender when the highlighted item changes', async ({
page,
}) => {
await page.route('/', async (route) => {
const request = route.request()
if (request.resourceType() !== 'document') return route.continue()
const response = await route.fetch()

let html = await response.text()
const scriptToInject = `
<script>
let internals
function enhanceExistingHook(existingHook) {
const originalInject = existingHook.inject
existingHook.inject = (injectedInternals) => {
internals = injectedInternals
// Returning a number as React expects a renderer ID
return originalInject?.call(existingHook, injectedInternals) ?? 1
}
return existingHook
}
function createMinimalHook() {
return {
renderers: [],
supportsFiber: true,
inject: (injectedInternals) => {
internals = injectedInternals
return 1 // Returning a number as React expects a renderer ID
},
onCommitFiberRoot: () => {},
onCommitFiberUnmount: () => {},
}
}
async function getComponentCalls(cb) {
const componentNames = []
if (!internals) {
throw new Error('🚨 React DevTools is not available')
}
internals.enableProfilerTimer = true
internals.enableProfilerCommitHooks = true
internals.injectProfilingHooks({
markComponentRenderStarted: (fiber) => {
componentNames.push(fiber.type.name || 'Anonymous')
},
})
await cb()
internals.enableProfilerTimer = false
internals.enableProfilerCommitHooks = false
internals.injectProfilingHooks(null)
return componentNames
}
window.getComponentCalls = getComponentCalls
if (window.__REACT_DEVTOOLS_GLOBAL_HOOK__) {
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = enhanceExistingHook(
window.__REACT_DEVTOOLS_GLOBAL_HOOK__,
)
} else {
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = createMinimalHook()
}
</script>
`
html = html.replace('<head>', `<head>${scriptToInject}`)
route.fulfill({ body: html, headers: { 'content-type': 'text/html' } })
})

await page.goto('/')
await page.waitForLoadState('networkidle')

// get the first item highlighted
await page.evaluate(() => {
const input = document.querySelector('input')
if (!input) {
throw new Error('🚨 could not find the input')
}
input.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'ArrowDown',
keyCode: 40,
bubbles: true,
}),
)
})

// 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
const calledComponents: Array<string> = await page.evaluate(() =>
(window as any).getComponentCalls(() => {
const input = document.querySelector('input')
if (!input) {
throw new Error('🚨 could not find the input')
}
input.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'ArrowDown',
keyCode: 40,
bubbles: true,
}),
)
}),
)

// memo can change the name of the components, so we'll be more generous with a regex
const listItemRenders = calledComponents.filter((c) => /ListItem/i.test(c))

expect(
listItemRenders,
'🚨 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.',
).toHaveLength(2)
})

This file was deleted.

122 changes: 122 additions & 0 deletions exercises/06.rerenders/02.solution.comparator/tests/memoized.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { test, expect } from '@playwright/test'

test('Only two ListItems should not rerender when the highlighted item changes', async ({
page,
}) => {
await page.route('/', async (route) => {
const request = route.request()
if (request.resourceType() !== 'document') return route.continue()
const response = await route.fetch()

let html = await response.text()
const scriptToInject = `
<script>
let internals
function enhanceExistingHook(existingHook) {
const originalInject = existingHook.inject
existingHook.inject = (injectedInternals) => {
internals = injectedInternals
// Returning a number as React expects a renderer ID
return originalInject?.call(existingHook, injectedInternals) ?? 1
}
return existingHook
}
function createMinimalHook() {
return {
renderers: [],
supportsFiber: true,
inject: (injectedInternals) => {
internals = injectedInternals
return 1 // Returning a number as React expects a renderer ID
},
onCommitFiberRoot: () => {},
onCommitFiberUnmount: () => {},
}
}
async function getComponentCalls(cb) {
const componentNames = []
if (!internals) {
throw new Error('🚨 React DevTools is not available')
}
internals.enableProfilerTimer = true
internals.enableProfilerCommitHooks = true
internals.injectProfilingHooks({
markComponentRenderStarted: (fiber) => {
componentNames.push(fiber.type.name || 'Anonymous')
},
})
await cb()
internals.enableProfilerTimer = false
internals.enableProfilerCommitHooks = false
internals.injectProfilingHooks(null)
return componentNames
}
window.getComponentCalls = getComponentCalls
if (window.__REACT_DEVTOOLS_GLOBAL_HOOK__) {
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = enhanceExistingHook(
window.__REACT_DEVTOOLS_GLOBAL_HOOK__,
)
} else {
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = createMinimalHook()
}
</script>
`
html = html.replace('<head>', `<head>${scriptToInject}`)
route.fulfill({ body: html, headers: { 'content-type': 'text/html' } })
})

await page.goto('/')
await page.waitForLoadState('networkidle')

// get the first item highlighted
await page.evaluate(() => {
const input = document.querySelector('input')
if (!input) {
throw new Error('🚨 could not find the input')
}
input.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'ArrowDown',
keyCode: 40,
bubbles: true,
}),
)
})

// 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
const calledComponents: Array<string> = await page.evaluate(() =>
(window as any).getComponentCalls(() => {
const input = document.querySelector('input')
if (!input) {
throw new Error('🚨 could not find the input')
}
input.dispatchEvent(
new KeyboardEvent('keydown', {
key: 'ArrowDown',
keyCode: 40,
bubbles: true,
}),
)
}),
)

// memo can change the name of the components, so we'll be more generous with a regex
const listItemRenders = calledComponents.filter((c) => /ListItem/i.test(c))

expect(
listItemRenders,
'🚨 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.',
).toHaveLength(2)
})

This file was deleted.

6 changes: 6 additions & 0 deletions exercises/06.rerenders/03.problem.primitives/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,9 @@ because a simple check with `===` will be enough.
Let's do this for our `ListItem`.

And make sure to check the before/after of your work!

<callout-info>
🚨 There is no change in how many times the ListItem renders with this change
so the tests will be passing from the start. But you'll want to make sure the
tests continue to pass when you're finished.
</callout-info>
Loading

0 comments on commit cce10ef

Please sign in to comment.