From 02bd183bb2c71d57d3072b8daf94a6636d32012a Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Tue, 15 Oct 2024 14:30:53 -0600 Subject: [PATCH] woo, some great test improvements --- epicshop/fix-watch.js | 2 +- ...{code-split.test.js => code-split.test.ts} | 5 +- ...{code-split.test.js => code-split.test.ts} | 5 +- ...-loading.test.js => eager-loading.test.ts} | 10 +- ...-loading.test.js => eager-loading.test.ts} | 10 +- .../tests/{flash.test.js => flash.test.ts} | 5 +- .../tests/{flash.test.js => flash.test.ts} | 5 +- .../01.problem.use-memo/tests/todo.test.js | 7 -- .../tests/use-memo.test.ts | 42 ++++++++ .../01.solution.use-memo/tests/todo.test.js | 7 -- .../tests/use-memo.test.ts | 42 ++++++++ .../01.problem.memo/tests/memoized.test.ts | 99 +++++++++++++++++++ .../01.problem.memo/tests/todo.test.js | 7 -- .../01.solution.memo/tests/memoized.test.ts | 99 +++++++++++++++++++ .../01.solution.memo/tests/todo.test.js | 7 -- 15 files changed, 315 insertions(+), 37 deletions(-) rename exercises/04.code-splitting/01.problem.lazy/tests/{code-split.test.js => code-split.test.ts} (90%) rename exercises/04.code-splitting/01.solution.lazy/tests/{code-split.test.js => code-split.test.ts} (90%) rename exercises/04.code-splitting/02.problem.eager/tests/{eager-loading.test.js => eager-loading.test.ts} (88%) rename exercises/04.code-splitting/02.solution.eager/tests/{eager-loading.test.js => eager-loading.test.ts} (88%) rename exercises/04.code-splitting/03.problem.transition/tests/{flash.test.js => flash.test.ts} (94%) rename exercises/04.code-splitting/03.solution.transition/tests/{flash.test.js => flash.test.ts} (94%) delete mode 100644 exercises/05.calculations/01.problem.use-memo/tests/todo.test.js create mode 100644 exercises/05.calculations/01.problem.use-memo/tests/use-memo.test.ts delete mode 100644 exercises/05.calculations/01.solution.use-memo/tests/todo.test.js create mode 100644 exercises/05.calculations/01.solution.use-memo/tests/use-memo.test.ts create mode 100644 exercises/06.rerenders/01.problem.memo/tests/memoized.test.ts delete mode 100644 exercises/06.rerenders/01.problem.memo/tests/todo.test.js create mode 100644 exercises/06.rerenders/01.solution.memo/tests/memoized.test.ts delete mode 100644 exercises/06.rerenders/01.solution.memo/tests/todo.test.js diff --git a/epicshop/fix-watch.js b/epicshop/fix-watch.js index 9a36210fb..8d10916e1 100644 --- a/epicshop/fix-watch.js +++ b/epicshop/fix-watch.js @@ -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 { diff --git a/exercises/04.code-splitting/01.problem.lazy/tests/code-split.test.js b/exercises/04.code-splitting/01.problem.lazy/tests/code-split.test.ts similarity index 90% rename from exercises/04.code-splitting/01.problem.lazy/tests/code-split.test.js rename to exercises/04.code-splitting/01.problem.lazy/tests/code-split.test.ts index 2c8a02729..bd2276a45 100644 --- a/exercises/04.code-splitting/01.problem.lazy/tests/code-split.test.js +++ b/exercises/04.code-splitting/01.problem.lazy/tests/code-split.test.ts @@ -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), ) diff --git a/exercises/04.code-splitting/01.solution.lazy/tests/code-split.test.js b/exercises/04.code-splitting/01.solution.lazy/tests/code-split.test.ts similarity index 90% rename from exercises/04.code-splitting/01.solution.lazy/tests/code-split.test.js rename to exercises/04.code-splitting/01.solution.lazy/tests/code-split.test.ts index 2c8a02729..bd2276a45 100644 --- a/exercises/04.code-splitting/01.solution.lazy/tests/code-split.test.js +++ b/exercises/04.code-splitting/01.solution.lazy/tests/code-split.test.ts @@ -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), ) diff --git a/exercises/04.code-splitting/02.problem.eager/tests/eager-loading.test.js b/exercises/04.code-splitting/02.problem.eager/tests/eager-loading.test.ts similarity index 88% rename from exercises/04.code-splitting/02.problem.eager/tests/eager-loading.test.js rename to exercises/04.code-splitting/02.problem.eager/tests/eager-loading.test.ts index bacd4a5df..aabfb88cd 100644 --- a/exercises/04.code-splitting/02.problem.eager/tests/eager-loading.test.js +++ b/exercises/04.code-splitting/02.problem.eager/tests/eager-loading.test.ts @@ -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), ) @@ -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), ) diff --git a/exercises/04.code-splitting/02.solution.eager/tests/eager-loading.test.js b/exercises/04.code-splitting/02.solution.eager/tests/eager-loading.test.ts similarity index 88% rename from exercises/04.code-splitting/02.solution.eager/tests/eager-loading.test.js rename to exercises/04.code-splitting/02.solution.eager/tests/eager-loading.test.ts index bacd4a5df..aabfb88cd 100644 --- a/exercises/04.code-splitting/02.solution.eager/tests/eager-loading.test.js +++ b/exercises/04.code-splitting/02.solution.eager/tests/eager-loading.test.ts @@ -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), ) @@ -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), ) diff --git a/exercises/04.code-splitting/03.problem.transition/tests/flash.test.js b/exercises/04.code-splitting/03.problem.transition/tests/flash.test.ts similarity index 94% rename from exercises/04.code-splitting/03.problem.transition/tests/flash.test.js rename to exercises/04.code-splitting/03.problem.transition/tests/flash.test.ts index 686b77ed9..236e91d9b 100644 --- a/exercises/04.code-splitting/03.problem.transition/tests/flash.test.js +++ b/exercises/04.code-splitting/03.problem.transition/tests/flash.test.ts @@ -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), ) diff --git a/exercises/04.code-splitting/03.solution.transition/tests/flash.test.js b/exercises/04.code-splitting/03.solution.transition/tests/flash.test.ts similarity index 94% rename from exercises/04.code-splitting/03.solution.transition/tests/flash.test.js rename to exercises/04.code-splitting/03.solution.transition/tests/flash.test.ts index 686b77ed9..236e91d9b 100644 --- a/exercises/04.code-splitting/03.solution.transition/tests/flash.test.js +++ b/exercises/04.code-splitting/03.solution.transition/tests/flash.test.ts @@ -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), ) diff --git a/exercises/05.calculations/01.problem.use-memo/tests/todo.test.js b/exercises/05.calculations/01.problem.use-memo/tests/todo.test.js deleted file mode 100644 index 89017c5c3..000000000 --- a/exercises/05.calculations/01.problem.use-memo/tests/todo.test.js +++ /dev/null @@ -1,7 +0,0 @@ -import { test, expect } from '@playwright/test' - -test('todo', async ({ page }) => { - await page.goto('/') - await page.waitForLoadState('networkidle') - // TODO: finish this test -}) diff --git a/exercises/05.calculations/01.problem.use-memo/tests/use-memo.test.ts b/exercises/05.calculations/01.problem.use-memo/tests/use-memo.test.ts new file mode 100644 index 000000000..7d73a8d4f --- /dev/null +++ b/exercises/05.calculations/01.problem.use-memo/tests/use-memo.test.ts @@ -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> = 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) +}) diff --git a/exercises/05.calculations/01.solution.use-memo/tests/todo.test.js b/exercises/05.calculations/01.solution.use-memo/tests/todo.test.js deleted file mode 100644 index 89017c5c3..000000000 --- a/exercises/05.calculations/01.solution.use-memo/tests/todo.test.js +++ /dev/null @@ -1,7 +0,0 @@ -import { test, expect } from '@playwright/test' - -test('todo', async ({ page }) => { - await page.goto('/') - await page.waitForLoadState('networkidle') - // TODO: finish this test -}) diff --git a/exercises/05.calculations/01.solution.use-memo/tests/use-memo.test.ts b/exercises/05.calculations/01.solution.use-memo/tests/use-memo.test.ts new file mode 100644 index 000000000..7d73a8d4f --- /dev/null +++ b/exercises/05.calculations/01.solution.use-memo/tests/use-memo.test.ts @@ -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> = 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) +}) diff --git a/exercises/06.rerenders/01.problem.memo/tests/memoized.test.ts b/exercises/06.rerenders/01.problem.memo/tests/memoized.test.ts new file mode 100644 index 000000000..0b4a238c0 --- /dev/null +++ b/exercises/06.rerenders/01.problem.memo/tests/memoized.test.ts @@ -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 = ` + + ` + html = html.replace('', `${scriptToInject}`) + route.fulfill({ body: html, headers: { 'content-type': 'text/html' } }) + }) + + await page.goto('/') + await page.waitForLoadState('networkidle') + + const calledComponents: Array = 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 + } +} diff --git a/exercises/06.rerenders/01.problem.memo/tests/todo.test.js b/exercises/06.rerenders/01.problem.memo/tests/todo.test.js deleted file mode 100644 index 89017c5c3..000000000 --- a/exercises/06.rerenders/01.problem.memo/tests/todo.test.js +++ /dev/null @@ -1,7 +0,0 @@ -import { test, expect } from '@playwright/test' - -test('todo', async ({ page }) => { - await page.goto('/') - await page.waitForLoadState('networkidle') - // TODO: finish this test -}) diff --git a/exercises/06.rerenders/01.solution.memo/tests/memoized.test.ts b/exercises/06.rerenders/01.solution.memo/tests/memoized.test.ts new file mode 100644 index 000000000..0b4a238c0 --- /dev/null +++ b/exercises/06.rerenders/01.solution.memo/tests/memoized.test.ts @@ -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 = ` + + ` + html = html.replace('', `${scriptToInject}`) + route.fulfill({ body: html, headers: { 'content-type': 'text/html' } }) + }) + + await page.goto('/') + await page.waitForLoadState('networkidle') + + const calledComponents: Array = 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 + } +} diff --git a/exercises/06.rerenders/01.solution.memo/tests/todo.test.js b/exercises/06.rerenders/01.solution.memo/tests/todo.test.js deleted file mode 100644 index 89017c5c3..000000000 --- a/exercises/06.rerenders/01.solution.memo/tests/todo.test.js +++ /dev/null @@ -1,7 +0,0 @@ -import { test, expect } from '@playwright/test' - -test('todo', async ({ page }) => { - await page.goto('/') - await page.waitForLoadState('networkidle') - // TODO: finish this test -})