From 4b025e2a079b350fd2218e6b68f85a184e6c55fa Mon Sep 17 00:00:00 2001 From: Steve Kinney Date: Wed, 2 Oct 2024 08:41:56 -0500 Subject: [PATCH] Add some additional examples --- examples/directory/package.json | 35 +++++++++ examples/directory/src/get-user.js | 11 +++ examples/directory/src/mocks/handlers.js | 5 ++ examples/directory/src/mocks/server.js | 4 + examples/directory/src/mocks/tasks.json | 16 ++++ examples/directory/src/user.jsx | 44 +++++++++++ examples/directory/src/user.test.jsx | 10 +++ examples/directory/vite.config.ts | 15 ++++ .../element-factory/src/login-form.test.js | 5 ++ examples/element-factory/src/notification.jsx | 30 ++++++++ .../element-factory/src/notification.test.jsx | 73 +++++++++++++++++++ examples/element-factory/vitest.config.ts | 2 +- examples/logjam/src/make-request.js | 6 ++ examples/logjam/src/make-request.test.js | 3 + examples/scratchpad/fake-time.test.js | 21 ++++++ examples/scratchpad/index.js | 5 ++ examples/scratchpad/package.json | 26 +++++++ examples/task-list/vite.config.ts | 2 +- package-lock.json | 38 +++++++++- package.json | 3 +- tsconfig.json | 7 +- 21 files changed, 355 insertions(+), 6 deletions(-) create mode 100644 examples/directory/package.json create mode 100644 examples/directory/src/get-user.js create mode 100644 examples/directory/src/mocks/handlers.js create mode 100644 examples/directory/src/mocks/server.js create mode 100644 examples/directory/src/mocks/tasks.json create mode 100644 examples/directory/src/user.jsx create mode 100644 examples/directory/src/user.test.jsx create mode 100644 examples/directory/vite.config.ts create mode 100644 examples/element-factory/src/notification.jsx create mode 100644 examples/element-factory/src/notification.test.jsx create mode 100644 examples/logjam/src/make-request.js create mode 100644 examples/logjam/src/make-request.test.js create mode 100644 examples/scratchpad/fake-time.test.js create mode 100644 examples/scratchpad/package.json diff --git a/examples/directory/package.json b/examples/directory/package.json new file mode 100644 index 0000000..59255a7 --- /dev/null +++ b/examples/directory/package.json @@ -0,0 +1,35 @@ +{ + "name": "directory", + "version": "1.0.0", + "main": "index.js", + "type": "module", + "scripts": { + "start": "vite dev", + "test": "vitest" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/stevekinney/testing-javascript.git" + }, + "author": "Steve Kinney ", + "license": "MIT", + "bugs": { + "url": "https://github.com/stevekinney/testing-javascript/issues" + }, + "homepage": "https://github.com/stevekinney/testing-javascript#readme", + "devDependencies": { + "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.5.0", + "@testing-library/react": "^16.0.1", + "@types/body-parser": "^1.19.5", + "@types/express": "^4.17.21", + "@types/react": "^18.3.6", + "@types/react-dom": "^18.3.0", + "@types/testing-library__jest-dom": "^5.14.9", + "@vitejs/plugin-react": "^4.3.1", + "@vitest/ui": "^2.1.1", + "msw": "^2.4.9", + "vite": "^5.4.6", + "vitest": "^2.1.1" + } +} diff --git a/examples/directory/src/get-user.js b/examples/directory/src/get-user.js new file mode 100644 index 0000000..a434b9f --- /dev/null +++ b/examples/directory/src/get-user.js @@ -0,0 +1,11 @@ +export const getUser = async (id) => { + const response = await fetch( + `https://jsonplaceholder.typicode.com/users/${id}`, + ); + + if (!response.ok) { + throw new Error('Failed to fetch user'); + } + + return response.json(); +}; diff --git a/examples/directory/src/mocks/handlers.js b/examples/directory/src/mocks/handlers.js new file mode 100644 index 0000000..90e988d --- /dev/null +++ b/examples/directory/src/mocks/handlers.js @@ -0,0 +1,5 @@ +import { http, HttpResponse } from 'msw'; + +// Hint: https://jsonplaceholder.typicode.com/users/:id + +export const handlers = []; diff --git a/examples/directory/src/mocks/server.js b/examples/directory/src/mocks/server.js new file mode 100644 index 0000000..e52fee0 --- /dev/null +++ b/examples/directory/src/mocks/server.js @@ -0,0 +1,4 @@ +import { setupServer } from 'msw/node'; +import { handlers } from './handlers'; + +export const server = setupServer(...handlers); diff --git a/examples/directory/src/mocks/tasks.json b/examples/directory/src/mocks/tasks.json new file mode 100644 index 0000000..97d4600 --- /dev/null +++ b/examples/directory/src/mocks/tasks.json @@ -0,0 +1,16 @@ +[ + { + "id": "test-1", + "title": "Get a Phone Charger", + "completed": true, + "createdAt": "2024-09-19T08:30:00.711Z", + "lastModified": "2024-09-19T08:30:00.711Z" + }, + { + "id": "test-2", + "title": "Charge Your Phone", + "completed": false, + "createdAt": "2024-09-19T08:31:00.261Z", + "lastModified": "2024-09-19T08:31:00.261Z" + } +] diff --git a/examples/directory/src/user.jsx b/examples/directory/src/user.jsx new file mode 100644 index 0000000..2a852ed --- /dev/null +++ b/examples/directory/src/user.jsx @@ -0,0 +1,44 @@ +import { useEffect, useState } from 'react'; +import { getUser } from './get-user'; + +export const User = ({ id }) => { + const [user, setUser] = useState(null); + + useEffect(() => { + getUser(id).then(setUser); + }, [setUser]); + + if (!user) { + return

Loading…

; + } + + return ( +
+

{user.name}

+ + + + + + + + + + + + + + + + + + + + + + + +
Company{user.company.name}
Username{user.username}
Email{user.email}
Phone{user.phone}
Website{user.website}
+
+ ); +}; diff --git a/examples/directory/src/user.test.jsx b/examples/directory/src/user.test.jsx new file mode 100644 index 0000000..a505ec1 --- /dev/null +++ b/examples/directory/src/user.test.jsx @@ -0,0 +1,10 @@ +import { screen, render } from '@testing-library/react'; +import { User } from './user'; + +it.skip('should render a user', async () => { + // This calls an API. That's not great. We should mock it. + const id = 1; + render(); + + expect(user).toBeInTheDocument(); +}); diff --git a/examples/directory/vite.config.ts b/examples/directory/vite.config.ts new file mode 100644 index 0000000..53f0c26 --- /dev/null +++ b/examples/directory/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { css } from 'css-configuration'; + +const PORT = process.env.PORT || 3000; + +export default defineConfig({ + plugins: [react()], + css, + test: { + globals: true, + environment: 'happy-dom', + setupFiles: ['@testing-library/jest-dom/vitest'], + }, +}); diff --git a/examples/element-factory/src/login-form.test.js b/examples/element-factory/src/login-form.test.js index 4aa6538..b714389 100644 --- a/examples/element-factory/src/login-form.test.js +++ b/examples/element-factory/src/login-form.test.js @@ -12,6 +12,7 @@ describe('LoginForm', 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'? document.body.replaceChildren(createLoginForm({ action: '/custom' })); const form = screen.getByRole('form', { name: /login/i }); @@ -20,6 +21,7 @@ describe('LoginForm', 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'? document.body.replaceChildren(createLoginForm({ method: 'get' })); const form = screen.getByRole('form', { name: /login/i }); @@ -28,6 +30,9 @@ describe('LoginForm', 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. + // 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? const onSubmit = vi.fn(); document.body.replaceChildren(createLoginForm({ onSubmit })); diff --git a/examples/element-factory/src/notification.jsx b/examples/element-factory/src/notification.jsx new file mode 100644 index 0000000..8dff5f1 --- /dev/null +++ b/examples/element-factory/src/notification.jsx @@ -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 ( +
+ + + + + {message &&

{message}

} +
+ ); +}; diff --git a/examples/element-factory/src/notification.test.jsx b/examples/element-factory/src/notification.test.jsx new file mode 100644 index 0000000..15f886c --- /dev/null +++ b/examples/element-factory/src/notification.test.jsx @@ -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(); + 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(); + }); +}); diff --git a/examples/element-factory/vitest.config.ts b/examples/element-factory/vitest.config.ts index 3c340b6..70e93e8 100644 --- a/examples/element-factory/vitest.config.ts +++ b/examples/element-factory/vitest.config.ts @@ -5,8 +5,8 @@ import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [svelte(), react()], test: { - environment: 'happy-dom', globals: true, + environment: 'happy-dom', setupFiles: ['@testing-library/jest-dom/vitest'], }, }); diff --git a/examples/logjam/src/make-request.js b/examples/logjam/src/make-request.js new file mode 100644 index 0000000..84e4a0d --- /dev/null +++ b/examples/logjam/src/make-request.js @@ -0,0 +1,6 @@ +export const makeRequest = async (url) => { + if (!import.meta.env.API_KEY) { + throw new Error('API_KEY is required'); + } + return { data: 'Some data would go here.' }; +}; diff --git a/examples/logjam/src/make-request.test.js b/examples/logjam/src/make-request.test.js new file mode 100644 index 0000000..8fd6a76 --- /dev/null +++ b/examples/logjam/src/make-request.test.js @@ -0,0 +1,3 @@ +import { makeRequest } from './make-request'; + +describe.todo('makeRequest', () => {}); diff --git a/examples/scratchpad/fake-time.test.js b/examples/scratchpad/fake-time.test.js new file mode 100644 index 0000000..fd1dca5 --- /dev/null +++ b/examples/scratchpad/fake-time.test.js @@ -0,0 +1,21 @@ +import { vi, describe, it, expect } from 'vitest'; + +vi.useFakeTimers(); + +function delay(callback) { + setTimeout(() => { + callback('Delayed'); + }, 1000); +} + +describe('delay function', () => { + it('should call callback after delay', () => { + const callback = vi.fn(); + + delay(callback); + + vi.advanceTimersByTime(1000); + + expect(callback).toHaveBeenCalledWith('Delayed'); + }); +}); diff --git a/examples/scratchpad/index.js b/examples/scratchpad/index.js index e69de29..725147d 100644 --- a/examples/scratchpad/index.js +++ b/examples/scratchpad/index.js @@ -0,0 +1,5 @@ +import { it, expect } from 'vitest'; + +it('is a super simple test', () => { + expect(true).toBe(true); +}); diff --git a/examples/scratchpad/package.json b/examples/scratchpad/package.json new file mode 100644 index 0000000..106cfb5 --- /dev/null +++ b/examples/scratchpad/package.json @@ -0,0 +1,26 @@ +{ + "name": "scratchpad", + "version": "1.0.0", + "main": "index.js", + "main": "src/index.js", + "type": "module", + "scripts": { + "start": "vitest --ui", + "test": "vitest" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/stevekinney/testing-javascript.git" + }, + "author": "Steve Kinney ", + "license": "MIT", + "bugs": { + "url": "https://github.com/stevekinney/testing-javascript/issues" + }, + "homepage": "https://github.com/stevekinney/testing-javascript#readme", + "devDependencies": { + "@vitest/ui": "^2.1.1", + "vite": "^5.4.5", + "vitest": "^2.1.1" + } +} diff --git a/examples/task-list/vite.config.ts b/examples/task-list/vite.config.ts index aa4d639..43a5884 100644 --- a/examples/task-list/vite.config.ts +++ b/examples/task-list/vite.config.ts @@ -17,8 +17,8 @@ export default defineConfig({ }, }, test: { - environment: 'happy-dom', globals: true, + environment: 'happy-dom', setupFiles: ['@testing-library/jest-dom/vitest'], }, }); diff --git a/package-lock.json b/package-lock.json index 1247655..d031aed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,8 @@ "examples/utility-belt", "examples/strictly-speaking", "examples/element-factory", - "examples/logjam" + "examples/logjam", + "examples/directory" ], "devDependencies": { "prettier": "^3.3.3", @@ -110,6 +111,25 @@ "vitest": "^2.1.1" } }, + "examples/directory": { + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.5.0", + "@testing-library/react": "^16.0.1", + "@types/body-parser": "^1.19.5", + "@types/express": "^4.17.21", + "@types/react": "^18.3.6", + "@types/react-dom": "^18.3.0", + "@types/testing-library__jest-dom": "^5.14.9", + "@vitejs/plugin-react": "^4.3.1", + "@vitest/ui": "^2.1.1", + "msw": "^2.4.9", + "vite": "^5.4.6", + "vitest": "^2.1.1" + } + }, "examples/element-factory": { "name": "button-factory", "version": "1.0.0", @@ -159,7 +179,13 @@ } }, "examples/scratchpad": { - "extraneous": true + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "@vitest/ui": "^2.1.1", + "vite": "^5.4.5", + "vitest": "^2.1.1" + } }, "examples/strictly-speaking": { "version": "1.0.0", @@ -2705,6 +2731,10 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/directory": { + "resolved": "examples/directory", + "link": true + }, "node_modules/dlv": { "version": "1.1.3", "dev": true, @@ -4831,6 +4861,10 @@ "loose-envify": "^1.1.0" } }, + "node_modules/scratchpad": { + "resolved": "examples/scratchpad", + "link": true + }, "node_modules/semver": { "version": "6.3.1", "dev": true, diff --git a/package.json b/package.json index 61919d6..30de158 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,8 @@ "examples/utility-belt", "examples/strictly-speaking", "examples/element-factory", - "examples/logjam" + "examples/logjam", + "examples/directory" ], "devDependencies": { "prettier": "^3.3.3", diff --git a/tsconfig.json b/tsconfig.json index 3f861db..135c744 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,11 @@ "jsx": "react-jsx", "forceConsistentCasingInFileNames": true }, - "include": ["examples/**/*.js", "examples/**/*.svelte", "examples/**/*.ts"], + "include": [ + "examples/**/*.js", + "examples/**/*.svelte", + "examples/**/*.ts", + "examples/directory/src/user.jsx" + ], "exclude": ["node_modules", "dist"] }