Skip to content

Commit

Permalink
woo, some great test improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
kentcdodds committed Oct 15, 2024
1 parent 6f6e609 commit 02bd183
Show file tree
Hide file tree
Showing 15 changed files with 315 additions and 37 deletions.
2 changes: 1 addition & 1 deletion epicshop/fix-watch.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ async function run() {
await $({
stdio: 'inherit',
cwd: workshopRoot,
})`node ./scripts/fix.js`
})`node ./epicshop/fix.js`
} catch (error) {
throw error
} finally {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ test('should load the globe and countries modules on demand', async ({
const jsRequests = await page.evaluate(() =>
performance
.getEntriesByType('resource')
.filter((entry) => entry.initiatorType === 'script')
.filter(
(entry) =>
(entry as PerformanceResourceTiming).initiatorType === 'script',
)
.map((entry) => entry.name),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ test('should load the globe and countries modules on demand', async ({
const jsRequests = await page.evaluate(() =>
performance
.getEntriesByType('resource')
.filter((entry) => entry.initiatorType === 'script')
.filter(
(entry) =>
(entry as PerformanceResourceTiming).initiatorType === 'script',
)
.map((entry) => entry.name),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ test('should load the globe and countries modules on hover', async ({
const jsRequests = await page.evaluate(() =>
performance
.getEntriesByType('resource')
.filter((entry) => entry.initiatorType === 'script')
.filter(
(entry) =>
(entry as PerformanceResourceTiming).initiatorType === 'script',
)
.map((entry) => entry.name),
)

Expand Down Expand Up @@ -41,7 +44,10 @@ test('should load the globe and countries modules on focus', async ({
const jsRequests = await page.evaluate(() =>
performance
.getEntriesByType('resource')
.filter((entry) => entry.initiatorType === 'script')
.filter(
(entry) =>
(entry as PerformanceResourceTiming).initiatorType === 'script',
)
.map((entry) => entry.name),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ test('should load the globe and countries modules on hover', async ({
const jsRequests = await page.evaluate(() =>
performance
.getEntriesByType('resource')
.filter((entry) => entry.initiatorType === 'script')
.filter(
(entry) =>
(entry as PerformanceResourceTiming).initiatorType === 'script',
)
.map((entry) => entry.name),
)

Expand Down Expand Up @@ -41,7 +44,10 @@ test('should load the globe and countries modules on focus', async ({
const jsRequests = await page.evaluate(() =>
performance
.getEntriesByType('resource')
.filter((entry) => entry.initiatorType === 'script')
.filter(
(entry) =>
(entry as PerformanceResourceTiming).initiatorType === 'script',
)
.map((entry) => entry.name),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ test('should not show a pending UI when the globe is ready', async ({
const jsRequests = await page.evaluate(() =>
performance
.getEntriesByType('resource')
.filter((entry) => entry.initiatorType === 'script')
.filter(
(entry) =>
(entry as PerformanceResourceTiming).initiatorType === 'script',
)
.map((entry) => entry.name),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ test('should not show a pending UI when the globe is ready', async ({
const jsRequests = await page.evaluate(() =>
performance
.getEntriesByType('resource')
.filter((entry) => entry.initiatorType === 'script')
.filter(
(entry) =>
(entry as PerformanceResourceTiming).initiatorType === 'script',
)
.map((entry) => entry.name),
)

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { test, expect } from '@playwright/test'

test('searchCities should not be called when clicking force rerender', async ({
page,
}) => {
await page.route('**/cities/index.ts', async (route) => {
const response = await route.fetch()
let content = await response.text()
content = content.replace(
'export function searchCities',
'function searchCities',
)
const instrumentedCode = `
// code below is added by the test:
window.__epicshop = { clearSearchCitiesCalls: () => window.__epicshop.searchCitiesCalls = [] }
window.__epicshop.clearSearchCitiesCalls()
function searchCitiesInstrumented(...args) {
window.__epicshop.searchCitiesCalls.push(args)
return searchCities(...args)
}
export { searchCitiesInstrumented as searchCities }
`
content = `${content}\n\n${instrumentedCode}`

route.fulfill({ body: content, headers: response.headers() })
})
await page.goto('/')
await page.waitForLoadState('networkidle')

await page.evaluate(() => (window as any).__epicshop.clearSearchCitiesCalls())
await page.getByRole('button', { name: /force/i }).click()
const searchCitiesCalls: Array<Array<string>> = await page.evaluate(
() => (window as any).__epicshop.searchCitiesCalls,
)

expect(
searchCitiesCalls,
'🚨 searchCities was called when clicking force rerender. Because nothing changed in the user input when clicking that button, searchCities should not have been called. Wrap searchCities inside useMemo with the input as a dependency to fix this.',
).toHaveLength(0)
})

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { test, expect } from '@playwright/test'

test('searchCities should not be called when clicking force rerender', async ({
page,
}) => {
await page.route('**/cities/index.ts', async (route) => {
const response = await route.fetch()
let content = await response.text()
content = content.replace(
'export function searchCities',
'function searchCities',
)
const instrumentedCode = `
// code below is added by the test:
window.__epicshop = { clearSearchCitiesCalls: () => window.__epicshop.searchCitiesCalls = [] }
window.__epicshop.clearSearchCitiesCalls()
function searchCitiesInstrumented(...args) {
window.__epicshop.searchCitiesCalls.push(args)
return searchCities(...args)
}
export { searchCitiesInstrumented as searchCities }
`
content = `${content}\n\n${instrumentedCode}`

route.fulfill({ body: content, headers: response.headers() })
})
await page.goto('/')
await page.waitForLoadState('networkidle')

await page.evaluate(() => (window as any).__epicshop.clearSearchCitiesCalls())
await page.getByRole('button', { name: /force/i }).click()
const searchCitiesCalls: Array<Array<string>> = await page.evaluate(
() => (window as any).__epicshop.searchCitiesCalls,
)

expect(
searchCitiesCalls,
'🚨 searchCities was called when clicking force rerender. Because nothing changed in the user input when clicking that button, searchCities should not have been called. Wrap searchCities inside useMemo with the input as a dependency to fix this.',
).toHaveLength(0)
})
99 changes: 99 additions & 0 deletions exercises/06.rerenders/01.problem.memo/tests/memoized.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { test, expect } from '@playwright/test'

test('Only ListItem should not rerender when clicking force rerender', 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')

const calledComponents: Array<string> = await page.evaluate(() =>
(window as any).getComponentCalls(() => {
document.querySelector('button')?.click()
}),
)

expect(
calledComponents,
'🚨 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
}
}
7 changes: 0 additions & 7 deletions exercises/06.rerenders/01.problem.memo/tests/todo.test.js

This file was deleted.

Loading

0 comments on commit 02bd183

Please sign in to comment.