From 99c1cb5623d3ef3a8ddfdbc26b72751e68db5a67 Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Fri, 23 Aug 2024 14:41:16 -0600 Subject: [PATCH] add tests to 02 --- .../05.solution.cb/search-params.test.ts | 5 +- exercises/01.managing-ui-state/README.mdx | 4 +- .../01.problem.effects/index.tsx | 7 +- .../controlled-checkbox.test.ts | 30 ++++++++ .../controlled-search.test.ts | 43 ++++++++++++ .../01.solution.effects/filtering.test.ts | 30 ++++++++ .../01.solution.effects/index.tsx | 7 +- .../01.solution.effects/popstate.test.ts | 38 ++++++++++ .../01.solution.effects/search-params.test.ts | 14 ++++ .../02.problem.cleanup/README.mdx | 15 ++-- .../02.problem.cleanup/index.tsx | 17 ++--- .../controlled-checkbox.test.ts | 30 ++++++++ .../controlled-search.test.ts | 43 ++++++++++++ .../02.solution.cleanup/filtering.test.ts | 30 ++++++++ .../02.solution.cleanup/index.tsx | 13 +--- .../02.solution.cleanup/memory-leak.test.ts | 69 +++++++++++++++++++ .../02.solution.cleanup/popstate.test.ts | 38 ++++++++++ .../02.solution.cleanup/search-params.test.ts | 14 ++++ .../01.problem.lift/index.tsx | 7 +- .../01.solution.lift/index.tsx | 7 +- .../02.problem.lift-array/index.tsx | 7 +- .../02.solution.lift-array/index.tsx | 7 +- .../03.problem.colocate/index.tsx | 7 +- .../03.solution.colocate/index.tsx | 7 +- 24 files changed, 413 insertions(+), 76 deletions(-) create mode 100644 exercises/02.side-effects/01.solution.effects/controlled-checkbox.test.ts create mode 100644 exercises/02.side-effects/01.solution.effects/controlled-search.test.ts create mode 100644 exercises/02.side-effects/01.solution.effects/filtering.test.ts create mode 100644 exercises/02.side-effects/01.solution.effects/popstate.test.ts create mode 100644 exercises/02.side-effects/01.solution.effects/search-params.test.ts create mode 100644 exercises/02.side-effects/02.solution.cleanup/controlled-checkbox.test.ts create mode 100644 exercises/02.side-effects/02.solution.cleanup/controlled-search.test.ts create mode 100644 exercises/02.side-effects/02.solution.cleanup/filtering.test.ts create mode 100644 exercises/02.side-effects/02.solution.cleanup/memory-leak.test.ts create mode 100644 exercises/02.side-effects/02.solution.cleanup/popstate.test.ts create mode 100644 exercises/02.side-effects/02.solution.cleanup/search-params.test.ts diff --git a/exercises/01.managing-ui-state/05.solution.cb/search-params.test.ts b/exercises/01.managing-ui-state/05.solution.cb/search-params.test.ts index 9ea83939a..d55fb5f05 100644 --- a/exercises/01.managing-ui-state/05.solution.cb/search-params.test.ts +++ b/exercises/01.managing-ui-state/05.solution.cb/search-params.test.ts @@ -1,7 +1,8 @@ import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' -const { screen, fireEvent } = dtl +const { screen } = dtl -window.history.pushState({}, '', '?query=dog') +const currentPath = window.location.pathname +window.history.pushState({}, '', `${currentPath}?query=dog`) await import('./index.tsx') diff --git a/exercises/01.managing-ui-state/README.mdx b/exercises/01.managing-ui-state/README.mdx index 4db46f924..c10e71ecf 100644 --- a/exercises/01.managing-ui-state/README.mdx +++ b/exercises/01.managing-ui-state/README.mdx @@ -14,8 +14,8 @@ From there's a cycle of user interaction, state changes, and re-rendering. This is the core of how React works for interactive applications. The `render` phase is what what we've done so far with creating React elements. -Handling user interactions is what we've done with event listeners like -`onSubmit`. Now we're going to get into the `state changes` bit. +Handling user interactions is what we do with event listeners like `onChange`. +Now we're going to get into the `state changes` bit. In React, you use special functions called "hooks" to do this. Common built-in hooks include: diff --git a/exercises/02.side-effects/01.problem.effects/index.tsx b/exercises/02.side-effects/01.problem.effects/index.tsx index ff6951a0a..f6b2315e6 100644 --- a/exercises/02.side-effects/01.problem.effects/index.tsx +++ b/exercises/02.side-effects/01.problem.effects/index.tsx @@ -29,12 +29,7 @@ function App() { return (
-
{ - e.preventDefault() - setGlobalSearchParams({ query }) - }} - > + setGlobalSearchParams({ query })}>
{ + const result = await screen.findByRole('searchbox', { name: /search/i }) + expect(result).toHaveValue('') + return result + }, +) + +const dogCheckbox = await testStep( + 'The user can see the dog checkbox', + async () => { + const result = await screen.findByRole('checkbox', { name: /dog/i }) + expect(result).not.toBeChecked() + return result + }, +) + +await testStep('The user can search for a checkbox value', async () => { + fireEvent.change(searchBox, { target: { value: 'dog' } }) +}) + +await testStep('checkbox is checked automatically', async () => { + expect(dogCheckbox).toBeChecked() +}) diff --git a/exercises/02.side-effects/01.solution.effects/controlled-search.test.ts b/exercises/02.side-effects/01.solution.effects/controlled-search.test.ts new file mode 100644 index 000000000..6cee02db8 --- /dev/null +++ b/exercises/02.side-effects/01.solution.effects/controlled-search.test.ts @@ -0,0 +1,43 @@ +import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' +const { screen, fireEvent } = dtl + +import './index.tsx' + +const searchBox = await testStep( + 'The user can see the search box', + async () => { + const result = await screen.findByRole('searchbox', { name: /search/i }) + expect(result).toHaveValue('') + return result + }, +) + +const dogCheckbox = await testStep( + 'The user can see the dog checkbox', + async () => { + const result = await screen.findByRole('checkbox', { name: /dog/i }) + expect(result).not.toBeChecked() + return result + }, +) + +await testStep('The user can select the dog checkbox', async () => { + fireEvent.click(dogCheckbox) + expect(dogCheckbox).toBeChecked() +}) + +await testStep( + 'Selecting the checkbox updates the search and results', + async () => { + // Check that the search box value has been updated + expect(searchBox).toHaveValue('dog') + + // Check that the results have been filtered + await dtl.waitFor(async () => { + await screen.findByText(/the joy of owning a dog/i) + + const catResult = screen.queryByText(/caring for your feline friend/i) + expect(catResult).not.toBeInTheDocument() + }) + }, +) diff --git a/exercises/02.side-effects/01.solution.effects/filtering.test.ts b/exercises/02.side-effects/01.solution.effects/filtering.test.ts new file mode 100644 index 000000000..dd9eb9f2e --- /dev/null +++ b/exercises/02.side-effects/01.solution.effects/filtering.test.ts @@ -0,0 +1,30 @@ +import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' +const { screen, fireEvent } = dtl + +import './index.tsx' + +const searchBox = await testStep( + 'The user can see the search box', + async () => { + const result = await screen.findByRole('searchbox', { name: /search/i }) + expect(result).toHaveValue('') + return result + }, +) + +const catResult = await testStep('The user can see the results', async () => { + const result = screen.getByText(/caring for your feline friend/i) + expect(result).toBeInTheDocument() + return result +}) + +await testStep('The user can search for a term', async () => { + fireEvent.change(searchBox, { target: { value: 'dog' } }) +}) + +await testStep('The results are filtered', async () => { + await dtl.waitFor(() => { + expect(catResult).not.toBeInTheDocument() + }) + await screen.findByText(/the joy of owning a dog/i) +}) diff --git a/exercises/02.side-effects/01.solution.effects/index.tsx b/exercises/02.side-effects/01.solution.effects/index.tsx index 5ad8ee9ad..7f3b88b2c 100644 --- a/exercises/02.side-effects/01.solution.effects/index.tsx +++ b/exercises/02.side-effects/01.solution.effects/index.tsx @@ -28,12 +28,7 @@ function App() { return (
- { - e.preventDefault() - setGlobalSearchParams({ query }) - }} - > + setGlobalSearchParams({ query })}>
{ + const searchBox = await screen.findByRole('searchbox', { name: /search/i }) + expect(searchBox).toHaveValue('dog') + }, +) + +// wait for the event handler to be set up +// for some reason it takes a bit +await new Promise(resolve => setTimeout(resolve, 100)) + +await testStep( + 'The search box updates when popstate event is triggered', + async () => { + // Simulate navigation to a new URL + const currentPath = window.location.pathname + window.history.pushState({}, '', `${currentPath}?query=cat`) + + // Trigger popstate event + fireEvent.popState(window) + + // Check if the search box value is updated + await dtl.waitFor(async () => + expect( + await screen.findByRole('searchbox', { name: /search/i }), + ).toHaveValue('cat'), + ) + }, +) diff --git a/exercises/02.side-effects/01.solution.effects/search-params.test.ts b/exercises/02.side-effects/01.solution.effects/search-params.test.ts new file mode 100644 index 000000000..9ea83939a --- /dev/null +++ b/exercises/02.side-effects/01.solution.effects/search-params.test.ts @@ -0,0 +1,14 @@ +import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' +const { screen, fireEvent } = dtl + +window.history.pushState({}, '', '?query=dog') + +await import('./index.tsx') + +await testStep( + 'The search box is initialized with URL query parameter', + async () => { + const searchBox = await screen.findByRole('searchbox', { name: /search/i }) + expect(searchBox).toHaveValue('dog') + }, +) diff --git a/exercises/02.side-effects/02.problem.cleanup/README.mdx b/exercises/02.side-effects/02.problem.cleanup/README.mdx index 2d4991fb5..f7267e7be 100644 --- a/exercises/02.side-effects/02.problem.cleanup/README.mdx +++ b/exercises/02.side-effects/02.problem.cleanup/README.mdx @@ -61,9 +61,16 @@ You can add `console.log` statements to make sure things are being called (unles you want to open up the memory profiling tab in your dev tools and click the checkbox a bunch of times to see the memory usage go up πŸ˜…). -πŸ’― If you really do want to test this out, I've added a couple lines you can -uncomment to allocate huge amounts of memory to huge arrays. If you uncomment -those, then watch the quick climb of the memory in the +🚨 To test this, I've added a couple lines to allocate huge amounts of memory to +huge arrays. Watch the quick climb of the memory in the [Memory tab of dev tools](https://developer.chrome.com/docs/devtools/memory) or [Browser Task manager](https://developer.chrome.com/docs/devtools/memory-problems#monitor_memory_use_in_realtime_with_the_chrome_task_manager) -every time you check and uncheck the box. +every time you check and uncheck the box. The test toggles the checkbox many +times and then checks that the memory usage is a reasonable increase of the +initial memory usage. + + + Testing memory leaks is tricky. It's possible the memory usage starts out + higher than it should leading to a test that passes but should not. Try + running the test a few times to be certain you've got it right. + diff --git a/exercises/02.side-effects/02.problem.cleanup/index.tsx b/exercises/02.side-effects/02.problem.cleanup/index.tsx index af87e6b30..04aea1a9b 100644 --- a/exercises/02.side-effects/02.problem.cleanup/index.tsx +++ b/exercises/02.side-effects/02.problem.cleanup/index.tsx @@ -18,13 +18,15 @@ function App() { const caterpillarChecked = words.includes('caterpillar') useEffect(() => { - // πŸ’― you can use this to test whether your cleanup is working (make sure to include the console.log below as well) - // const hugeData = new Array(1_000_000).fill(new Array(1_000_000).fill('πŸΆπŸ±πŸ›')) + // 🚨 we use this to test whether your cleanup is working + const hugeData = new Array(1_000_000).fill( + new Array(1_000_000).fill('πŸΆπŸ±πŸ›'), + ) // 🐨 extract your event handler here into a function called updateQuery window.addEventListener('popstate', () => { - // πŸ’― you can use this to test whether your cleanup is freeing up memory - // console.log(hugeData) + // 🚨 this console.log forces the hugeData to hang around as long as the event listener is active + console.log(hugeData) console.log('popstate event listener called') setQuery(getQueryParam()) @@ -40,12 +42,7 @@ function App() { return (
- { - e.preventDefault() - setGlobalSearchParams({ query }) - }} - > + setGlobalSearchParams({ query })}>
{ + const result = await screen.findByRole('searchbox', { name: /search/i }) + expect(result).toHaveValue('') + return result + }, +) + +const dogCheckbox = await testStep( + 'The user can see the dog checkbox', + async () => { + const result = await screen.findByRole('checkbox', { name: /dog/i }) + expect(result).not.toBeChecked() + return result + }, +) + +await testStep('The user can search for a checkbox value', async () => { + fireEvent.change(searchBox, { target: { value: 'dog' } }) +}) + +await testStep('checkbox is checked automatically', async () => { + expect(dogCheckbox).toBeChecked() +}) diff --git a/exercises/02.side-effects/02.solution.cleanup/controlled-search.test.ts b/exercises/02.side-effects/02.solution.cleanup/controlled-search.test.ts new file mode 100644 index 000000000..6cee02db8 --- /dev/null +++ b/exercises/02.side-effects/02.solution.cleanup/controlled-search.test.ts @@ -0,0 +1,43 @@ +import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' +const { screen, fireEvent } = dtl + +import './index.tsx' + +const searchBox = await testStep( + 'The user can see the search box', + async () => { + const result = await screen.findByRole('searchbox', { name: /search/i }) + expect(result).toHaveValue('') + return result + }, +) + +const dogCheckbox = await testStep( + 'The user can see the dog checkbox', + async () => { + const result = await screen.findByRole('checkbox', { name: /dog/i }) + expect(result).not.toBeChecked() + return result + }, +) + +await testStep('The user can select the dog checkbox', async () => { + fireEvent.click(dogCheckbox) + expect(dogCheckbox).toBeChecked() +}) + +await testStep( + 'Selecting the checkbox updates the search and results', + async () => { + // Check that the search box value has been updated + expect(searchBox).toHaveValue('dog') + + // Check that the results have been filtered + await dtl.waitFor(async () => { + await screen.findByText(/the joy of owning a dog/i) + + const catResult = screen.queryByText(/caring for your feline friend/i) + expect(catResult).not.toBeInTheDocument() + }) + }, +) diff --git a/exercises/02.side-effects/02.solution.cleanup/filtering.test.ts b/exercises/02.side-effects/02.solution.cleanup/filtering.test.ts new file mode 100644 index 000000000..dd9eb9f2e --- /dev/null +++ b/exercises/02.side-effects/02.solution.cleanup/filtering.test.ts @@ -0,0 +1,30 @@ +import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' +const { screen, fireEvent } = dtl + +import './index.tsx' + +const searchBox = await testStep( + 'The user can see the search box', + async () => { + const result = await screen.findByRole('searchbox', { name: /search/i }) + expect(result).toHaveValue('') + return result + }, +) + +const catResult = await testStep('The user can see the results', async () => { + const result = screen.getByText(/caring for your feline friend/i) + expect(result).toBeInTheDocument() + return result +}) + +await testStep('The user can search for a term', async () => { + fireEvent.change(searchBox, { target: { value: 'dog' } }) +}) + +await testStep('The results are filtered', async () => { + await dtl.waitFor(() => { + expect(catResult).not.toBeInTheDocument() + }) + await screen.findByText(/the joy of owning a dog/i) +}) diff --git a/exercises/02.side-effects/02.solution.cleanup/index.tsx b/exercises/02.side-effects/02.solution.cleanup/index.tsx index 1ca211eb5..ce8579815 100644 --- a/exercises/02.side-effects/02.solution.cleanup/index.tsx +++ b/exercises/02.side-effects/02.solution.cleanup/index.tsx @@ -18,14 +18,12 @@ function App() { const caterpillarChecked = words.includes('caterpillar') useEffect(() => { - // πŸ’― this is here for you to test that the memory is freed up when the - // component is unmounted. + // 🚨 we use this to test whether your cleanup is working const hugeData = new Array(1_000_000).fill( new Array(1_000_000).fill('πŸΆπŸ±πŸ›'), ) function updateQuery() { - // πŸ’― this is here to make sure the function hangs onto the reference with - // hugeData (because browsers can be pretty clever with their JIT optimizations) + // 🚨 this console.log forces the hugeData to hang around as long as the event listener is active console.log(hugeData) console.log('popstate event listener called') setQuery(getQueryParam()) @@ -43,12 +41,7 @@ function App() { return (
- { - e.preventDefault() - setGlobalSearchParams({ query }) - }} - > + setGlobalSearchParams({ query })}>
setTimeout(resolve, 10)) + } + } + + await testStep( + 'Memory usage does not increase linearly when toggling showForm', + async () => { + // Check if memory measurement is available + if (!performance.memory) { + console.warn( + 'Memory measurement is not available in this browser. Skipping test.', + ) + return + } + + // wait a bit for garbage collection to finish + await new Promise(resolve => setTimeout(resolve, 500)) + const initialMemory = performance.memory.usedJSHeapSize + + await toggleShowForm(250) + + // wait a bit for garbage collection to finish + await new Promise(resolve => setTimeout(resolve, 500)) + + const finalMemory = performance.memory.usedJSHeapSize + + const initialMemoryMB = + (initialMemory / (1024 * 1024)).toLocaleString() + ' MB' + const finalMemoryMB = + (finalMemory / (1024 * 1024)).toLocaleString() + ' MB' + + const percentageChange = ( + ((finalMemory - initialMemory) / initialMemory) * + 100 + ).toFixed(2) + expect( + Number(percentageChange), + `The memory usage increased from ${initialMemoryMB} to ${finalMemoryMB} (a ${percentageChange}% increase)`, + ).toBeLessThan(110) + }, + ) +} else { + await testStep( + 'Memory measurement is not available in this browser. Skipping test.', + () => {}, + ) +} diff --git a/exercises/02.side-effects/02.solution.cleanup/popstate.test.ts b/exercises/02.side-effects/02.solution.cleanup/popstate.test.ts new file mode 100644 index 000000000..963155d79 --- /dev/null +++ b/exercises/02.side-effects/02.solution.cleanup/popstate.test.ts @@ -0,0 +1,38 @@ +import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' +const { screen, fireEvent } = dtl + +const currentPath = window.location.pathname +window.history.pushState({}, '', `${currentPath}?query=dog`) + +await import('./index.tsx') + +await testStep( + 'The search box is initialized with URL query parameter', + async () => { + const searchBox = await screen.findByRole('searchbox', { name: /search/i }) + expect(searchBox).toHaveValue('dog') + }, +) + +// wait for the event handler to be set up +// for some reason it takes a bit +await new Promise(resolve => setTimeout(resolve, 100)) + +await testStep( + 'The search box updates when popstate event is triggered', + async () => { + // Simulate navigation to a new URL + const currentPath = window.location.pathname + window.history.pushState({}, '', `${currentPath}?query=cat`) + + // Trigger popstate event + fireEvent.popState(window) + + // Check if the search box value is updated + await dtl.waitFor(async () => + expect( + await screen.findByRole('searchbox', { name: /search/i }), + ).toHaveValue('cat'), + ) + }, +) diff --git a/exercises/02.side-effects/02.solution.cleanup/search-params.test.ts b/exercises/02.side-effects/02.solution.cleanup/search-params.test.ts new file mode 100644 index 000000000..9ea83939a --- /dev/null +++ b/exercises/02.side-effects/02.solution.cleanup/search-params.test.ts @@ -0,0 +1,14 @@ +import { expect, testStep, dtl } from '@epic-web/workshop-utils/test' +const { screen, fireEvent } = dtl + +window.history.pushState({}, '', '?query=dog') + +await import('./index.tsx') + +await testStep( + 'The search box is initialized with URL query parameter', + async () => { + const searchBox = await screen.findByRole('searchbox', { name: /search/i }) + expect(searchBox).toHaveValue('dog') + }, +) diff --git a/exercises/03.lifting-state/01.problem.lift/index.tsx b/exercises/03.lifting-state/01.problem.lift/index.tsx index 65e783c5b..2e84b449c 100644 --- a/exercises/03.lifting-state/01.problem.lift/index.tsx +++ b/exercises/03.lifting-state/01.problem.lift/index.tsx @@ -50,12 +50,7 @@ function Form() { } return ( - { - e.preventDefault() - setGlobalSearchParams({ query }) - }} - > + setGlobalSearchParams({ query })}>
{ - e.preventDefault() - setGlobalSearchParams({ query }) - }} - > + setGlobalSearchParams({ query })}>
{ - e.preventDefault() - setGlobalSearchParams({ query }) - }} - > + setGlobalSearchParams({ query })}>
{ - e.preventDefault() - setGlobalSearchParams({ query }) - }} - > + setGlobalSearchParams({ query })}>
{ - e.preventDefault() - setGlobalSearchParams({ query }) - }} - > + setGlobalSearchParams({ query })}>
{ - e.preventDefault() - setGlobalSearchParams({ query }) - }} - > + setGlobalSearchParams({ query })}>