Compare commits
4 Commits
main
...
answer-key
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eac857a010 | ||
|
|
9ac6b6e1ef | ||
|
|
17a5b21baa | ||
|
|
d9679e954a |
16
README.md
16
README.md
@@ -1,16 +0,0 @@
|
|||||||
## Testing Fundamentals Course
|
|
||||||
|
|
||||||
This is a companion repository for the [Testing Fundamentals](https://frontendmasters.com/courses/testing/) course on Frontend Masters.
|
|
||||||
[](https://frontendmasters.com/courses/testing/)
|
|
||||||
|
|
||||||
## Setup Instructions
|
|
||||||
|
|
||||||
> We recommend using Node.js version 20+ for this course
|
|
||||||
|
|
||||||
Clone this repository and install the dependencies:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/stevekinney/introduction-to-testing.git
|
|
||||||
cd introduction-to-testing
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
@@ -5,40 +5,37 @@ import { Counter } from './counter';
|
|||||||
|
|
||||||
import '@testing-library/jest-dom/vitest';
|
import '@testing-library/jest-dom/vitest';
|
||||||
|
|
||||||
describe.todo('Counter ', () => {
|
describe('Counter ', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
render(<Counter />);
|
render(<Counter />);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders with an initial count of 0');
|
it('renders with an initial count of 0', () => {
|
||||||
|
const countElement = screen.getByTestId('counter-count');
|
||||||
|
expect(countElement).toHaveTextContent('0');
|
||||||
|
});
|
||||||
|
|
||||||
it('disables the "Decrement" and "Reset" buttons when the count is 0');
|
it('disables the "Decrement" and "Reset" buttons when the count is 0', () => {
|
||||||
|
const decrementButton = screen.getByRole('button', { name: 'Decrement' });
|
||||||
|
const resetButton = screen.getByRole('button', { name: 'Reset' });
|
||||||
|
|
||||||
|
expect(decrementButton).toBeDisabled();
|
||||||
|
expect(resetButton).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
it.todo('displays "days" when the count is 0', () => {});
|
it.todo('displays "days" when the count is 0', () => {});
|
||||||
|
|
||||||
it.todo(
|
it.todo('increments the count when the "Increment" button is clicked', async () => {});
|
||||||
'increments the count when the "Increment" button is clicked',
|
|
||||||
async () => {},
|
|
||||||
);
|
|
||||||
|
|
||||||
it.todo('displays "day" when the count is 1', async () => {});
|
it.todo('displays "day" when the count is 1', async () => {});
|
||||||
|
|
||||||
it.todo(
|
it.todo('decrements the count when the "Decrement" button is clicked', async () => {});
|
||||||
'decrements the count when the "Decrement" button is clicked',
|
|
||||||
async () => {},
|
|
||||||
);
|
|
||||||
|
|
||||||
it.todo('does not allow decrementing below 0', async () => {});
|
it.todo('does not allow decrementing below 0', async () => {});
|
||||||
|
|
||||||
it.todo(
|
it.todo('resets the count when the "Reset" button is clicked', async () => {});
|
||||||
'resets the count when the "Reset" button is clicked',
|
|
||||||
async () => {},
|
|
||||||
);
|
|
||||||
|
|
||||||
it.todo(
|
it.todo('disables the "Decrement" and "Reset" buttons when the count is 0', () => {});
|
||||||
'disables the "Decrement" and "Reset" buttons when the count is 0',
|
|
||||||
() => {},
|
|
||||||
);
|
|
||||||
|
|
||||||
it.todo('updates the document title based on the count', async () => {});
|
it.todo('updates the document title based on the count', async () => {});
|
||||||
});
|
});
|
||||||
|
|||||||
12
examples/accident-counter/tests/counter.spec.js
Normal file
12
examples/accident-counter/tests/counter.spec.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.goto('http://localhost:5173');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it has a counter', async ({ page }) => {
|
||||||
|
const count = page.getByTestId('counter-count');
|
||||||
|
const incrementButton = page.getByRole('button', { name: /increment/i });
|
||||||
|
|
||||||
|
await incrementButton.click();
|
||||||
|
});
|
||||||
@@ -3,12 +3,32 @@ import userEvent from '@testing-library/user-event';
|
|||||||
|
|
||||||
import { AlertButton } from './alert-button';
|
import { AlertButton } from './alert-button';
|
||||||
|
|
||||||
describe.todo('AlertButton', () => {
|
describe('AlertButton', () => {
|
||||||
beforeEach(() => {});
|
beforeEach(() => {
|
||||||
|
vi.spyOn(window, 'alert').mockImplementation(() => {});
|
||||||
afterEach(() => {});
|
render(<AlertButton />);
|
||||||
|
});
|
||||||
it('should render an alert button', async () => {});
|
|
||||||
|
afterEach(() => {
|
||||||
it('should trigger an alert', async () => {});
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render an alert button', async () => {
|
||||||
|
const button = screen.getByRole('button', { name: /trigger alert/i });
|
||||||
|
|
||||||
|
expect(button).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trigger an alert', async () => {
|
||||||
|
const button = screen.getByRole('button', { name: /trigger alert/i });
|
||||||
|
const messageInput = screen.getByLabelText(/message/i);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await userEvent.clear(messageInput);
|
||||||
|
await userEvent.type(messageInput, 'Hello, world!');
|
||||||
|
await userEvent.click(button);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(window.alert).toHaveBeenCalledWith('Hello, world!');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
import { createButton } from './button.js';
|
import { createButton } from './button.js';
|
||||||
|
|
||||||
describe.todo('createButton', () => {
|
describe('createButton', () => {
|
||||||
it('should create a button element', () => {});
|
it('should create a button element', () => {
|
||||||
|
const button = createButton();
|
||||||
it('should have the text "Click Me"', () => {});
|
expect(button.tagName).toBe('BUTTON');
|
||||||
|
});
|
||||||
it('should change the text to "Clicked!" when clicked', async () => {});
|
|
||||||
|
it('should have the text "Click Me"', () => {
|
||||||
|
const button = createButton();
|
||||||
|
expect(button.textContent).toBe('Click Me');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should change the text to "Clicked!" when clicked', async () => {
|
||||||
|
const button = createButton();
|
||||||
|
button.click();
|
||||||
|
expect(button.textContent).toBe('Clicked!');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,20 +2,45 @@ import { screen } from '@testing-library/dom';
|
|||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { createLoginForm } from './login-form';
|
import { createLoginForm } from './login-form';
|
||||||
|
|
||||||
describe.todo('Login Form', async () => {
|
describe('LoginForm', async () => {
|
||||||
it('should render a login form', async () => {});
|
it('should render a login form', async () => {
|
||||||
|
document.body.replaceChildren(createLoginForm());
|
||||||
|
|
||||||
|
const form = screen.getByRole('form', { name: /login/i });
|
||||||
|
|
||||||
|
expect(form).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it('should render a login form with a custom action', async () => {
|
it('should render a login form with a custom action', async () => {
|
||||||
// Can you make sure that the form we render has an `action` attribute set to '/custom'?
|
// Can you make sure that the form we render has an `action` attribute set to '/custom'?
|
||||||
|
document.body.replaceChildren(createLoginForm({ action: '/custom' }));
|
||||||
|
|
||||||
|
const form = screen.getByRole('form', { name: /login/i });
|
||||||
|
|
||||||
|
expect(form).toHaveAttribute('action', '/custom');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render a login form with a custom method', async () => {
|
it('should render a login form with a custom method', async () => {
|
||||||
// Can you make sure that the form we render has a `method` attribute set to 'get'?
|
// Can you make sure that the form we render has a `method` attribute set to 'get'?
|
||||||
|
document.body.replaceChildren(createLoginForm({ method: 'get' }));
|
||||||
|
|
||||||
|
const form = screen.getByRole('form', { name: /login/i });
|
||||||
|
|
||||||
|
expect(form).toHaveAttribute('method', 'get');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render a login form with a custom submit handler', async () => {
|
it('should render a login form with a custom submit handler', async () => {
|
||||||
// We'll do this one later. Don't worry about it for now.
|
// We'll do this one later. Don't worry about it for now.
|
||||||
// If it *is* later, then you should worry about it.
|
// If it *is* later, then you should worry about it.
|
||||||
// Can you make sure that the form we render has a submit handler that calls a custom function?
|
// Can you make sure that the form we render has a submit handler that calls a custom function?
|
||||||
|
const onSubmit = vi.fn();
|
||||||
|
document.body.replaceChildren(createLoginForm({ onSubmit }));
|
||||||
|
|
||||||
|
const form = screen.getByRole('form', { name: /login/i });
|
||||||
|
const submitButton = screen.getByRole('button', { name: /login/i });
|
||||||
|
|
||||||
|
await userEvent.click(submitButton);
|
||||||
|
|
||||||
|
expect(onSubmit).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
30
examples/element-factory/src/notification.jsx
Normal file
30
examples/element-factory/src/notification.jsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export const Notification = () => {
|
||||||
|
const [content, setContent] = useState('');
|
||||||
|
const [message, setMessage] = useState('');
|
||||||
|
|
||||||
|
const showNotification = () => {
|
||||||
|
console.log({ content });
|
||||||
|
if (!content) return;
|
||||||
|
setMessage(content);
|
||||||
|
setTimeout(() => setMessage(''), 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
Message Content
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={content}
|
||||||
|
onChange={(event) => setContent(event.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<button onClick={showNotification}>Show Notification</button>
|
||||||
|
|
||||||
|
{message && <p data-testid="message">{message}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
73
examples/element-factory/src/notification.test.jsx
Normal file
73
examples/element-factory/src/notification.test.jsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { vi } from 'vitest';
|
||||||
|
import { render, screen, act } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
|
||||||
|
import { Notification } from './notification';
|
||||||
|
|
||||||
|
describe('Notification', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render(<Notification />);
|
||||||
|
vi.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render a notification', async () => {
|
||||||
|
const input = screen.getByRole('textbox', { name: /message content/i });
|
||||||
|
const button = screen.getByRole('button', { name: /show notification/i });
|
||||||
|
|
||||||
|
expect(input).toBeInTheDocument();
|
||||||
|
expect(button).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.only('should show a notification', async () => {
|
||||||
|
const input = screen.getByRole('textbox', { name: /message content/i });
|
||||||
|
const button = screen.getByRole('button', { name: /show notification/i });
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await userEvent.type(input, 'Hello, world!');
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await userEvent.click(button);
|
||||||
|
});
|
||||||
|
|
||||||
|
const message = await screen.findByTestId('message');
|
||||||
|
|
||||||
|
expect(message).toHaveTextContent('Hello, world!');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show a notification if there is no content', async () => {
|
||||||
|
const button = screen.getByRole('button', { name: /show notification/i });
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await userEvent.click(button);
|
||||||
|
});
|
||||||
|
|
||||||
|
const message = screen.queryByTestId('message');
|
||||||
|
|
||||||
|
expect(message).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should hide a notification after 5 seconds', async () => {
|
||||||
|
const input = screen.getByRole('textbox', { name: /message content/i });
|
||||||
|
const button = screen.getByRole('button', { name: /show notification/i });
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await userEvent.type(input, 'Hello, world!');
|
||||||
|
await userEvent.click(button);
|
||||||
|
});
|
||||||
|
|
||||||
|
const message = screen.getByTestId('message');
|
||||||
|
|
||||||
|
expect(message).toHaveTextContent('Hello, world!');
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
vi.advanceTimersByTime(5000);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(message).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,14 +4,41 @@ import '@testing-library/jest-dom/vitest';
|
|||||||
|
|
||||||
import { createSecretInput } from './secret-input.js';
|
import { createSecretInput } from './secret-input.js';
|
||||||
|
|
||||||
describe.todo('createSecretInput', async () => {
|
describe('createSecretInput', async () => {
|
||||||
beforeEach(() => {});
|
beforeEach(() => {
|
||||||
|
vi.spyOn(localStorage, 'getItem').mockReturnValue('test secret');
|
||||||
|
vi.spyOn(localStorage, 'setItem');
|
||||||
|
vi.spyOn(localStorage, 'removeItem');
|
||||||
|
|
||||||
afterEach(() => {});
|
document.body.innerHTML = '';
|
||||||
|
document.body.appendChild(createSecretInput());
|
||||||
it('should have loaded the secret from localStorage', async () => {});
|
});
|
||||||
|
|
||||||
it('should save the secret to localStorage', async () => {});
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
it('should clear the secret from localStorage', async () => {});
|
});
|
||||||
|
|
||||||
|
it('should have loaded the secret from localStorage', async () => {
|
||||||
|
expect(screen.getByLabelText('Secret')).toHaveValue('test secret');
|
||||||
|
expect(localStorage.getItem).toHaveBeenCalledWith('secret');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should save the secret to localStorage', async () => {
|
||||||
|
const input = screen.getByLabelText('Secret');
|
||||||
|
const button = screen.getByText('Store Secret');
|
||||||
|
|
||||||
|
await userEvent.clear(input);
|
||||||
|
await userEvent.type(input, 'new secret');
|
||||||
|
await userEvent.click(button);
|
||||||
|
|
||||||
|
expect(localStorage.setItem).toHaveBeenCalledWith('secret', 'new secret');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear the secret from localStorage', async () => {
|
||||||
|
const button = screen.getByText('Clear Secret');
|
||||||
|
|
||||||
|
await userEvent.click(button);
|
||||||
|
|
||||||
|
expect(localStorage.removeItem).toHaveBeenCalledWith('secret');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import userEvent from '@testing-library/user-event';
|
|||||||
|
|
||||||
import Tabs from './tabs.svelte';
|
import Tabs from './tabs.svelte';
|
||||||
|
|
||||||
describe.todo('Tabs', () => {
|
describe('Tabs', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
render(Tabs, {
|
render(Tabs, {
|
||||||
tabs: [
|
tabs: [
|
||||||
@@ -14,11 +14,32 @@ describe.todo('Tabs', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render three tabs', async () => {});
|
it('should render three tabs', async () => {
|
||||||
|
const tabs = screen.getAllByRole('tab');
|
||||||
it('should switch tabs', async () => {});
|
expect(tabs).toHaveLength(3);
|
||||||
|
});
|
||||||
it('should render the content of the selected tab', async () => {});
|
|
||||||
|
it('should switch tabs', async () => {
|
||||||
it('should render the content of the first tab by default', async () => {});
|
const tabs = screen.getAllByRole('tab');
|
||||||
|
const secondTab = tabs[1];
|
||||||
|
|
||||||
|
await userEvent.click(secondTab);
|
||||||
|
|
||||||
|
expect(secondTab).toHaveAttribute('aria-selected', 'true');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the content of the selected tab', async () => {
|
||||||
|
const tabs = screen.getAllByRole('tab');
|
||||||
|
const secondTab = tabs[1];
|
||||||
|
|
||||||
|
await userEvent.click(secondTab);
|
||||||
|
|
||||||
|
const content = screen.getByRole('tabpanel', { hidden: false });
|
||||||
|
expect(content).toHaveTextContent('lineup');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the content of the first tab by default', async () => {
|
||||||
|
const content = screen.getByRole('tabpanel', { hidden: false });
|
||||||
|
expect(content).toHaveTextContent('We will be at this place!');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
3
examples/scratchpad/__snapshots__/index.test.js.snap
Normal file
3
examples/scratchpad/__snapshots__/index.test.js.snap
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`a super simple test 1`] = `"<div>wowowow</div>"`;
|
||||||
@@ -9,5 +9,13 @@ function delay(callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('delay function', () => {
|
describe('delay function', () => {
|
||||||
it.todo('should call callback after delay', () => {});
|
it('should call callback after delay', () => {
|
||||||
|
const callback = vi.fn();
|
||||||
|
|
||||||
|
delay(callback);
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(1000);
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledWith('Delayed');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "scratchpad",
|
"name": "scratchpad",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -11,4 +11,13 @@ const createTask = (title) => ({
|
|||||||
lastModified: new Date('02-29-2024').toISOString(),
|
lastModified: new Date('02-29-2024').toISOString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const handlers = [];
|
export const handlers = [
|
||||||
|
http.get('/api/tasks', async () => {
|
||||||
|
return HttpResponse.json(tasks);
|
||||||
|
}),
|
||||||
|
http.post('/api/tasks', async ({ request }) => {
|
||||||
|
const { title } = await request.json();
|
||||||
|
const task = createTask(title);
|
||||||
|
return HttpResponse.json(task);
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
/** @type {import('../start-server').DevelopmentServer} */
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('http://localhost:5173');
|
await page.goto('http://localhost:5174');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it should load the page', async ({ page }) => {
|
test('it should load the page', async ({ page }) => {
|
||||||
@@ -16,7 +15,7 @@ test('it should add a task', async ({ page }) => {
|
|||||||
await input.fill('Learn Playwright');
|
await input.fill('Learn Playwright');
|
||||||
await submit.click();
|
await submit.click();
|
||||||
|
|
||||||
const heading = await page.getByRole('heading', { name: 'Learn Playwright' });
|
const heading = page.getByRole('heading', { name: 'Learn Playwright' });
|
||||||
|
|
||||||
await expect(heading).toBeVisible();
|
await expect(heading).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,5 +6,10 @@ describe('stringToNumber', () => {
|
|||||||
expect(stringToNumber('42')).toBe(42);
|
expect(stringToNumber('42')).toBe(42);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.todo('throws an error if given a string that is not a number', () => {});
|
it('throws an error if given a string that is not a number', () => {
|
||||||
|
const value = 'foo';
|
||||||
|
expect(() => stringToNumber(value)).toThrowError(
|
||||||
|
`cannot be parsed as a number`,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user