+)
function lessonTitle(title) {
- return document.createElement('span').setAttribute('text', title);
+ return document.createElement('span').setAttribute('text', title)
}
const fib = (n) => {
diff --git a/code-examples/fib-final.example b/code-examples/fib-final.example
index 024e941..bf4908c 100644
--- a/code-examples/fib-final.example
+++ b/code-examples/fib-final.example
@@ -7,7 +7,7 @@ const fib = (n) => {
if (n === 0 || n === 1) {
return n
}
- return makeAdder(fib(makeSubtracter(1)(n) + fib(makeSubtracter(2)(n))(n)
+ return makeAdder(fib(makeSubtracter(1)(n)))(fib(makeSubtracter(2)(n)))
}
export default { fib }
diff --git a/code-examples/ltr-basic-test.example b/code-examples/ltr-basic-test.example
new file mode 100644
index 0000000..85ef98c
--- /dev/null
+++ b/code-examples/ltr-basic-test.example
@@ -0,0 +1,15 @@
+import {render, screen} from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import Greeter from './greeter'
+
+it('displays a greeting', async () => {
+ // Arrange
+ const name = "World"
+ render()
+
+ // Act
+ await screen.findByRole('heading')
+
+ // Assert
+ expect(screen.getByRole('heading')).toHaveTextContent(`Hello ${name}`)
+})
diff --git a/code-examples/ltr-fake-timers.example b/code-examples/ltr-fake-timers.example
new file mode 100644
index 0000000..f4ea6cc
--- /dev/null
+++ b/code-examples/ltr-fake-timers.example
@@ -0,0 +1,12 @@
+describe('my fake timers', () => {
+ beforeEach(() => {
+ jest.useFakeTimers()
+ })
+
+ it('runs setTimeout', ...)
+
+ afterEach(() => {
+ jest.runOnlyPendingTimers()
+ jest.useRealTimers()
+ })
+})
diff --git a/code-examples/ltr-mock-test.example b/code-examples/ltr-mock-test.example
new file mode 100644
index 0000000..3a7eb27
--- /dev/null
+++ b/code-examples/ltr-mock-test.example
@@ -0,0 +1,19 @@
+import {render, screen} from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import Greeter from './greeter'
+
+jest.mock('react-i18next', () => ({
+ useTranslation: () => ({ t: (str: string) => 'Привіт' })
+}))
+
+test('displays a greeting', async () => {
+ // Arrange
+ const name = "Світ"
+ render()
+
+ // Act
+ await screen.findByRole('heading')
+
+ // Assert
+ expect(screen.getByRole('heading')).toHaveTextContent(`Привіт ${name}`)
+})
diff --git a/code-examples/ltr-screen-queries.example b/code-examples/ltr-screen-queries.example
new file mode 100644
index 0000000..eee7b0a
--- /dev/null
+++ b/code-examples/ltr-screen-queries.example
@@ -0,0 +1,13 @@
+// Queries Accessible to Everyone
+screen.getByRole('button', { hidden: true })
+screen.getByLabelText('Username')
+screen.getByPlaceholderText('Username')
+screen.getByText(/about/i)
+screen.getByDisplayValue('Alaska')
+
+// Semantic Queries
+screen.getByAltText(/incredibles.*? poster/i)
+screen.getByTitle('Delete')
+
+// Test IDs
+screen.getByTestId('custom-element')
diff --git a/code-examples/ltr-screen-query-types.example b/code-examples/ltr-screen-query-types.example
new file mode 100644
index 0000000..56cd5e4
--- /dev/null
+++ b/code-examples/ltr-screen-query-types.example
@@ -0,0 +1,11 @@
+screen.getByRole('spinbutton', {value: {now: 5}})
+//
+
+screen.getAllByRole('spinbutton', {value: {min: 0}})
+// [
+// ,
+//
+// ]
+
+screen.queryByRole('spinbutton', {value: {now: 5}})
+// null
\ No newline at end of file
diff --git a/code-examples/ltr-spies-test.example b/code-examples/ltr-spies-test.example
new file mode 100644
index 0000000..4279767
--- /dev/null
+++ b/code-examples/ltr-spies-test.example
@@ -0,0 +1,15 @@
+import {render, screen} from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import Button from './button'
+
+test('calls the onClick handler', async () => {
+ // Arrange
+ const handleClick = jest.fn();
+ render()
+
+ // Act
+ await userEvent.click(await screen.findByRole('button'))
+
+ // Assert
+ expect(handleClick).toHaveBeenCalled()
+})
diff --git a/code-examples/ltr-user-event-types.example b/code-examples/ltr-user-event-types.example
new file mode 100644
index 0000000..69cb0c3
--- /dev/null
+++ b/code-examples/ltr-user-event-types.example
@@ -0,0 +1,37 @@
+pointer(input: PointerActionInput | Array): Promise
+pointer([
+ // touch the screen at element1
+ {keys: '[TouchA>]', target: element1},
+ // move the touch pointer to element2
+ {pointerName: 'TouchA', target: element2},
+ // release the touch pointer at the last position (element2)
+ {keys: '[/TouchA]'},
+])
+
+keyboard(input: KeyboardInput): Promise
+keyboard('{Shift>}A{/Shift}')
+
+clear(element: Element): Promise
+clear(screen.queryByRole('spinbutton'))
+
+selectOptions(
+ element: Element,
+ values: HTMLElement | HTMLElement[] | string[] | string,
+): Promise
+deselectOptions(
+ element: Element,
+ values: HTMLElement | HTMLElement[] | string[] | string,
+): Promise
+selectOptions(screen.getByRole('listbox'), ['1', 'C'])
+
+upload(
+ element: HTMLElement,
+ fileOrFiles: File | File[],
+): Promise
+upload(screen.getByLabelText(/upload file/i), new File())
+
+hover(element: Element): Promise
+hover(screen.getByRole('button'))
+
+unhover(element: Element): Promise
+unhover(screen.getByRole('button'))
diff --git a/code-examples/ltr-user-events.example b/code-examples/ltr-user-events.example
new file mode 100644
index 0000000..e9e53c9
--- /dev/null
+++ b/code-examples/ltr-user-events.example
@@ -0,0 +1,10 @@
+test('type into an input field', async () => {
+ const user = userEvent.setup()
+
+ render()
+ const input = screen.getByRole('textbox')
+
+ await user.type(input, ' World!')
+
+ expect(input).toHaveValue('Hello, World!')
+})
diff --git a/code-examples/storybook-naming-hierarchy.example b/code-examples/storybook-naming-hierarchy.example
new file mode 100644
index 0000000..dca43a1
--- /dev/null
+++ b/code-examples/storybook-naming-hierarchy.example
@@ -0,0 +1,13 @@
+import type { Meta, StoryObj } from '@storybook/react'
+
+import { DurationInput as DurationInputComponent } from './common/forms/DurationInput'
+
+const meta: Meta = {
+ title: 'common/forms/DurationInput',
+ component: DurationInputComponent,
+}
+
+export default meta
+type Story = StoryObj
+
+export const DurationInput: Story = {}
diff --git a/code-examples/using-args.example b/code-examples/using-args.example
new file mode 100644
index 0000000..17190e6
--- /dev/null
+++ b/code-examples/using-args.example
@@ -0,0 +1,20 @@
+import type { Meta } from '@storybook/react';
+
+import { Button } from './Button';
+
+const meta: Meta = {
+ component: Button,
+ argTypes: {
+ variant: { control: 'select', options: ['primary', 'secondary', 'tertiary'] },
+ label: { control: 'text' }
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {
+ render({ label, variant }) {
+ return
+ }
+}
diff --git a/code-examples/writing-a-story.example b/code-examples/writing-a-story.example
new file mode 100644
index 0000000..4f6ff46
--- /dev/null
+++ b/code-examples/writing-a-story.example
@@ -0,0 +1,18 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { Button } from './Button';
+
+const meta: Meta = {
+ component: Button,
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Primary: Story = {
+ render: () => ,
+};
+
+export const Secondary: Story = {
+ render: () => ,
+}
diff --git a/presentation/writing-fe-tests.js b/presentation/writing-fe-tests.js
new file mode 100644
index 0000000..4068fe9
--- /dev/null
+++ b/presentation/writing-fe-tests.js
@@ -0,0 +1,235 @@
+import React from "react";
+import { createRoot } from "react-dom/client";
+import {
+ Box,
+ CodePane,
+ CodeSpan,
+ Deck,
+ FlexBox,
+ Heading,
+ Image,
+ ListItem,
+ Notes,
+ Slide,
+ Text,
+ UnorderedList,
+} from "spectacle";
+
+import "normalize.css";
+import theme from "../themes/spectrum";
+
+import blackBoxTesting from "../assets/black-box-testing.png";
+import thatsAllFolks from "../assets/thats-all-folks.gif";
+
+import basicTest from '../code-examples/ltr-basic-test.example';
+import componentsAsFunctions from "../code-examples/components-as-functions.example";
+import fakeTimers from '../code-examples/ltr-fake-timers.example';
+import fibFinal from "../code-examples/fib-final.example";
+import fibrMock from "../code-examples/fibr-mock.example";
+import initialFib from "../code-examples/initial-fib.example";
+import mockTest from '../code-examples/ltr-mock-test.example';
+import namingAndHierarchy from '../code-examples/storybook-naming-hierarchy.example';
+import screenQueryTypes from '../code-examples/ltr-screen-query-types.example';
+import screenQueries from '../code-examples/ltr-screen-queries.example';
+import spiesTest from '../code-examples/ltr-spies-test.example';
+import userEvents from '../code-examples/ltr-user-events.example';
+import userEventTypes from '../code-examples/ltr-user-event-types.example';
+import usingArgs from '../code-examples/using-args.example';
+import writingAStory from '../code-examples/writing-a-story.example';
+
+const Presentation = () => (
+
+
+ Writing Frontend Tests
+
+ With @testing-library/react and storybook
+
+
+
+ Agenda
+
+ Unit and visual testing
+ Unit testing theory
+ Unit testing with @testing-library/react
+ Visual testing with storybook
+
+
+
+
+ Unit testing and visual testing
+
+
+ Help guarantee correctness
+ Faster to write
+ Mimics actual user behavior
+
+
+
+
+ Unit testing vs visual testing
+
+
+
+ Unit tests
+
+ Run on every build
+ Behavior only
+
+
+
+ Visual tests
+
+ Run on-demand
+ Includes appearance
+
+
+
+
+
+
+ Why unit testing?
+
+
+ Safety when making changes
+ Write better code
+ Form of documentation
+
+
+
+ Components as pure functions
+ {componentsAsFunctions}
+
+ I'm going to get started with something really simple and NOT a component so we can think about unit testing
+ independent of React; then we'll add the particulars of the framework. Note that all three examples are the
+ same for the purpose of unit testing -- they are a pure function that takes some input and returns some output.
+
+
+
+ Getting started
+ {initialFib}
+
+ So here's a test! It doesn't really get any simpler than that. I include the
+ export because, along with the function definition, it defines the contract
+ we have with our client. There's not a lot to test here, so there's not a lot
+ to distract us from the fact that we are testing inputs and outputs --
+ in particular that given the same input, we get the same output, and the
+ expected output at that. This is simple because this is a "pure" function.
+
+
+
+ Refactoring
+ {fibrMock}
+
+ We've refactored! Note that we don't have to change our tests, so we can feel confident
+ that we haven't broken anything, and we know that we won't have to change any of the call
+ sites, since the contract hasn't changed. Also note that we _don't_ test fibR -- its not
+ part of our customer contract, so its just an implementation detail -- there are a lot of
+ other ways to implement memoization that are more efficient (but less functional), and if
+ we were to switch to one (for instance, hoisting the memo out of the function), we don't
+ want to change the tests.
+
+
+
+ Black box testing
+
+
+
+
+
+ More refactoring
+ {fibFinal}
+
+ Note that when we refactor a second time, our original unit test still covers the unit
+ under test, while our newer test no longer applies.
+
+
+
+ A basic unit test
+ {basicTest}
+
+
+ Available Screen Queries
+ {screenQueries}
+
+
+ Screen Query Types
+ {screenQueryTypes}
+
+
+ User events
+ {userEvents}
+
+
+ User Event Types
+ {userEventTypes}
+
+
+ Mocks
+ {mockTest}
+
+
+ Mocks: What to mock?
+
+ Unit tests should mock every import
+ Integration tests only mock side-effects
+ Both approaches have merits and drawbacks
+
+
+
+ Spies
+ {spiesTest}
+
+
+ Faking Timers
+ When your code uses timers (setTimeout, setInterval, clearTimeout, clearInterval), use fake timers
+ {fakeTimers}
+
+
+ Running the unit tests
+
+ yarn test --coverage
+
+
+
+
+ Why visual testing?
+
+
+ Safety when making changes
+ Form of documentation
+ Ship smaller changesets
+
+
+
+ Writing a story
+ {writingAStory}
+
+
+ Using args
+ {usingArgs}
+
+
+ Naming and hierarchy
+ {namingAndHierarchy}
+
+
+ Mocking
+ We will want to mock network requests, but are yet to set up the infrastructure.
+ Reach out if you need this and we can coordinate.
+
+
+ Viewing storybook
+ yarn storybook
+
+
+ Questions?
+
+
+
+
+
+);
+
+const container = document.getElementById("root");
+const root = createRoot(container);
+root.render();