├── .gitignore ├── .prettierrc ├── README.md ├── examples ├── accident-counter │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── playwright.config.js │ ├── src │ │ ├── counter.test.jsx │ │ ├── counter.tsx │ │ ├── reducer.test.js │ │ └── reducer.ts │ ├── styles.css │ └── vite.config.ts ├── basic-math │ ├── README.md │ ├── package.json │ ├── src │ │ ├── arithmetic.js │ │ ├── arithmetic.test.js │ │ ├── counter.js │ │ ├── counter.test.js │ │ ├── fibonacci.js │ │ └── fibonacci.test.ts │ └── vitest.config.js ├── calculator │ ├── index.html │ ├── package.json │ ├── playwright.config.js │ ├── setup-tests.js │ ├── src │ │ ├── calculator.html │ │ ├── calculator.js │ │ └── calculator.test.js │ ├── styles.css │ ├── tests │ │ └── calculator.spec.js │ └── vite.config.js ├── characters │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── character.js │ │ ├── character.test.ts │ │ ├── person.js │ │ ├── person.test.js │ │ └── roll-dice.js │ └── vitest.config.js ├── directory │ ├── package.json │ ├── src │ │ ├── get-user.js │ │ ├── mocks │ │ │ ├── handlers.js │ │ │ ├── server.js │ │ │ └── tasks.json │ │ ├── user.jsx │ │ └── user.test.jsx │ └── vite.config.ts ├── element-factory │ ├── package.json │ ├── src │ │ ├── alert-button.jsx │ │ ├── alert-button.test.jsx │ │ ├── button.js │ │ ├── button.test.js │ │ ├── local-storage.test.js │ │ ├── login-form.js │ │ ├── login-form.test.js │ │ ├── secret-input.js │ │ ├── secret-input.test.js │ │ ├── tabs.svelte │ │ └── tabs.test.js │ └── vitest.config.ts ├── guess-the-number │ ├── game.js │ ├── game.test.js │ ├── index.js │ ├── package.json │ └── vitest.config.js ├── logjam │ ├── package.json │ ├── src │ │ ├── log.js │ │ ├── log.test.js │ │ ├── make-request.js │ │ ├── make-request.test.js │ │ └── send-to-server.js │ └── vitest.config.js ├── scratchpad │ ├── fake-time.test.js │ ├── index.test.js │ └── package.json ├── strictly-speaking │ ├── package.json │ └── strictly-speaking.test.js ├── task-list │ ├── index.html │ ├── package.json │ ├── playwright.config.js │ ├── server │ │ ├── index.js │ │ └── tasks.js │ ├── src │ │ ├── actions.test.js │ │ ├── actions.ts │ │ ├── api.ts │ │ ├── components │ │ │ ├── application.jsx │ │ │ ├── create-task.jsx │ │ │ ├── date-time.jsx │ │ │ ├── task.jsx │ │ │ └── tasks.jsx │ │ ├── index.jsx │ │ ├── mocks │ │ │ ├── handlers.js │ │ │ ├── server.js │ │ │ └── tasks.json │ │ ├── reducer.ts │ │ └── types.ts │ ├── styles.css │ ├── tests │ │ └── task-list.spec.js │ └── vite.config.ts └── utility-belt │ ├── package.json │ ├── src │ ├── index.js │ ├── string-to-number.js │ └── string-to-number.test.js │ └── vitest.config.js ├── jsconfig.json ├── package-lock.json ├── package.json ├── packages ├── css-configuration │ ├── index.d.ts │ ├── index.js │ └── package.json └── get-error-message │ ├── index.d.ts │ ├── index.js │ └── package.json ├── tsconfig.json └── types.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Node.js 2 | node_modules/ 3 | 4 | # Logs 5 | logs/ 6 | *.log 7 | 8 | # Dependency directories 9 | pids/ 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Build output 15 | dist/ 16 | build/ 17 | out/ 18 | playwright-report 19 | test-results 20 | 21 | # Environment variables 22 | .env 23 | 24 | # IDE files 25 | .vscode/ 26 | .idea/ 27 | *.sublime-project 28 | *.sublime-workspace 29 | 30 | # Miscellaneous 31 | .DS_Store 32 | Thumbs.db 33 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "all", 8 | "bracketSpacing": true, 9 | "plugins": ["prettier-plugin-tailwindcss"] 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Testing Fundamentals Course 2 | 3 | This is a companion repository for the [Testing Fundamentals](https://frontendmasters.com/courses/testing/) course on Frontend Masters. 4 | [![Frontend Masters](https://static.frontendmasters.com/assets/brand/logos/full.png)](https://frontendmasters.com/courses/testing/) 5 | 6 | ## Setup Instructions 7 | 8 | > We recommend using Node.js version 20+ for this course 9 | 10 | Clone this repository and install the dependencies: 11 | 12 | ```bash 13 | git clone https://github.com/stevekinney/introduction-to-testing.git 14 | cd introduction-to-testing 15 | npm install 16 | ``` 17 | -------------------------------------------------------------------------------- /examples/accident-counter/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /test-results/ 3 | /playwright-report/ 4 | /blob-report/ 5 | /playwright/.cache/ 6 | -------------------------------------------------------------------------------- /examples/accident-counter/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevekinney/introduction-to-testing/e4ab8518f4d2d938fcb1a4bd68c256d2f9322e77/examples/accident-counter/README.md -------------------------------------------------------------------------------- /examples/accident-counter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Accident Counter 5 | 6 | 14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /examples/accident-counter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "accident-counter", 3 | "version": "1.0.0", 4 | "main": "src/index.js", 5 | "type": "module", 6 | "scripts": { 7 | "start": "vite dev", 8 | "test": "vitest", 9 | "test:ui": "vitest --ui" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/stevekinney/testing-javascript.git" 14 | }, 15 | "author": "Steve Kinney ", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/stevekinney/testing-javascript/issues" 19 | }, 20 | "homepage": "https://github.com/stevekinney/testing-javascript#readme", 21 | "devDependencies": { 22 | "@playwright/test": "^1.47.2", 23 | "@testing-library/dom": "^10.4.0", 24 | "@testing-library/jest-dom": "^6.5.0", 25 | "@testing-library/react": "^16.0.1", 26 | "@types/node": "^22.7.4", 27 | "@types/react": "^18.3.6", 28 | "@types/react-dom": "^18.3.0", 29 | "@types/testing-library__jest-dom": "^5.14.9", 30 | "@vitejs/plugin-react": "^4.3.1", 31 | "@vitest/ui": "^2.1.1", 32 | "react": "^18.3.1", 33 | "react-dom": "^18.3.1", 34 | "tailwindcss": "^3.4.11", 35 | "vite": "^5.4.5", 36 | "vitest": "^2.1.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/accident-counter/playwright.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { defineConfig, devices } from '@playwright/test'; 3 | 4 | /** 5 | * Read environment variables from file. 6 | * https://github.com/motdotla/dotenv 7 | */ 8 | // require('dotenv').config({ path: path.resolve(__dirname, '.env') }); 9 | 10 | /** 11 | * @see https://playwright.dev/docs/test-configuration 12 | */ 13 | export default defineConfig({ 14 | testDir: './tests', 15 | /* Run tests in files in parallel */ 16 | fullyParallel: true, 17 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 18 | forbidOnly: !!process.env.CI, 19 | /* Retry on CI only */ 20 | retries: process.env.CI ? 2 : 0, 21 | /* Opt out of parallel tests on CI. */ 22 | workers: process.env.CI ? 1 : undefined, 23 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 24 | reporter: 'html', 25 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 26 | use: { 27 | /* Base URL to use in actions like `await page.goto('/')`. */ 28 | // baseURL: 'http://127.0.0.1:3000', 29 | 30 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 31 | trace: 'on-first-retry', 32 | }, 33 | 34 | /* Configure projects for major browsers */ 35 | projects: [ 36 | { 37 | name: 'chromium', 38 | use: { ...devices['Desktop Chrome'] }, 39 | }, 40 | 41 | { 42 | name: 'firefox', 43 | use: { ...devices['Desktop Firefox'] }, 44 | }, 45 | 46 | { 47 | name: 'webkit', 48 | use: { ...devices['Desktop Safari'] }, 49 | }, 50 | 51 | /* Test against mobile viewports. */ 52 | // { 53 | // name: 'Mobile Chrome', 54 | // use: { ...devices['Pixel 5'] }, 55 | // }, 56 | // { 57 | // name: 'Mobile Safari', 58 | // use: { ...devices['iPhone 12'] }, 59 | // }, 60 | 61 | /* Test against branded browsers. */ 62 | // { 63 | // name: 'Microsoft Edge', 64 | // use: { ...devices['Desktop Edge'], channel: 'msedge' }, 65 | // }, 66 | // { 67 | // name: 'Google Chrome', 68 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, 69 | // }, 70 | ], 71 | 72 | /* Run your local dev server before starting the tests */ 73 | // webServer: { 74 | // command: 'npm run start', 75 | // url: 'http://127.0.0.1:3000', 76 | // reuseExistingServer: !process.env.CI, 77 | // }, 78 | }); 79 | -------------------------------------------------------------------------------- /examples/accident-counter/src/counter.test.jsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import userEvent from '@testing-library/user-event'; 3 | 4 | import { Counter } from './counter'; 5 | 6 | import '@testing-library/jest-dom/vitest'; 7 | 8 | describe.todo('Counter ', () => { 9 | beforeEach(() => { 10 | render(); 11 | }); 12 | 13 | it('renders with an initial count of 0'); 14 | 15 | it('disables the "Decrement" and "Reset" buttons when the count is 0'); 16 | 17 | it.todo('displays "days" when the count is 0', () => {}); 18 | 19 | it.todo( 20 | 'increments the count when the "Increment" button is clicked', 21 | async () => {}, 22 | ); 23 | 24 | it.todo('displays "day" when the count is 1', async () => {}); 25 | 26 | it.todo( 27 | 'decrements the count when the "Decrement" button is clicked', 28 | async () => {}, 29 | ); 30 | 31 | it.todo('does not allow decrementing below 0', async () => {}); 32 | 33 | it.todo( 34 | 'resets the count when the "Reset" button is clicked', 35 | async () => {}, 36 | ); 37 | 38 | it.todo( 39 | 'disables the "Decrement" and "Reset" buttons when the count is 0', 40 | () => {}, 41 | ); 42 | 43 | it.todo('updates the document title based on the count', async () => {}); 44 | }); 45 | -------------------------------------------------------------------------------- /examples/accident-counter/src/counter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useReducer, useEffect } from 'react'; 3 | import { reducer } from './reducer'; 4 | 5 | export const Counter = () => { 6 | const [state, dispatch] = useReducer(reducer, { count: 0 }); 7 | const unit = state.count === 1 ? 'day' : 'days'; 8 | 9 | useEffect(() => { 10 | window.document.title = `${state.count} ${unit} — Accident Counter`; 11 | }, [state.count]); 12 | 13 | return ( 14 |
15 |
16 |
17 |
18 | {state.count} 19 |
20 |

21 | {unit} since the last 22 | JavaScript-related accident. 23 |

24 |
25 |
26 | 29 | 35 | 41 |
42 |
43 |
44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /examples/accident-counter/src/reducer.test.js: -------------------------------------------------------------------------------- 1 | import { reducer } from './reducer'; 2 | 3 | describe('reducer', () => { 4 | it('should return the initial state', () => { 5 | const initialState = { count: 0 }; 6 | expect(reducer()).toEqual(initialState); 7 | }); 8 | 9 | it('should handle decrement action', () => { 10 | const initialState = { count: 0 }; 11 | const action = { type: 'increment' }; 12 | const expectedState = { count: 1 }; 13 | 14 | expect(reducer(initialState, action)).toEqual(expectedState); 15 | }); 16 | 17 | it('should handle increment action', () => { 18 | const initialState = { count: 1 }; 19 | const action = { type: 'decrement' }; 20 | const expectedState = { count: 0 }; 21 | 22 | expect(reducer(initialState, action)).toEqual(expectedState); 23 | }); 24 | 25 | it('should handle unknown action', () => { 26 | const initialState = { count: 0 }; 27 | const action = { type: 'UNKNOWN' }; 28 | 29 | expect(reducer(initialState, action)).toEqual(initialState); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /examples/accident-counter/src/reducer.ts: -------------------------------------------------------------------------------- 1 | type CounterState = { 2 | count: number; 3 | }; 4 | 5 | type CounterAction = 6 | | { type: 'increment' } 7 | | { type: 'decrement' } 8 | | { type: 'reset' }; 9 | 10 | export function reducer( 11 | state: CounterState = { count: 0 }, 12 | action: CounterAction, 13 | ): CounterState { 14 | if (!action) return state; 15 | 16 | switch (action.type) { 17 | case 'increment': 18 | return { count: state.count + 1 }; 19 | case 'decrement': 20 | if (state.count > 0) { 21 | return { count: state.count - 1 }; 22 | } 23 | return { count: 0 }; 24 | case 'reset': 25 | return { count: 0 }; 26 | default: 27 | return state; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/accident-counter/styles.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | body { 7 | @apply bg-slate-300; 8 | } 9 | 10 | button { 11 | @apply inline-flex cursor-pointer items-center justify-center rounded-md bg-cyan-600 px-4 py-2.5 text-white transition duration-100 ease-in-out hover:bg-cyan-700 active:bg-cyan-800 disabled:cursor-not-allowed disabled:bg-slate-200/50 disabled:text-cyan-600; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/accident-counter/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | import { css } from 'css-configuration'; 5 | 6 | export default defineConfig({ 7 | plugins: [react()], 8 | css, 9 | test: { 10 | environment: 'happy-dom', 11 | globals: true, 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /examples/basic-math/README.md: -------------------------------------------------------------------------------- 1 | # Basic Math 2 | -------------------------------------------------------------------------------- /examples/basic-math/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-math", 3 | "version": "1.0.0", 4 | "description": "Let's talk about adding and subtracting numbers.", 5 | "main": "src/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "vitest --ui", 9 | "test": "vitest" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/stevekinney/testing-javascript.git" 14 | }, 15 | "author": "Steve Kinney ", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/stevekinney/testing-javascript/issues" 19 | }, 20 | "homepage": "https://github.com/stevekinney/testing-javascript#readme", 21 | "devDependencies": { 22 | "@vitest/ui": "^2.1.1", 23 | "vite": "^5.4.5", 24 | "vitest": "^2.1.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/basic-math/src/arithmetic.js: -------------------------------------------------------------------------------- 1 | export const add = () => {}; 2 | 3 | export const subtract = () => {}; 4 | 5 | export const multiply = () => {}; 6 | 7 | export const divide = () => {}; 8 | -------------------------------------------------------------------------------- /examples/basic-math/src/arithmetic.test.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | 3 | describe.todo('add', () => {}); 4 | 5 | describe.todo('subtract', () => {}); 6 | 7 | describe.todo('multiply', () => {}); 8 | 9 | describe.todo('divide', () => {}); 10 | -------------------------------------------------------------------------------- /examples/basic-math/src/counter.js: -------------------------------------------------------------------------------- 1 | let value = 0; 2 | 3 | export const counter = { 4 | get value() { 5 | return value; 6 | }, 7 | increment() { 8 | value++; 9 | }, 10 | decrement() { 11 | value--; 12 | }, 13 | reset() { 14 | value = 0; 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /examples/basic-math/src/counter.test.js: -------------------------------------------------------------------------------- 1 | import { expect, it, describe } from 'vitest'; 2 | import { counter } from './counter'; 3 | 4 | // For added fun, we can try `describe.shuffle`. 5 | describe.todo('Counter', () => { 6 | it('starts at zero', () => { 7 | expect(counter.value).toBe(0); 8 | }); 9 | 10 | it('can increment', () => { 11 | counter.increment(); 12 | expect(counter.value).toBe(1); 13 | }); 14 | 15 | // Let's get this test to *not* fail. 16 | it('can decrement', () => { 17 | counter.increment(); 18 | counter.decrement(); 19 | expect(counter.value).toBe(0); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /examples/basic-math/src/fibonacci.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generates a Fibonacci sequence of a given length. 3 | * @param {number} n The length of the sequence. 4 | * @returns {number[]} The Fibonacci sequence. 5 | */ 6 | export const generateFibonacci = (n) => { 7 | const sequence = [0, 1]; 8 | 9 | for (let i = 2; i < n; i++) { 10 | sequence[i] = sequence[i - 1] + sequence[i - 2]; 11 | } 12 | 13 | return sequence; 14 | }; 15 | -------------------------------------------------------------------------------- /examples/basic-math/src/fibonacci.test.ts: -------------------------------------------------------------------------------- 1 | import { it, expect } from 'vitest'; 2 | import { generateFibonacci } from './fibonacci'; 3 | 4 | it.skip('should generate fibonacci sequence', () => { 5 | const fibonacci = generateFibonacci(10); 6 | expect(fibonacci).toBe([0, 1, 1, 2, 3, 5, 8, 13, 21, 34]); 7 | }); 8 | -------------------------------------------------------------------------------- /examples/basic-math/vitest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | test: { 3 | environment: 'node', 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /examples/calculator/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Calculator 6 | 7 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/calculator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "calculator-example", 3 | "version": "1.0.0", 4 | "description": "A calculator that calculates numbers, but with tests.", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "vite dev", 9 | "test": "vitest" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/stevekinney/testing-javascript.git" 14 | }, 15 | "author": "Steve Kinney ", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/stevekinney/testing-javascript/issues" 19 | }, 20 | "homepage": "https://github.com/stevekinney/testing-javascript#readme", 21 | "devDependencies": { 22 | "@playwright/test": "^1.47.2", 23 | "@testing-library/dom": "^10.4.0", 24 | "@testing-library/user-event": "^14.5.2", 25 | "@vitest/ui": "^2.1.1", 26 | "autoprefixer": "^10.4.20", 27 | "happy-dom": "^15.7.4", 28 | "tailwindcss": "^3.4.11", 29 | "vite": "^5.4.5", 30 | "vitest": "^2.1.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/calculator/playwright.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { defineConfig, devices } from '@playwright/test'; 3 | 4 | /** 5 | * Read environment variables from file. 6 | * https://github.com/motdotla/dotenv 7 | */ 8 | // require('dotenv').config({ path: path.resolve(__dirname, '.env') }); 9 | 10 | /** 11 | * @see https://playwright.dev/docs/test-configuration 12 | */ 13 | export default defineConfig({ 14 | testDir: './tests', 15 | /* Run tests in files in parallel */ 16 | fullyParallel: true, 17 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 18 | forbidOnly: !!process.env.CI, 19 | /* Retry on CI only */ 20 | retries: process.env.CI ? 2 : 0, 21 | /* Opt out of parallel tests on CI. */ 22 | workers: process.env.CI ? 1 : undefined, 23 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 24 | reporter: 'html', 25 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 26 | use: { 27 | /* Base URL to use in actions like `await page.goto('/')`. */ 28 | // baseURL: 'http://127.0.0.1:3000', 29 | 30 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 31 | trace: 'on-first-retry', 32 | }, 33 | 34 | /* Configure projects for major browsers */ 35 | projects: [ 36 | { 37 | name: 'chromium', 38 | use: { ...devices['Desktop Chrome'] }, 39 | }, 40 | 41 | { 42 | name: 'firefox', 43 | use: { ...devices['Desktop Firefox'] }, 44 | }, 45 | 46 | { 47 | name: 'webkit', 48 | use: { ...devices['Desktop Safari'] }, 49 | }, 50 | 51 | /* Test against mobile viewports. */ 52 | // { 53 | // name: 'Mobile Chrome', 54 | // use: { ...devices['Pixel 5'] }, 55 | // }, 56 | // { 57 | // name: 'Mobile Safari', 58 | // use: { ...devices['iPhone 12'] }, 59 | // }, 60 | 61 | /* Test against branded browsers. */ 62 | // { 63 | // name: 'Microsoft Edge', 64 | // use: { ...devices['Desktop Edge'], channel: 'msedge' }, 65 | // }, 66 | // { 67 | // name: 'Google Chrome', 68 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, 69 | // }, 70 | ], 71 | 72 | /* Run your local dev server before starting the tests */ 73 | // webServer: { 74 | // command: 'npm run start', 75 | // url: 'http://127.0.0.1:3000', 76 | // reuseExistingServer: !process.env.CI, 77 | // }, 78 | }); 79 | -------------------------------------------------------------------------------- /examples/calculator/setup-tests.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | -------------------------------------------------------------------------------- /examples/calculator/src/calculator.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | -------------------------------------------------------------------------------- /examples/calculator/src/calculator.js: -------------------------------------------------------------------------------- 1 | import calculator from './calculator.html?raw'; 2 | 3 | /** 4 | * Renders the calculator component into the target element. 5 | * @param {HTMLElement} target The target element to render the calculator into. 6 | */ 7 | export function renderCalculator(target) { 8 | target.innerHTML = calculator; 9 | 10 | let buffer = 0; 11 | 12 | /** @type {HTMLInputElement} */ 13 | const display = target.querySelector('#display'); 14 | 15 | /** @type {NodeListOf} */ 16 | const numbers = target.querySelectorAll('.number'); 17 | 18 | /** @type {NodeListOf} */ 19 | const operators = target.querySelectorAll('.operator'); 20 | 21 | /** @type {HTMLButtonElement} */ 22 | const clear = target.querySelector('#clear'); 23 | 24 | /** @type {HTMLButtonElement} */ 25 | const equals = target.querySelector('#equals'); 26 | 27 | numbers.forEach((number) => { 28 | number.addEventListener('click', () => { 29 | display.value += number.dataset.value; 30 | }); 31 | }); 32 | 33 | operators.forEach((operator) => { 34 | operator.addEventListener('click', () => { 35 | buffer = display.valueAsNumber; 36 | display.value = ''; 37 | }); 38 | }); 39 | 40 | clear.addEventListener('click', () => { 41 | buffer = 0; 42 | display.value = ''; 43 | }); 44 | 45 | equals.addEventListener('click', () => { 46 | const result = buffer + display.valueAsNumber; 47 | display.value = String(result); 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /examples/calculator/src/calculator.test.js: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, it } from 'vitest'; 2 | import { fireEvent } from '@testing-library/dom'; 3 | import { renderCalculator } from './calculator.js'; 4 | 5 | it('is running our test in a browser-like environment', () => { 6 | expect(typeof window).not.toBe('undefined'); 7 | }); 8 | 9 | describe('Calculator', () => { 10 | let display; 11 | 12 | beforeEach(() => { 13 | renderCalculator(document.body); 14 | 15 | display = document.getElementById('display'); 16 | }); 17 | 18 | it('displays number when a number button is clicked', () => { 19 | /** @type {NodeListOf} */ 20 | const [button] = document.querySelectorAll('button'); 21 | const value = button.dataset.value; 22 | 23 | fireEvent.click(button); 24 | 25 | expect(display.value).toBe(value); 26 | }); 27 | 28 | it('display the sum of multiple numbers when the equals button is clicked', () => { 29 | const one = document.getElementById('digit-1'); 30 | const two = document.getElementById('digit-2'); 31 | const three = document.getElementById('digit-3'); 32 | 33 | fireEvent.click(one); 34 | fireEvent.click(two); 35 | fireEvent.click(three); 36 | 37 | expect(display.value).toBe('123'); 38 | }); 39 | 40 | it('supports addings two numbers and displaying the result', () => { 41 | const one = document.getElementById('digit-1'); 42 | const two = document.getElementById('digit-2'); 43 | const add = document.getElementById('add'); 44 | const equals = document.getElementById('equals'); 45 | 46 | fireEvent.click(one); 47 | fireEvent.click(add); 48 | fireEvent.click(two); 49 | fireEvent.click(equals); 50 | 51 | expect(display.value).toBe('3'); 52 | }); 53 | 54 | it('clears the display when the clear button is clicked', () => { 55 | const one = document.getElementById('digit-1'); 56 | const clear = document.getElementById('clear'); 57 | 58 | fireEvent.click(one); 59 | fireEvent.click(clear); 60 | 61 | expect(display.value).toBe(''); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /examples/calculator/styles.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | body { 7 | @apply m-0 flex h-screen items-center justify-center bg-purple-50 font-sans; 8 | } 9 | 10 | button { 11 | @apply flex cursor-pointer items-center justify-center rounded-xl bg-slate-600 p-4 text-xl text-white transition duration-100 ease-in-out hover:bg-slate-700 active:bg-slate-800; 12 | } 13 | } 14 | 15 | @layer components { 16 | .calculator { 17 | @apply grid grid-cols-4 gap-2 rounded-xl bg-slate-950 p-4 shadow-lg; 18 | } 19 | 20 | .number { 21 | @apply flex cursor-pointer items-center justify-center rounded-xl bg-slate-600 p-4 text-xl text-white transition duration-100 ease-in-out hover:bg-slate-700 active:bg-slate-800; 22 | &[data-value='0'] { 23 | @apply col-span-3; 24 | } 25 | } 26 | 27 | .operator { 28 | @apply bg-orange-500 hover:bg-orange-600 active:bg-orange-700; 29 | } 30 | 31 | #equals { 32 | @apply col-span-2 col-start-3 bg-green-500 hover:bg-green-600 active:bg-green-700; 33 | } 34 | 35 | #clear { 36 | @apply bg-red-500 hover:bg-red-600 active:bg-red-700; 37 | } 38 | 39 | .display { 40 | @apply col-span-4 block rounded-md border-cyan-200 bg-cyan-100 p-4 text-right text-2xl font-semibold text-cyan-800 outline outline-2 outline-transparent; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/calculator/tests/calculator.spec.js: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | import { createServer } from 'vite'; 3 | -------------------------------------------------------------------------------- /examples/calculator/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import { css } from 'css-configuration'; 3 | 4 | export default defineConfig({ 5 | assetsInclude: ['**/*.html'], 6 | css, 7 | test: { 8 | environment: 'happy-dom', 9 | globals: true, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /examples/characters/README.md: -------------------------------------------------------------------------------- 1 | # Cool Kids Club 2 | -------------------------------------------------------------------------------- /examples/characters/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-math", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "basic-math", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "uuid": "^10.0.0" 13 | }, 14 | "devDependencies": { 15 | "@vitest/ui": "^2.1.1", 16 | "vite": "^5.4.5", 17 | "vitest": "^2.1.1" 18 | } 19 | }, 20 | "node_modules/@esbuild/aix-ppc64": { 21 | "version": "0.21.5", 22 | "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", 23 | "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", 24 | "cpu": [ 25 | "ppc64" 26 | ], 27 | "dev": true, 28 | "license": "MIT", 29 | "optional": true, 30 | "os": [ 31 | "aix" 32 | ], 33 | "engines": { 34 | "node": ">=12" 35 | } 36 | }, 37 | "node_modules/@esbuild/android-arm": { 38 | "version": "0.21.5", 39 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", 40 | "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", 41 | "cpu": [ 42 | "arm" 43 | ], 44 | "dev": true, 45 | "license": "MIT", 46 | "optional": true, 47 | "os": [ 48 | "android" 49 | ], 50 | "engines": { 51 | "node": ">=12" 52 | } 53 | }, 54 | "node_modules/@esbuild/android-arm64": { 55 | "version": "0.21.5", 56 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", 57 | "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", 58 | "cpu": [ 59 | "arm64" 60 | ], 61 | "dev": true, 62 | "license": "MIT", 63 | "optional": true, 64 | "os": [ 65 | "android" 66 | ], 67 | "engines": { 68 | "node": ">=12" 69 | } 70 | }, 71 | "node_modules/@esbuild/android-x64": { 72 | "version": "0.21.5", 73 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", 74 | "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", 75 | "cpu": [ 76 | "x64" 77 | ], 78 | "dev": true, 79 | "license": "MIT", 80 | "optional": true, 81 | "os": [ 82 | "android" 83 | ], 84 | "engines": { 85 | "node": ">=12" 86 | } 87 | }, 88 | "node_modules/@esbuild/darwin-arm64": { 89 | "version": "0.21.5", 90 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", 91 | "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", 92 | "cpu": [ 93 | "arm64" 94 | ], 95 | "dev": true, 96 | "license": "MIT", 97 | "optional": true, 98 | "os": [ 99 | "darwin" 100 | ], 101 | "engines": { 102 | "node": ">=12" 103 | } 104 | }, 105 | "node_modules/@esbuild/darwin-x64": { 106 | "version": "0.21.5", 107 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", 108 | "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", 109 | "cpu": [ 110 | "x64" 111 | ], 112 | "dev": true, 113 | "license": "MIT", 114 | "optional": true, 115 | "os": [ 116 | "darwin" 117 | ], 118 | "engines": { 119 | "node": ">=12" 120 | } 121 | }, 122 | "node_modules/@esbuild/freebsd-arm64": { 123 | "version": "0.21.5", 124 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", 125 | "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", 126 | "cpu": [ 127 | "arm64" 128 | ], 129 | "dev": true, 130 | "license": "MIT", 131 | "optional": true, 132 | "os": [ 133 | "freebsd" 134 | ], 135 | "engines": { 136 | "node": ">=12" 137 | } 138 | }, 139 | "node_modules/@esbuild/freebsd-x64": { 140 | "version": "0.21.5", 141 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", 142 | "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", 143 | "cpu": [ 144 | "x64" 145 | ], 146 | "dev": true, 147 | "license": "MIT", 148 | "optional": true, 149 | "os": [ 150 | "freebsd" 151 | ], 152 | "engines": { 153 | "node": ">=12" 154 | } 155 | }, 156 | "node_modules/@esbuild/linux-arm": { 157 | "version": "0.21.5", 158 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", 159 | "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", 160 | "cpu": [ 161 | "arm" 162 | ], 163 | "dev": true, 164 | "license": "MIT", 165 | "optional": true, 166 | "os": [ 167 | "linux" 168 | ], 169 | "engines": { 170 | "node": ">=12" 171 | } 172 | }, 173 | "node_modules/@esbuild/linux-arm64": { 174 | "version": "0.21.5", 175 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", 176 | "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", 177 | "cpu": [ 178 | "arm64" 179 | ], 180 | "dev": true, 181 | "license": "MIT", 182 | "optional": true, 183 | "os": [ 184 | "linux" 185 | ], 186 | "engines": { 187 | "node": ">=12" 188 | } 189 | }, 190 | "node_modules/@esbuild/linux-ia32": { 191 | "version": "0.21.5", 192 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", 193 | "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", 194 | "cpu": [ 195 | "ia32" 196 | ], 197 | "dev": true, 198 | "license": "MIT", 199 | "optional": true, 200 | "os": [ 201 | "linux" 202 | ], 203 | "engines": { 204 | "node": ">=12" 205 | } 206 | }, 207 | "node_modules/@esbuild/linux-loong64": { 208 | "version": "0.21.5", 209 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", 210 | "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", 211 | "cpu": [ 212 | "loong64" 213 | ], 214 | "dev": true, 215 | "license": "MIT", 216 | "optional": true, 217 | "os": [ 218 | "linux" 219 | ], 220 | "engines": { 221 | "node": ">=12" 222 | } 223 | }, 224 | "node_modules/@esbuild/linux-mips64el": { 225 | "version": "0.21.5", 226 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", 227 | "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", 228 | "cpu": [ 229 | "mips64el" 230 | ], 231 | "dev": true, 232 | "license": "MIT", 233 | "optional": true, 234 | "os": [ 235 | "linux" 236 | ], 237 | "engines": { 238 | "node": ">=12" 239 | } 240 | }, 241 | "node_modules/@esbuild/linux-ppc64": { 242 | "version": "0.21.5", 243 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", 244 | "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", 245 | "cpu": [ 246 | "ppc64" 247 | ], 248 | "dev": true, 249 | "license": "MIT", 250 | "optional": true, 251 | "os": [ 252 | "linux" 253 | ], 254 | "engines": { 255 | "node": ">=12" 256 | } 257 | }, 258 | "node_modules/@esbuild/linux-riscv64": { 259 | "version": "0.21.5", 260 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", 261 | "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", 262 | "cpu": [ 263 | "riscv64" 264 | ], 265 | "dev": true, 266 | "license": "MIT", 267 | "optional": true, 268 | "os": [ 269 | "linux" 270 | ], 271 | "engines": { 272 | "node": ">=12" 273 | } 274 | }, 275 | "node_modules/@esbuild/linux-s390x": { 276 | "version": "0.21.5", 277 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", 278 | "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", 279 | "cpu": [ 280 | "s390x" 281 | ], 282 | "dev": true, 283 | "license": "MIT", 284 | "optional": true, 285 | "os": [ 286 | "linux" 287 | ], 288 | "engines": { 289 | "node": ">=12" 290 | } 291 | }, 292 | "node_modules/@esbuild/linux-x64": { 293 | "version": "0.21.5", 294 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", 295 | "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", 296 | "cpu": [ 297 | "x64" 298 | ], 299 | "dev": true, 300 | "license": "MIT", 301 | "optional": true, 302 | "os": [ 303 | "linux" 304 | ], 305 | "engines": { 306 | "node": ">=12" 307 | } 308 | }, 309 | "node_modules/@esbuild/netbsd-x64": { 310 | "version": "0.21.5", 311 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", 312 | "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", 313 | "cpu": [ 314 | "x64" 315 | ], 316 | "dev": true, 317 | "license": "MIT", 318 | "optional": true, 319 | "os": [ 320 | "netbsd" 321 | ], 322 | "engines": { 323 | "node": ">=12" 324 | } 325 | }, 326 | "node_modules/@esbuild/openbsd-x64": { 327 | "version": "0.21.5", 328 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", 329 | "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", 330 | "cpu": [ 331 | "x64" 332 | ], 333 | "dev": true, 334 | "license": "MIT", 335 | "optional": true, 336 | "os": [ 337 | "openbsd" 338 | ], 339 | "engines": { 340 | "node": ">=12" 341 | } 342 | }, 343 | "node_modules/@esbuild/sunos-x64": { 344 | "version": "0.21.5", 345 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", 346 | "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", 347 | "cpu": [ 348 | "x64" 349 | ], 350 | "dev": true, 351 | "license": "MIT", 352 | "optional": true, 353 | "os": [ 354 | "sunos" 355 | ], 356 | "engines": { 357 | "node": ">=12" 358 | } 359 | }, 360 | "node_modules/@esbuild/win32-arm64": { 361 | "version": "0.21.5", 362 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", 363 | "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", 364 | "cpu": [ 365 | "arm64" 366 | ], 367 | "dev": true, 368 | "license": "MIT", 369 | "optional": true, 370 | "os": [ 371 | "win32" 372 | ], 373 | "engines": { 374 | "node": ">=12" 375 | } 376 | }, 377 | "node_modules/@esbuild/win32-ia32": { 378 | "version": "0.21.5", 379 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", 380 | "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", 381 | "cpu": [ 382 | "ia32" 383 | ], 384 | "dev": true, 385 | "license": "MIT", 386 | "optional": true, 387 | "os": [ 388 | "win32" 389 | ], 390 | "engines": { 391 | "node": ">=12" 392 | } 393 | }, 394 | "node_modules/@esbuild/win32-x64": { 395 | "version": "0.21.5", 396 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", 397 | "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", 398 | "cpu": [ 399 | "x64" 400 | ], 401 | "dev": true, 402 | "license": "MIT", 403 | "optional": true, 404 | "os": [ 405 | "win32" 406 | ], 407 | "engines": { 408 | "node": ">=12" 409 | } 410 | }, 411 | "node_modules/@jridgewell/sourcemap-codec": { 412 | "version": "1.5.0", 413 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", 414 | "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", 415 | "dev": true, 416 | "license": "MIT" 417 | }, 418 | "node_modules/@polka/url": { 419 | "version": "1.0.0-next.25", 420 | "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz", 421 | "integrity": "sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==", 422 | "dev": true, 423 | "license": "MIT" 424 | }, 425 | "node_modules/@rollup/rollup-android-arm-eabi": { 426 | "version": "4.21.3", 427 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.3.tgz", 428 | "integrity": "sha512-MmKSfaB9GX+zXl6E8z4koOr/xU63AMVleLEa64v7R0QF/ZloMs5vcD1sHgM64GXXS1csaJutG+ddtzcueI/BLg==", 429 | "cpu": [ 430 | "arm" 431 | ], 432 | "dev": true, 433 | "license": "MIT", 434 | "optional": true, 435 | "os": [ 436 | "android" 437 | ] 438 | }, 439 | "node_modules/@rollup/rollup-android-arm64": { 440 | "version": "4.21.3", 441 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.3.tgz", 442 | "integrity": "sha512-zrt8ecH07PE3sB4jPOggweBjJMzI1JG5xI2DIsUbkA+7K+Gkjys6eV7i9pOenNSDJH3eOr/jLb/PzqtmdwDq5g==", 443 | "cpu": [ 444 | "arm64" 445 | ], 446 | "dev": true, 447 | "license": "MIT", 448 | "optional": true, 449 | "os": [ 450 | "android" 451 | ] 452 | }, 453 | "node_modules/@rollup/rollup-darwin-arm64": { 454 | "version": "4.21.3", 455 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.3.tgz", 456 | "integrity": "sha512-P0UxIOrKNBFTQaXTxOH4RxuEBVCgEA5UTNV6Yz7z9QHnUJ7eLX9reOd/NYMO3+XZO2cco19mXTxDMXxit4R/eQ==", 457 | "cpu": [ 458 | "arm64" 459 | ], 460 | "dev": true, 461 | "license": "MIT", 462 | "optional": true, 463 | "os": [ 464 | "darwin" 465 | ] 466 | }, 467 | "node_modules/@rollup/rollup-darwin-x64": { 468 | "version": "4.21.3", 469 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.3.tgz", 470 | "integrity": "sha512-L1M0vKGO5ASKntqtsFEjTq/fD91vAqnzeaF6sfNAy55aD+Hi2pBI5DKwCO+UNDQHWsDViJLqshxOahXyLSh3EA==", 471 | "cpu": [ 472 | "x64" 473 | ], 474 | "dev": true, 475 | "license": "MIT", 476 | "optional": true, 477 | "os": [ 478 | "darwin" 479 | ] 480 | }, 481 | "node_modules/@rollup/rollup-linux-arm-gnueabihf": { 482 | "version": "4.21.3", 483 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.3.tgz", 484 | "integrity": "sha512-btVgIsCjuYFKUjopPoWiDqmoUXQDiW2A4C3Mtmp5vACm7/GnyuprqIDPNczeyR5W8rTXEbkmrJux7cJmD99D2g==", 485 | "cpu": [ 486 | "arm" 487 | ], 488 | "dev": true, 489 | "license": "MIT", 490 | "optional": true, 491 | "os": [ 492 | "linux" 493 | ] 494 | }, 495 | "node_modules/@rollup/rollup-linux-arm-musleabihf": { 496 | "version": "4.21.3", 497 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.3.tgz", 498 | "integrity": "sha512-zmjbSphplZlau6ZTkxd3+NMtE4UKVy7U4aVFMmHcgO5CUbw17ZP6QCgyxhzGaU/wFFdTfiojjbLG3/0p9HhAqA==", 499 | "cpu": [ 500 | "arm" 501 | ], 502 | "dev": true, 503 | "license": "MIT", 504 | "optional": true, 505 | "os": [ 506 | "linux" 507 | ] 508 | }, 509 | "node_modules/@rollup/rollup-linux-arm64-gnu": { 510 | "version": "4.21.3", 511 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.3.tgz", 512 | "integrity": "sha512-nSZfcZtAnQPRZmUkUQwZq2OjQciR6tEoJaZVFvLHsj0MF6QhNMg0fQ6mUOsiCUpTqxTx0/O6gX0V/nYc7LrgPw==", 513 | "cpu": [ 514 | "arm64" 515 | ], 516 | "dev": true, 517 | "license": "MIT", 518 | "optional": true, 519 | "os": [ 520 | "linux" 521 | ] 522 | }, 523 | "node_modules/@rollup/rollup-linux-arm64-musl": { 524 | "version": "4.21.3", 525 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.3.tgz", 526 | "integrity": "sha512-MnvSPGO8KJXIMGlQDYfvYS3IosFN2rKsvxRpPO2l2cum+Z3exiExLwVU+GExL96pn8IP+GdH8Tz70EpBhO0sIQ==", 527 | "cpu": [ 528 | "arm64" 529 | ], 530 | "dev": true, 531 | "license": "MIT", 532 | "optional": true, 533 | "os": [ 534 | "linux" 535 | ] 536 | }, 537 | "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { 538 | "version": "4.21.3", 539 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.3.tgz", 540 | "integrity": "sha512-+W+p/9QNDr2vE2AXU0qIy0qQE75E8RTwTwgqS2G5CRQ11vzq0tbnfBd6brWhS9bCRjAjepJe2fvvkvS3dno+iw==", 541 | "cpu": [ 542 | "ppc64" 543 | ], 544 | "dev": true, 545 | "license": "MIT", 546 | "optional": true, 547 | "os": [ 548 | "linux" 549 | ] 550 | }, 551 | "node_modules/@rollup/rollup-linux-riscv64-gnu": { 552 | "version": "4.21.3", 553 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.3.tgz", 554 | "integrity": "sha512-yXH6K6KfqGXaxHrtr+Uoy+JpNlUlI46BKVyonGiaD74ravdnF9BUNC+vV+SIuB96hUMGShhKV693rF9QDfO6nQ==", 555 | "cpu": [ 556 | "riscv64" 557 | ], 558 | "dev": true, 559 | "license": "MIT", 560 | "optional": true, 561 | "os": [ 562 | "linux" 563 | ] 564 | }, 565 | "node_modules/@rollup/rollup-linux-s390x-gnu": { 566 | "version": "4.21.3", 567 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.3.tgz", 568 | "integrity": "sha512-R8cwY9wcnApN/KDYWTH4gV/ypvy9yZUHlbJvfaiXSB48JO3KpwSpjOGqO4jnGkLDSk1hgjYkTbTt6Q7uvPf8eg==", 569 | "cpu": [ 570 | "s390x" 571 | ], 572 | "dev": true, 573 | "license": "MIT", 574 | "optional": true, 575 | "os": [ 576 | "linux" 577 | ] 578 | }, 579 | "node_modules/@rollup/rollup-linux-x64-gnu": { 580 | "version": "4.21.3", 581 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.3.tgz", 582 | "integrity": "sha512-kZPbX/NOPh0vhS5sI+dR8L1bU2cSO9FgxwM8r7wHzGydzfSjLRCFAT87GR5U9scj2rhzN3JPYVC7NoBbl4FZ0g==", 583 | "cpu": [ 584 | "x64" 585 | ], 586 | "dev": true, 587 | "license": "MIT", 588 | "optional": true, 589 | "os": [ 590 | "linux" 591 | ] 592 | }, 593 | "node_modules/@rollup/rollup-linux-x64-musl": { 594 | "version": "4.21.3", 595 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.3.tgz", 596 | "integrity": "sha512-S0Yq+xA1VEH66uiMNhijsWAafffydd2X5b77eLHfRmfLsRSpbiAWiRHV6DEpz6aOToPsgid7TI9rGd6zB1rhbg==", 597 | "cpu": [ 598 | "x64" 599 | ], 600 | "dev": true, 601 | "license": "MIT", 602 | "optional": true, 603 | "os": [ 604 | "linux" 605 | ] 606 | }, 607 | "node_modules/@rollup/rollup-win32-arm64-msvc": { 608 | "version": "4.21.3", 609 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.3.tgz", 610 | "integrity": "sha512-9isNzeL34yquCPyerog+IMCNxKR8XYmGd0tHSV+OVx0TmE0aJOo9uw4fZfUuk2qxobP5sug6vNdZR6u7Mw7Q+Q==", 611 | "cpu": [ 612 | "arm64" 613 | ], 614 | "dev": true, 615 | "license": "MIT", 616 | "optional": true, 617 | "os": [ 618 | "win32" 619 | ] 620 | }, 621 | "node_modules/@rollup/rollup-win32-ia32-msvc": { 622 | "version": "4.21.3", 623 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.3.tgz", 624 | "integrity": "sha512-nMIdKnfZfzn1Vsk+RuOvl43ONTZXoAPUUxgcU0tXooqg4YrAqzfKzVenqqk2g5efWh46/D28cKFrOzDSW28gTA==", 625 | "cpu": [ 626 | "ia32" 627 | ], 628 | "dev": true, 629 | "license": "MIT", 630 | "optional": true, 631 | "os": [ 632 | "win32" 633 | ] 634 | }, 635 | "node_modules/@rollup/rollup-win32-x64-msvc": { 636 | "version": "4.21.3", 637 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.3.tgz", 638 | "integrity": "sha512-fOvu7PCQjAj4eWDEuD8Xz5gpzFqXzGlxHZozHP4b9Jxv9APtdxL6STqztDzMLuRXEc4UpXGGhx029Xgm91QBeA==", 639 | "cpu": [ 640 | "x64" 641 | ], 642 | "dev": true, 643 | "license": "MIT", 644 | "optional": true, 645 | "os": [ 646 | "win32" 647 | ] 648 | }, 649 | "node_modules/@types/estree": { 650 | "version": "1.0.5", 651 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", 652 | "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", 653 | "dev": true, 654 | "license": "MIT" 655 | }, 656 | "node_modules/@vitest/expect": { 657 | "version": "2.1.1", 658 | "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.1.tgz", 659 | "integrity": "sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==", 660 | "dev": true, 661 | "license": "MIT", 662 | "dependencies": { 663 | "@vitest/spy": "2.1.1", 664 | "@vitest/utils": "2.1.1", 665 | "chai": "^5.1.1", 666 | "tinyrainbow": "^1.2.0" 667 | }, 668 | "funding": { 669 | "url": "https://opencollective.com/vitest" 670 | } 671 | }, 672 | "node_modules/@vitest/mocker": { 673 | "version": "2.1.1", 674 | "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.1.tgz", 675 | "integrity": "sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==", 676 | "dev": true, 677 | "license": "MIT", 678 | "dependencies": { 679 | "@vitest/spy": "^2.1.0-beta.1", 680 | "estree-walker": "^3.0.3", 681 | "magic-string": "^0.30.11" 682 | }, 683 | "funding": { 684 | "url": "https://opencollective.com/vitest" 685 | }, 686 | "peerDependencies": { 687 | "@vitest/spy": "2.1.1", 688 | "msw": "^2.3.5", 689 | "vite": "^5.0.0" 690 | }, 691 | "peerDependenciesMeta": { 692 | "msw": { 693 | "optional": true 694 | }, 695 | "vite": { 696 | "optional": true 697 | } 698 | } 699 | }, 700 | "node_modules/@vitest/pretty-format": { 701 | "version": "2.1.1", 702 | "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.1.tgz", 703 | "integrity": "sha512-SjxPFOtuINDUW8/UkElJYQSFtnWX7tMksSGW0vfjxMneFqxVr8YJ979QpMbDW7g+BIiq88RAGDjf7en6rvLPPQ==", 704 | "dev": true, 705 | "license": "MIT", 706 | "dependencies": { 707 | "tinyrainbow": "^1.2.0" 708 | }, 709 | "funding": { 710 | "url": "https://opencollective.com/vitest" 711 | } 712 | }, 713 | "node_modules/@vitest/runner": { 714 | "version": "2.1.1", 715 | "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.1.tgz", 716 | "integrity": "sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==", 717 | "dev": true, 718 | "license": "MIT", 719 | "dependencies": { 720 | "@vitest/utils": "2.1.1", 721 | "pathe": "^1.1.2" 722 | }, 723 | "funding": { 724 | "url": "https://opencollective.com/vitest" 725 | } 726 | }, 727 | "node_modules/@vitest/snapshot": { 728 | "version": "2.1.1", 729 | "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.1.tgz", 730 | "integrity": "sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==", 731 | "dev": true, 732 | "license": "MIT", 733 | "dependencies": { 734 | "@vitest/pretty-format": "2.1.1", 735 | "magic-string": "^0.30.11", 736 | "pathe": "^1.1.2" 737 | }, 738 | "funding": { 739 | "url": "https://opencollective.com/vitest" 740 | } 741 | }, 742 | "node_modules/@vitest/spy": { 743 | "version": "2.1.1", 744 | "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.1.tgz", 745 | "integrity": "sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==", 746 | "dev": true, 747 | "license": "MIT", 748 | "dependencies": { 749 | "tinyspy": "^3.0.0" 750 | }, 751 | "funding": { 752 | "url": "https://opencollective.com/vitest" 753 | } 754 | }, 755 | "node_modules/@vitest/ui": { 756 | "version": "2.1.1", 757 | "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-2.1.1.tgz", 758 | "integrity": "sha512-IIxo2LkQDA+1TZdPLYPclzsXukBWd5dX2CKpGqH8CCt8Wh0ZuDn4+vuQ9qlppEju6/igDGzjWF/zyorfsf+nHg==", 759 | "dev": true, 760 | "license": "MIT", 761 | "dependencies": { 762 | "@vitest/utils": "2.1.1", 763 | "fflate": "^0.8.2", 764 | "flatted": "^3.3.1", 765 | "pathe": "^1.1.2", 766 | "sirv": "^2.0.4", 767 | "tinyglobby": "^0.2.6", 768 | "tinyrainbow": "^1.2.0" 769 | }, 770 | "funding": { 771 | "url": "https://opencollective.com/vitest" 772 | }, 773 | "peerDependencies": { 774 | "vitest": "2.1.1" 775 | } 776 | }, 777 | "node_modules/@vitest/utils": { 778 | "version": "2.1.1", 779 | "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.1.tgz", 780 | "integrity": "sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==", 781 | "dev": true, 782 | "license": "MIT", 783 | "dependencies": { 784 | "@vitest/pretty-format": "2.1.1", 785 | "loupe": "^3.1.1", 786 | "tinyrainbow": "^1.2.0" 787 | }, 788 | "funding": { 789 | "url": "https://opencollective.com/vitest" 790 | } 791 | }, 792 | "node_modules/assertion-error": { 793 | "version": "2.0.1", 794 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", 795 | "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", 796 | "dev": true, 797 | "license": "MIT", 798 | "engines": { 799 | "node": ">=12" 800 | } 801 | }, 802 | "node_modules/cac": { 803 | "version": "6.7.14", 804 | "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", 805 | "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", 806 | "dev": true, 807 | "license": "MIT", 808 | "engines": { 809 | "node": ">=8" 810 | } 811 | }, 812 | "node_modules/chai": { 813 | "version": "5.1.1", 814 | "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", 815 | "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", 816 | "dev": true, 817 | "license": "MIT", 818 | "dependencies": { 819 | "assertion-error": "^2.0.1", 820 | "check-error": "^2.1.1", 821 | "deep-eql": "^5.0.1", 822 | "loupe": "^3.1.0", 823 | "pathval": "^2.0.0" 824 | }, 825 | "engines": { 826 | "node": ">=12" 827 | } 828 | }, 829 | "node_modules/check-error": { 830 | "version": "2.1.1", 831 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", 832 | "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", 833 | "dev": true, 834 | "license": "MIT", 835 | "engines": { 836 | "node": ">= 16" 837 | } 838 | }, 839 | "node_modules/debug": { 840 | "version": "4.3.7", 841 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", 842 | "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", 843 | "dev": true, 844 | "license": "MIT", 845 | "dependencies": { 846 | "ms": "^2.1.3" 847 | }, 848 | "engines": { 849 | "node": ">=6.0" 850 | }, 851 | "peerDependenciesMeta": { 852 | "supports-color": { 853 | "optional": true 854 | } 855 | } 856 | }, 857 | "node_modules/deep-eql": { 858 | "version": "5.0.2", 859 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", 860 | "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", 861 | "dev": true, 862 | "license": "MIT", 863 | "engines": { 864 | "node": ">=6" 865 | } 866 | }, 867 | "node_modules/esbuild": { 868 | "version": "0.21.5", 869 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", 870 | "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", 871 | "dev": true, 872 | "hasInstallScript": true, 873 | "license": "MIT", 874 | "bin": { 875 | "esbuild": "bin/esbuild" 876 | }, 877 | "engines": { 878 | "node": ">=12" 879 | }, 880 | "optionalDependencies": { 881 | "@esbuild/aix-ppc64": "0.21.5", 882 | "@esbuild/android-arm": "0.21.5", 883 | "@esbuild/android-arm64": "0.21.5", 884 | "@esbuild/android-x64": "0.21.5", 885 | "@esbuild/darwin-arm64": "0.21.5", 886 | "@esbuild/darwin-x64": "0.21.5", 887 | "@esbuild/freebsd-arm64": "0.21.5", 888 | "@esbuild/freebsd-x64": "0.21.5", 889 | "@esbuild/linux-arm": "0.21.5", 890 | "@esbuild/linux-arm64": "0.21.5", 891 | "@esbuild/linux-ia32": "0.21.5", 892 | "@esbuild/linux-loong64": "0.21.5", 893 | "@esbuild/linux-mips64el": "0.21.5", 894 | "@esbuild/linux-ppc64": "0.21.5", 895 | "@esbuild/linux-riscv64": "0.21.5", 896 | "@esbuild/linux-s390x": "0.21.5", 897 | "@esbuild/linux-x64": "0.21.5", 898 | "@esbuild/netbsd-x64": "0.21.5", 899 | "@esbuild/openbsd-x64": "0.21.5", 900 | "@esbuild/sunos-x64": "0.21.5", 901 | "@esbuild/win32-arm64": "0.21.5", 902 | "@esbuild/win32-ia32": "0.21.5", 903 | "@esbuild/win32-x64": "0.21.5" 904 | } 905 | }, 906 | "node_modules/estree-walker": { 907 | "version": "3.0.3", 908 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", 909 | "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", 910 | "dev": true, 911 | "license": "MIT", 912 | "dependencies": { 913 | "@types/estree": "^1.0.0" 914 | } 915 | }, 916 | "node_modules/fdir": { 917 | "version": "6.3.0", 918 | "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.3.0.tgz", 919 | "integrity": "sha512-QOnuT+BOtivR77wYvCWHfGt9s4Pz1VIMbD463vegT5MLqNXy8rYFT/lPVEqf/bhYeT6qmqrNHhsX+rWwe3rOCQ==", 920 | "dev": true, 921 | "license": "MIT", 922 | "peerDependencies": { 923 | "picomatch": "^3 || ^4" 924 | }, 925 | "peerDependenciesMeta": { 926 | "picomatch": { 927 | "optional": true 928 | } 929 | } 930 | }, 931 | "node_modules/fflate": { 932 | "version": "0.8.2", 933 | "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", 934 | "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", 935 | "dev": true, 936 | "license": "MIT" 937 | }, 938 | "node_modules/flatted": { 939 | "version": "3.3.1", 940 | "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", 941 | "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", 942 | "dev": true, 943 | "license": "ISC" 944 | }, 945 | "node_modules/fsevents": { 946 | "version": "2.3.3", 947 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 948 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 949 | "dev": true, 950 | "hasInstallScript": true, 951 | "license": "MIT", 952 | "optional": true, 953 | "os": [ 954 | "darwin" 955 | ], 956 | "engines": { 957 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 958 | } 959 | }, 960 | "node_modules/get-func-name": { 961 | "version": "2.0.2", 962 | "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", 963 | "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", 964 | "dev": true, 965 | "license": "MIT", 966 | "engines": { 967 | "node": "*" 968 | } 969 | }, 970 | "node_modules/loupe": { 971 | "version": "3.1.1", 972 | "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", 973 | "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", 974 | "dev": true, 975 | "license": "MIT", 976 | "dependencies": { 977 | "get-func-name": "^2.0.1" 978 | } 979 | }, 980 | "node_modules/magic-string": { 981 | "version": "0.30.11", 982 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", 983 | "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", 984 | "dev": true, 985 | "license": "MIT", 986 | "dependencies": { 987 | "@jridgewell/sourcemap-codec": "^1.5.0" 988 | } 989 | }, 990 | "node_modules/mrmime": { 991 | "version": "2.0.0", 992 | "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", 993 | "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", 994 | "dev": true, 995 | "license": "MIT", 996 | "engines": { 997 | "node": ">=10" 998 | } 999 | }, 1000 | "node_modules/ms": { 1001 | "version": "2.1.3", 1002 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 1003 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 1004 | "dev": true, 1005 | "license": "MIT" 1006 | }, 1007 | "node_modules/nanoid": { 1008 | "version": "3.3.7", 1009 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", 1010 | "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", 1011 | "dev": true, 1012 | "funding": [ 1013 | { 1014 | "type": "github", 1015 | "url": "https://github.com/sponsors/ai" 1016 | } 1017 | ], 1018 | "license": "MIT", 1019 | "bin": { 1020 | "nanoid": "bin/nanoid.cjs" 1021 | }, 1022 | "engines": { 1023 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 1024 | } 1025 | }, 1026 | "node_modules/pathe": { 1027 | "version": "1.1.2", 1028 | "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", 1029 | "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", 1030 | "dev": true, 1031 | "license": "MIT" 1032 | }, 1033 | "node_modules/pathval": { 1034 | "version": "2.0.0", 1035 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", 1036 | "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", 1037 | "dev": true, 1038 | "license": "MIT", 1039 | "engines": { 1040 | "node": ">= 14.16" 1041 | } 1042 | }, 1043 | "node_modules/picocolors": { 1044 | "version": "1.1.0", 1045 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", 1046 | "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", 1047 | "dev": true, 1048 | "license": "ISC" 1049 | }, 1050 | "node_modules/picomatch": { 1051 | "version": "4.0.2", 1052 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", 1053 | "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", 1054 | "dev": true, 1055 | "license": "MIT", 1056 | "engines": { 1057 | "node": ">=12" 1058 | }, 1059 | "funding": { 1060 | "url": "https://github.com/sponsors/jonschlinkert" 1061 | } 1062 | }, 1063 | "node_modules/postcss": { 1064 | "version": "8.4.47", 1065 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", 1066 | "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", 1067 | "dev": true, 1068 | "funding": [ 1069 | { 1070 | "type": "opencollective", 1071 | "url": "https://opencollective.com/postcss/" 1072 | }, 1073 | { 1074 | "type": "tidelift", 1075 | "url": "https://tidelift.com/funding/github/npm/postcss" 1076 | }, 1077 | { 1078 | "type": "github", 1079 | "url": "https://github.com/sponsors/ai" 1080 | } 1081 | ], 1082 | "license": "MIT", 1083 | "dependencies": { 1084 | "nanoid": "^3.3.7", 1085 | "picocolors": "^1.1.0", 1086 | "source-map-js": "^1.2.1" 1087 | }, 1088 | "engines": { 1089 | "node": "^10 || ^12 || >=14" 1090 | } 1091 | }, 1092 | "node_modules/rollup": { 1093 | "version": "4.21.3", 1094 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.3.tgz", 1095 | "integrity": "sha512-7sqRtBNnEbcBtMeRVc6VRsJMmpI+JU1z9VTvW8D4gXIYQFz0aLcsE6rRkyghZkLfEgUZgVvOG7A5CVz/VW5GIA==", 1096 | "dev": true, 1097 | "license": "MIT", 1098 | "dependencies": { 1099 | "@types/estree": "1.0.5" 1100 | }, 1101 | "bin": { 1102 | "rollup": "dist/bin/rollup" 1103 | }, 1104 | "engines": { 1105 | "node": ">=18.0.0", 1106 | "npm": ">=8.0.0" 1107 | }, 1108 | "optionalDependencies": { 1109 | "@rollup/rollup-android-arm-eabi": "4.21.3", 1110 | "@rollup/rollup-android-arm64": "4.21.3", 1111 | "@rollup/rollup-darwin-arm64": "4.21.3", 1112 | "@rollup/rollup-darwin-x64": "4.21.3", 1113 | "@rollup/rollup-linux-arm-gnueabihf": "4.21.3", 1114 | "@rollup/rollup-linux-arm-musleabihf": "4.21.3", 1115 | "@rollup/rollup-linux-arm64-gnu": "4.21.3", 1116 | "@rollup/rollup-linux-arm64-musl": "4.21.3", 1117 | "@rollup/rollup-linux-powerpc64le-gnu": "4.21.3", 1118 | "@rollup/rollup-linux-riscv64-gnu": "4.21.3", 1119 | "@rollup/rollup-linux-s390x-gnu": "4.21.3", 1120 | "@rollup/rollup-linux-x64-gnu": "4.21.3", 1121 | "@rollup/rollup-linux-x64-musl": "4.21.3", 1122 | "@rollup/rollup-win32-arm64-msvc": "4.21.3", 1123 | "@rollup/rollup-win32-ia32-msvc": "4.21.3", 1124 | "@rollup/rollup-win32-x64-msvc": "4.21.3", 1125 | "fsevents": "~2.3.2" 1126 | } 1127 | }, 1128 | "node_modules/siginfo": { 1129 | "version": "2.0.0", 1130 | "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", 1131 | "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", 1132 | "dev": true, 1133 | "license": "ISC" 1134 | }, 1135 | "node_modules/sirv": { 1136 | "version": "2.0.4", 1137 | "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", 1138 | "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", 1139 | "dev": true, 1140 | "license": "MIT", 1141 | "dependencies": { 1142 | "@polka/url": "^1.0.0-next.24", 1143 | "mrmime": "^2.0.0", 1144 | "totalist": "^3.0.0" 1145 | }, 1146 | "engines": { 1147 | "node": ">= 10" 1148 | } 1149 | }, 1150 | "node_modules/source-map-js": { 1151 | "version": "1.2.1", 1152 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", 1153 | "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", 1154 | "dev": true, 1155 | "license": "BSD-3-Clause", 1156 | "engines": { 1157 | "node": ">=0.10.0" 1158 | } 1159 | }, 1160 | "node_modules/stackback": { 1161 | "version": "0.0.2", 1162 | "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", 1163 | "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", 1164 | "dev": true, 1165 | "license": "MIT" 1166 | }, 1167 | "node_modules/std-env": { 1168 | "version": "3.7.0", 1169 | "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", 1170 | "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", 1171 | "dev": true, 1172 | "license": "MIT" 1173 | }, 1174 | "node_modules/tinybench": { 1175 | "version": "2.9.0", 1176 | "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", 1177 | "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", 1178 | "dev": true, 1179 | "license": "MIT" 1180 | }, 1181 | "node_modules/tinyexec": { 1182 | "version": "0.3.0", 1183 | "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.0.tgz", 1184 | "integrity": "sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==", 1185 | "dev": true, 1186 | "license": "MIT" 1187 | }, 1188 | "node_modules/tinyglobby": { 1189 | "version": "0.2.6", 1190 | "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.6.tgz", 1191 | "integrity": "sha512-NbBoFBpqfcgd1tCiO8Lkfdk+xrA7mlLR9zgvZcZWQQwU63XAfUePyd6wZBaU93Hqw347lHnwFzttAkemHzzz4g==", 1192 | "dev": true, 1193 | "license": "ISC", 1194 | "dependencies": { 1195 | "fdir": "^6.3.0", 1196 | "picomatch": "^4.0.2" 1197 | }, 1198 | "engines": { 1199 | "node": ">=12.0.0" 1200 | } 1201 | }, 1202 | "node_modules/tinypool": { 1203 | "version": "1.0.1", 1204 | "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.1.tgz", 1205 | "integrity": "sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==", 1206 | "dev": true, 1207 | "license": "MIT", 1208 | "engines": { 1209 | "node": "^18.0.0 || >=20.0.0" 1210 | } 1211 | }, 1212 | "node_modules/tinyrainbow": { 1213 | "version": "1.2.0", 1214 | "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", 1215 | "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", 1216 | "dev": true, 1217 | "license": "MIT", 1218 | "engines": { 1219 | "node": ">=14.0.0" 1220 | } 1221 | }, 1222 | "node_modules/tinyspy": { 1223 | "version": "3.0.2", 1224 | "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", 1225 | "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", 1226 | "dev": true, 1227 | "license": "MIT", 1228 | "engines": { 1229 | "node": ">=14.0.0" 1230 | } 1231 | }, 1232 | "node_modules/totalist": { 1233 | "version": "3.0.1", 1234 | "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", 1235 | "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", 1236 | "dev": true, 1237 | "license": "MIT", 1238 | "engines": { 1239 | "node": ">=6" 1240 | } 1241 | }, 1242 | "node_modules/uuid": { 1243 | "version": "10.0.0", 1244 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", 1245 | "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", 1246 | "funding": [ 1247 | "https://github.com/sponsors/broofa", 1248 | "https://github.com/sponsors/ctavan" 1249 | ], 1250 | "license": "MIT", 1251 | "bin": { 1252 | "uuid": "dist/bin/uuid" 1253 | } 1254 | }, 1255 | "node_modules/vite": { 1256 | "version": "5.4.5", 1257 | "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.5.tgz", 1258 | "integrity": "sha512-pXqR0qtb2bTwLkev4SE3r4abCNioP3GkjvIDLlzziPpXtHgiJIjuKl+1GN6ESOT3wMjG3JTeARopj2SwYaHTOA==", 1259 | "dev": true, 1260 | "license": "MIT", 1261 | "dependencies": { 1262 | "esbuild": "^0.21.3", 1263 | "postcss": "^8.4.43", 1264 | "rollup": "^4.20.0" 1265 | }, 1266 | "bin": { 1267 | "vite": "bin/vite.js" 1268 | }, 1269 | "engines": { 1270 | "node": "^18.0.0 || >=20.0.0" 1271 | }, 1272 | "funding": { 1273 | "url": "https://github.com/vitejs/vite?sponsor=1" 1274 | }, 1275 | "optionalDependencies": { 1276 | "fsevents": "~2.3.3" 1277 | }, 1278 | "peerDependencies": { 1279 | "@types/node": "^18.0.0 || >=20.0.0", 1280 | "less": "*", 1281 | "lightningcss": "^1.21.0", 1282 | "sass": "*", 1283 | "sass-embedded": "*", 1284 | "stylus": "*", 1285 | "sugarss": "*", 1286 | "terser": "^5.4.0" 1287 | }, 1288 | "peerDependenciesMeta": { 1289 | "@types/node": { 1290 | "optional": true 1291 | }, 1292 | "less": { 1293 | "optional": true 1294 | }, 1295 | "lightningcss": { 1296 | "optional": true 1297 | }, 1298 | "sass": { 1299 | "optional": true 1300 | }, 1301 | "sass-embedded": { 1302 | "optional": true 1303 | }, 1304 | "stylus": { 1305 | "optional": true 1306 | }, 1307 | "sugarss": { 1308 | "optional": true 1309 | }, 1310 | "terser": { 1311 | "optional": true 1312 | } 1313 | } 1314 | }, 1315 | "node_modules/vite-node": { 1316 | "version": "2.1.1", 1317 | "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.1.tgz", 1318 | "integrity": "sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA==", 1319 | "dev": true, 1320 | "license": "MIT", 1321 | "dependencies": { 1322 | "cac": "^6.7.14", 1323 | "debug": "^4.3.6", 1324 | "pathe": "^1.1.2", 1325 | "vite": "^5.0.0" 1326 | }, 1327 | "bin": { 1328 | "vite-node": "vite-node.mjs" 1329 | }, 1330 | "engines": { 1331 | "node": "^18.0.0 || >=20.0.0" 1332 | }, 1333 | "funding": { 1334 | "url": "https://opencollective.com/vitest" 1335 | } 1336 | }, 1337 | "node_modules/vitest": { 1338 | "version": "2.1.1", 1339 | "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.1.tgz", 1340 | "integrity": "sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==", 1341 | "dev": true, 1342 | "license": "MIT", 1343 | "dependencies": { 1344 | "@vitest/expect": "2.1.1", 1345 | "@vitest/mocker": "2.1.1", 1346 | "@vitest/pretty-format": "^2.1.1", 1347 | "@vitest/runner": "2.1.1", 1348 | "@vitest/snapshot": "2.1.1", 1349 | "@vitest/spy": "2.1.1", 1350 | "@vitest/utils": "2.1.1", 1351 | "chai": "^5.1.1", 1352 | "debug": "^4.3.6", 1353 | "magic-string": "^0.30.11", 1354 | "pathe": "^1.1.2", 1355 | "std-env": "^3.7.0", 1356 | "tinybench": "^2.9.0", 1357 | "tinyexec": "^0.3.0", 1358 | "tinypool": "^1.0.0", 1359 | "tinyrainbow": "^1.2.0", 1360 | "vite": "^5.0.0", 1361 | "vite-node": "2.1.1", 1362 | "why-is-node-running": "^2.3.0" 1363 | }, 1364 | "bin": { 1365 | "vitest": "vitest.mjs" 1366 | }, 1367 | "engines": { 1368 | "node": "^18.0.0 || >=20.0.0" 1369 | }, 1370 | "funding": { 1371 | "url": "https://opencollective.com/vitest" 1372 | }, 1373 | "peerDependencies": { 1374 | "@edge-runtime/vm": "*", 1375 | "@types/node": "^18.0.0 || >=20.0.0", 1376 | "@vitest/browser": "2.1.1", 1377 | "@vitest/ui": "2.1.1", 1378 | "happy-dom": "*", 1379 | "jsdom": "*" 1380 | }, 1381 | "peerDependenciesMeta": { 1382 | "@edge-runtime/vm": { 1383 | "optional": true 1384 | }, 1385 | "@types/node": { 1386 | "optional": true 1387 | }, 1388 | "@vitest/browser": { 1389 | "optional": true 1390 | }, 1391 | "@vitest/ui": { 1392 | "optional": true 1393 | }, 1394 | "happy-dom": { 1395 | "optional": true 1396 | }, 1397 | "jsdom": { 1398 | "optional": true 1399 | } 1400 | } 1401 | }, 1402 | "node_modules/why-is-node-running": { 1403 | "version": "2.3.0", 1404 | "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", 1405 | "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", 1406 | "dev": true, 1407 | "license": "MIT", 1408 | "dependencies": { 1409 | "siginfo": "^2.0.0", 1410 | "stackback": "0.0.2" 1411 | }, 1412 | "bin": { 1413 | "why-is-node-running": "cli.js" 1414 | }, 1415 | "engines": { 1416 | "node": ">=8" 1417 | } 1418 | } 1419 | } 1420 | } 1421 | -------------------------------------------------------------------------------- /examples/characters/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "characters", 3 | "version": "1.0.0", 4 | "description": "Let's talk about adding and subtracting numbers.", 5 | "main": "src/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "vitest --ui", 9 | "test": "vitest" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/stevekinney/testing-javascript.git" 14 | }, 15 | "author": "Steve Kinney ", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/stevekinney/testing-javascript/issues" 19 | }, 20 | "homepage": "https://github.com/stevekinney/testing-javascript#readme", 21 | "devDependencies": { 22 | "@vitest/ui": "^2.1.1", 23 | "vite": "^5.4.5", 24 | "vitest": "^2.1.1" 25 | }, 26 | "dependencies": { 27 | "uuid": "^10.0.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/characters/src/character.js: -------------------------------------------------------------------------------- 1 | import { Person } from './person.js'; 2 | import { rollDice } from './roll-dice.js'; 3 | 4 | export class Character extends Person { 5 | constructor(firstName, lastName, role) { 6 | super(firstName, lastName); 7 | 8 | this.role = role; 9 | this.level = 1; 10 | 11 | this.createdAt = new Date(); 12 | this.lastModified = this.createdAt; 13 | 14 | this.strength = rollDice(4, 6); 15 | this.dexterity = rollDice(4, 6); 16 | this.intelligence = rollDice(4, 6); 17 | this.wisdom = rollDice(4, 6); 18 | this.charisma = rollDice(4, 6); 19 | this.constitution = rollDice(4, 6); 20 | } 21 | 22 | levelUp() { 23 | this.level++; 24 | this.lastModified = new Date(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/characters/src/character.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { Character } from './character.js'; 3 | import { Person } from './person.js'; 4 | 5 | describe('Character', () => { 6 | it.todo( 7 | 'should create a character with a first name, last name, and role', 8 | () => {}, 9 | ); 10 | 11 | it.todo('should allow you to increase the level', () => {}); 12 | 13 | it.todo('should update the last modified date when leveling up', () => {}); 14 | }); 15 | -------------------------------------------------------------------------------- /examples/characters/src/person.js: -------------------------------------------------------------------------------- 1 | import { v4 as id } from 'uuid'; 2 | 3 | export class Person { 4 | constructor(firstName, lastName) { 5 | if (!firstName || !lastName) { 6 | throw new Error('First name and last name are required'); 7 | } 8 | 9 | this.id = 'person-' + id(); 10 | this.firstName = firstName; 11 | this.lastName = lastName; 12 | } 13 | 14 | get fullName() { 15 | return `${this.firstName} ${this.lastName}`; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/characters/src/person.test.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { Person } from './person.js'; 3 | 4 | // Remove the `todo` from the `describe` to run the tests. 5 | describe.todo('Person', () => { 6 | // This test will fail. Why? 7 | it('should create a person with a first name and last name', () => { 8 | const person = new Person('Grace', 'Hopper'); 9 | expect(person).toEqual({ 10 | firstName: 'Grace', 11 | lastName: 'Hopper', 12 | }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /examples/characters/src/roll-dice.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Rolls a specified number of dice with a specified number of sides, 3 | * drops the lowest roll, and returns the sum of the remaining rolls. 4 | * @param {number} diceCount - The number of dice to roll. 5 | * @param {number} diceSides - The number of sides on each die. 6 | */ 7 | export const rollDice = (diceCount = 4, diceSides = 6) => { 8 | const rolls = []; 9 | 10 | for (let i = 0; i < diceCount; i++) { 11 | rolls.push(Math.floor(Math.random() * diceSides) + 1); 12 | } 13 | 14 | rolls.sort((a, b) => a - b); // Sort rolls to drop the lowest one 15 | return rolls.slice(1).reduce((acc, curr) => acc + curr, 0); // Sum the top 3 rolls 16 | }; 17 | -------------------------------------------------------------------------------- /examples/characters/vitest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | test: { 3 | environment: 'node', 4 | globals: true, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /examples/directory/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "directory", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "type": "module", 6 | "scripts": { 7 | "start": "vite dev", 8 | "test": "vitest" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/stevekinney/testing-javascript.git" 13 | }, 14 | "author": "Steve Kinney ", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/stevekinney/testing-javascript/issues" 18 | }, 19 | "homepage": "https://github.com/stevekinney/testing-javascript#readme", 20 | "devDependencies": { 21 | "@testing-library/dom": "^10.4.0", 22 | "@testing-library/jest-dom": "^6.5.0", 23 | "@testing-library/react": "^16.0.1", 24 | "@types/body-parser": "^1.19.5", 25 | "@types/express": "^4.17.21", 26 | "@types/react": "^18.3.6", 27 | "@types/react-dom": "^18.3.0", 28 | "@types/testing-library__jest-dom": "^5.14.9", 29 | "@vitejs/plugin-react": "^4.3.1", 30 | "@vitest/ui": "^2.1.1", 31 | "msw": "^2.4.9", 32 | "vite": "^5.4.6", 33 | "vitest": "^2.1.1" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/directory/src/get-user.js: -------------------------------------------------------------------------------- 1 | export const getUser = async (id) => { 2 | const response = await fetch( 3 | `https://jsonplaceholder.typicode.com/users/${id}`, 4 | ); 5 | 6 | if (!response.ok) { 7 | throw new Error('Failed to fetch user'); 8 | } 9 | 10 | return response.json(); 11 | }; 12 | -------------------------------------------------------------------------------- /examples/directory/src/mocks/handlers.js: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from 'msw'; 2 | 3 | // Hint: https://jsonplaceholder.typicode.com/users/:id 4 | 5 | export const handlers = []; 6 | -------------------------------------------------------------------------------- /examples/directory/src/mocks/server.js: -------------------------------------------------------------------------------- 1 | import { setupServer } from 'msw/node'; 2 | import { handlers } from './handlers'; 3 | 4 | export const server = setupServer(...handlers); 5 | -------------------------------------------------------------------------------- /examples/directory/src/mocks/tasks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "test-1", 4 | "title": "Get a Phone Charger", 5 | "completed": true, 6 | "createdAt": "2024-09-19T08:30:00.711Z", 7 | "lastModified": "2024-09-19T08:30:00.711Z" 8 | }, 9 | { 10 | "id": "test-2", 11 | "title": "Charge Your Phone", 12 | "completed": false, 13 | "createdAt": "2024-09-19T08:31:00.261Z", 14 | "lastModified": "2024-09-19T08:31:00.261Z" 15 | } 16 | ] 17 | -------------------------------------------------------------------------------- /examples/directory/src/user.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { getUser } from './get-user'; 3 | 4 | export const User = ({ id }) => { 5 | const [user, setUser] = useState(null); 6 | 7 | useEffect(() => { 8 | getUser(id).then(setUser); 9 | }, [setUser]); 10 | 11 | if (!user) { 12 | return

Loading…

; 13 | } 14 | 15 | return ( 16 |
17 |

{user.name}

18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
Company{user.company.name}
Username{user.username}
Email{user.email}
Phone{user.phone}
Website{user.website}
42 |
43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /examples/directory/src/user.test.jsx: -------------------------------------------------------------------------------- 1 | import { screen, render } from '@testing-library/react'; 2 | import { User } from './user'; 3 | 4 | it.skip('should render a user', async () => { 5 | // This calls an API. That's not great. We should mock it. 6 | const id = 1; 7 | render(); 8 | 9 | expect(user).toBeInTheDocument(); 10 | }); 11 | -------------------------------------------------------------------------------- /examples/directory/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import { css } from 'css-configuration'; 4 | 5 | const PORT = process.env.PORT || 3000; 6 | 7 | export default defineConfig({ 8 | plugins: [react()], 9 | css, 10 | test: { 11 | globals: true, 12 | environment: 'happy-dom', 13 | setupFiles: ['@testing-library/jest-dom/vitest'], 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /examples/element-factory/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "button-factory", 3 | "version": "1.0.0", 4 | "main": "src/index.js", 5 | "type": "module", 6 | "scripts": { 7 | "start": "vitest --ui", 8 | "test": "vitest" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/stevekinney/testing-javascript.git" 13 | }, 14 | "author": "Steve Kinney ", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/stevekinney/testing-javascript/issues" 18 | }, 19 | "homepage": "https://github.com/stevekinney/testing-javascript#readme", 20 | "devDependencies": { 21 | "@vitest/ui": "^2.1.1", 22 | "vite": "^5.4.5", 23 | "vitest": "^2.1.1" 24 | }, 25 | "dependencies": { 26 | "@sveltejs/vite-plugin-svelte": "^3.1.2", 27 | "@testing-library/svelte": "^5.2.2", 28 | "lit": "^3.2.0", 29 | "svelte": "^4.2.19", 30 | "uuid": "^10.0.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/element-factory/src/alert-button.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | export const AlertButton = ({}) => { 4 | const [message, setMessage] = useState('Alert!'); 5 | 6 | return ( 7 |
8 | 16 | 17 | 18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /examples/element-factory/src/alert-button.test.jsx: -------------------------------------------------------------------------------- 1 | import { render, screen, act } from '@testing-library/react'; 2 | import userEvent from '@testing-library/user-event'; 3 | 4 | import { AlertButton } from './alert-button'; 5 | 6 | describe.todo('AlertButton', () => { 7 | beforeEach(() => {}); 8 | 9 | afterEach(() => {}); 10 | 11 | it('should render an alert button', async () => {}); 12 | 13 | it('should trigger an alert', async () => {}); 14 | }); 15 | -------------------------------------------------------------------------------- /examples/element-factory/src/button.js: -------------------------------------------------------------------------------- 1 | export function createButton() { 2 | const button = document.createElement('button'); 3 | button.textContent = 'Click Me'; 4 | 5 | button.addEventListener('click', () => { 6 | button.textContent = 'Clicked!'; 7 | }); 8 | 9 | return button; 10 | } 11 | -------------------------------------------------------------------------------- /examples/element-factory/src/button.test.js: -------------------------------------------------------------------------------- 1 | import { createButton } from './button.js'; 2 | 3 | describe.todo('createButton', () => { 4 | it('should create a button element', () => {}); 5 | 6 | it('should have the text "Click Me"', () => {}); 7 | 8 | it('should change the text to "Clicked!" when clicked', async () => {}); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/element-factory/src/local-storage.test.js: -------------------------------------------------------------------------------- 1 | it('should properly assign to localStorage', () => { 2 | const key = 'secret'; 3 | const message = "It's a secret to everybody."; 4 | 5 | localStorage.setItem(key, message); 6 | expect(localStorage.getItem(key)).toBe(message); 7 | }); 8 | 9 | it('should properly clear localStorage', () => { 10 | const key = 'secret'; 11 | const message = "It's a secret to everybody."; 12 | 13 | localStorage.setItem(key, message); 14 | localStorage.clear(); 15 | 16 | expect(localStorage.getItem(key)).toBeNull(); 17 | }); 18 | -------------------------------------------------------------------------------- /examples/element-factory/src/login-form.js: -------------------------------------------------------------------------------- 1 | import { html, render } from 'lit'; 2 | 3 | /** 4 | * @typedef {Object} LoginFormProperties 5 | * @property {string} action; 6 | * @property {'get' | 'post' | 'dialog'} method; 7 | * @property {(event: Event) => void} onSubmit; 8 | */ 9 | 10 | /** 11 | * Renders a login form. 12 | * @param {LoginFormProperties} parameters 13 | * @returns {HTMLDivElement} 14 | */ 15 | export const createLoginForm = ({ 16 | action = '/login', 17 | method = 'post', 18 | onSubmit = () => {}, 19 | } = {}) => { 20 | const template = html` 21 |
27 | 31 | 35 | 36 |
37 | `; 38 | 39 | const container = document.createElement('div'); 40 | render(template, container); 41 | 42 | return container; 43 | }; 44 | -------------------------------------------------------------------------------- /examples/element-factory/src/login-form.test.js: -------------------------------------------------------------------------------- 1 | import { screen } from '@testing-library/dom'; 2 | import userEvent from '@testing-library/user-event'; 3 | import { createLoginForm } from './login-form'; 4 | 5 | describe.todo('Login Form', async () => { 6 | it('should render a login form', async () => {}); 7 | 8 | it('should render a login form with a custom action', async () => { 9 | // Can you make sure that the form we render has an `action` attribute set to '/custom'? 10 | }); 11 | 12 | it('should render a login form with a custom method', async () => { 13 | // Can you make sure that the form we render has a `method` attribute set to 'get'? 14 | }); 15 | 16 | it('should render a login form with a custom submit handler', async () => { 17 | // We'll do this one later. Don't worry about it for now. 18 | // If it *is* later, then you should worry about it. 19 | // Can you make sure that the form we render has a submit handler that calls a custom function? 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /examples/element-factory/src/secret-input.js: -------------------------------------------------------------------------------- 1 | function getStoredSecret() { 2 | try { 3 | return localStorage.getItem('secret') || ''; 4 | } catch { 5 | return ''; 6 | } 7 | } 8 | 9 | export function createSecretInput() { 10 | const id = 'secret-input'; 11 | 12 | const container = document.createElement('div'); 13 | const input = document.createElement('input'); 14 | const label = document.createElement('label'); 15 | const submitButton = document.createElement('button'); 16 | const clearButton = document.createElement('button'); 17 | 18 | input.id = id; 19 | input.type = 'password'; 20 | input.name = 'secret'; 21 | input.placeholder = 'Enter your secret…'; 22 | input.value = getStoredSecret(); 23 | 24 | label.htmlFor = id; 25 | label.textContent = 'Secret'; 26 | 27 | submitButton.id = `${id}-button`; 28 | submitButton.textContent = 'Store Secret'; 29 | submitButton.addEventListener('click', () => { 30 | localStorage.setItem('secret', input.value); 31 | input.value = ''; 32 | }); 33 | 34 | clearButton.id = `${id}-clear-button`; 35 | clearButton.textContent = 'Clear Secret'; 36 | clearButton.addEventListener('click', () => { 37 | localStorage.removeItem('secret'); 38 | input.value = ''; 39 | }); 40 | 41 | container.appendChild(label); 42 | container.appendChild(input); 43 | container.appendChild(submitButton); 44 | container.appendChild(clearButton); 45 | 46 | return container; 47 | } 48 | -------------------------------------------------------------------------------- /examples/element-factory/src/secret-input.test.js: -------------------------------------------------------------------------------- 1 | import { screen } from '@testing-library/dom'; 2 | import userEvent from '@testing-library/user-event'; 3 | import '@testing-library/jest-dom/vitest'; 4 | 5 | import { createSecretInput } from './secret-input.js'; 6 | 7 | describe.todo('createSecretInput', async () => { 8 | beforeEach(() => {}); 9 | 10 | afterEach(() => {}); 11 | 12 | it('should have loaded the secret from localStorage', async () => {}); 13 | 14 | it('should save the secret to localStorage', async () => {}); 15 | 16 | it('should clear the secret from localStorage', async () => {}); 17 | }); 18 | -------------------------------------------------------------------------------- /examples/element-factory/src/tabs.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 |
9 | {#each tabs as tab, i} 10 | {@const id = tab.id || i} 11 | {@const selected = i === activeIndex} 12 | 22 | {/each} 23 |
24 | 25 | {#each tabs as tab, i} 26 | 29 | {/each} 30 |
31 | -------------------------------------------------------------------------------- /examples/element-factory/src/tabs.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/svelte'; 2 | import userEvent from '@testing-library/user-event'; 3 | 4 | import Tabs from './tabs.svelte'; 5 | 6 | describe.todo('Tabs', () => { 7 | beforeEach(() => { 8 | render(Tabs, { 9 | tabs: [ 10 | { label: 'Venue', content: 'We will be at this place!' }, 11 | { label: 'Lineup', content: 'Check out our exciting lineup!' }, 12 | { label: 'Tickets', content: 'Buy tickets today!' }, 13 | ], 14 | }); 15 | }); 16 | 17 | it('should render three tabs', async () => {}); 18 | 19 | it('should switch tabs', async () => {}); 20 | 21 | it('should render the content of the selected tab', async () => {}); 22 | 23 | it('should render the content of the first tab by default', async () => {}); 24 | }); 25 | -------------------------------------------------------------------------------- /examples/element-factory/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import { svelte } from '@sveltejs/vite-plugin-svelte'; 3 | import react from '@vitejs/plugin-react'; 4 | 5 | export default defineConfig({ 6 | plugins: [svelte(), react()], 7 | test: { 8 | globals: true, 9 | environment: 'happy-dom', 10 | setupFiles: ['@testing-library/jest-dom/vitest'], 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /examples/guess-the-number/game.js: -------------------------------------------------------------------------------- 1 | export class Game { 2 | /** 3 | * A number guessing game. 4 | * @param {number} minimum - The minimum number to guess. 5 | * @param {number} maximum - The maximum number to guess. 6 | */ 7 | constructor(minimum = 1, maximum = 100) { 8 | const seed = Math.random(); 9 | 10 | this.secretNumber = Math.ceil(seed * maximum - minimum) + minimum; 11 | this.guesses = new Set(); 12 | 13 | console.log(`Guess the number between ${minimum} and ${maximum}.`); 14 | } 15 | 16 | /** 17 | * Make a guess for the secret number. 18 | * @param {number} number - The number to guess. 19 | */ 20 | guess(number) { 21 | if (this.guesses.has(number)) { 22 | return 'You already guessed that number!'; 23 | } 24 | 25 | this.guesses.add(number); 26 | 27 | if (number < this.secretNumber) { 28 | return 'Too low!'; 29 | } else if (number > this.secretNumber) { 30 | return 'Too high!'; 31 | } else if (number === this.secretNumber) { 32 | return `Correct! You guessed the number in ${this.guesses.size} attempts.`; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/guess-the-number/game.test.js: -------------------------------------------------------------------------------- 1 | import { it, expect, describe, vi } from 'vitest'; 2 | import { Game } from './game'; 3 | 4 | describe('Game', () => { 5 | it('should return an instance of a game', () => { 6 | // This is mostly a dummy test. 7 | const game = new Game(); 8 | expect(game).toBeInstanceOf(Game); 9 | }); 10 | 11 | it('should have a secret number', () => { 12 | // Thisn't really a useful test. 13 | // Do I *really* care about the type of the secret number? 14 | // Do I *really* care about the name of a "private" property? 15 | const game = new Game(); 16 | expect(game.secretNumber).toBeTypeOf('number'); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /examples/guess-the-number/index.js: -------------------------------------------------------------------------------- 1 | import { Game } from './game.js'; 2 | import chalk from 'chalk'; 3 | import { createInterface } from 'readline'; 4 | 5 | const rl = createInterface({ 6 | input: process.stdin, 7 | output: process.stdout, 8 | }); 9 | 10 | let game = new Game(1, 100); 11 | 12 | function startGame() { 13 | rl.question(`Guess ${chalk.cyanBright('→')} `, (answer) => { 14 | const guess = parseInt(answer, 10); 15 | 16 | // Check if the user's guess is correct 17 | if (isNaN(guess)) { 18 | console.log(chalk.red('Please enter a valid number.')); 19 | startGame(); 20 | } 21 | 22 | const result = game.guess(guess); 23 | 24 | console.log(result); 25 | 26 | if (result.startsWith('Correct')) { 27 | rl.close(); 28 | } else { 29 | startGame(); 30 | } 31 | }); 32 | } 33 | 34 | startGame(); 35 | -------------------------------------------------------------------------------- /examples/guess-the-number/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "guess-the-number", 3 | "version": "1.0.0", 4 | "description": "Guess the number and test that you're logic actually works.", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "vitest" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/stevekinney/testing-javascript.git" 13 | }, 14 | "author": "Steve Kinney ", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/stevekinney/testing-javascript/issues" 18 | }, 19 | "homepage": "https://github.com/stevekinney/testing-javascript#readme", 20 | "devDependencies": { 21 | "@vitest/ui": "^2.1.1", 22 | "vite": "^5.4.5", 23 | "vitest": "^2.1.1" 24 | }, 25 | "dependencies": { 26 | "chalk": "^5.3.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/guess-the-number/vitest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | test: { 3 | environment: 'node', 4 | globals: true, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /examples/logjam/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "logjam", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "start": "vitest --ui", 7 | "test": "vitest" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/stevekinney/testing-javascript.git" 12 | }, 13 | "author": "Steve Kinney ", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/stevekinney/testing-javascript/issues" 17 | }, 18 | "homepage": "https://github.com/stevekinney/testing-javascript#readme", 19 | "devDependencies": { 20 | "@vitest/ui": "^2.1.1", 21 | "vite": "^5.4.5", 22 | "vitest": "^2.1.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/logjam/src/log.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { sendToServer } from './send-to-server'; 4 | 5 | /** 6 | * Log a message to the console in development mode or send it to the server in production mode. 7 | * @param {string} message 8 | */ 9 | export function log(message) { 10 | if (import.meta.env.MODE !== 'production') { 11 | console.log(message); 12 | } else { 13 | sendToServer('info', message); 14 | } 15 | } 16 | 17 | /** 18 | * Log a message to the console in development mode or send it to the server in production mode. 19 | * @param {string} message 20 | */ 21 | log.warn = function warn(message) { 22 | if (import.meta.env.MODE !== 'production') { 23 | console.warn(message); 24 | } else { 25 | sendToServer('warn', message); 26 | } 27 | }; 28 | 29 | /** 30 | * Log a message to the console in development mode or send it to the server in production mode. 31 | * @param {string} message 32 | */ 33 | log.error = function error(message) { 34 | if (import.meta.env.MODE !== 'production') { 35 | console.log(message); 36 | } else { 37 | sendToServer('error', message); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /examples/logjam/src/log.test.js: -------------------------------------------------------------------------------- 1 | import { expect, it, vi, beforeEach, afterEach, describe } from 'vitest'; 2 | import { log } from './log'; 3 | 4 | describe.todo('logger', () => {}); 5 | -------------------------------------------------------------------------------- /examples/logjam/src/make-request.js: -------------------------------------------------------------------------------- 1 | export const makeRequest = async (url) => { 2 | if (!import.meta.env.API_KEY) { 3 | throw new Error('API_KEY is required'); 4 | } 5 | return { data: 'Some data would go here.' }; 6 | }; 7 | -------------------------------------------------------------------------------- /examples/logjam/src/make-request.test.js: -------------------------------------------------------------------------------- 1 | import { makeRequest } from './make-request'; 2 | 3 | describe.todo('makeRequest', () => {}); 4 | -------------------------------------------------------------------------------- /examples/logjam/src/send-to-server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @param {'info' | 'error' | 'warn'} level 4 | * @param {string} message 5 | */ 6 | export const sendToServer = (level, message) => { 7 | return `You must mock this function: sendToServer(${level}, ${message})`; 8 | }; 9 | -------------------------------------------------------------------------------- /examples/logjam/vitest.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | environment: 'node', 6 | globals: true, 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /examples/scratchpad/fake-time.test.js: -------------------------------------------------------------------------------- 1 | import { vi, describe, it, expect } from 'vitest'; 2 | 3 | vi.useFakeTimers(); 4 | 5 | function delay(callback) { 6 | setTimeout(() => { 7 | callback('Delayed'); 8 | }, 1000); 9 | } 10 | 11 | describe('delay function', () => { 12 | it.todo('should call callback after delay', () => {}); 13 | }); 14 | -------------------------------------------------------------------------------- /examples/scratchpad/index.test.js: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest'; 2 | 3 | test('a super simple test', () => { 4 | expect(true).toBe(true); 5 | }); 6 | -------------------------------------------------------------------------------- /examples/scratchpad/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scratchpad", 3 | "version": "1.0.0", 4 | "main": "src/index.js", 5 | "type": "module", 6 | "scripts": { 7 | "start": "vitest --ui", 8 | "test": "vitest" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/stevekinney/testing-javascript.git" 13 | }, 14 | "author": "Steve Kinney ", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/stevekinney/testing-javascript/issues" 18 | }, 19 | "homepage": "https://github.com/stevekinney/testing-javascript#readme", 20 | "devDependencies": { 21 | "@vitest/ui": "^2.1.1", 22 | "vite": "^5.4.5", 23 | "vitest": "^2.1.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/strictly-speaking/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "strictly-speaking", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "start": "vitest --ui", 7 | "test": "vitest" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/stevekinney/testing-javascript.git" 12 | }, 13 | "author": "Steve Kinney ", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/stevekinney/testing-javascript/issues" 17 | }, 18 | "homepage": "https://github.com/stevekinney/testing-javascript#readme", 19 | "devDependencies": { 20 | "@vitest/ui": "^2.1.1", 21 | "vite": "^5.4.5", 22 | "vitest": "^2.1.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/strictly-speaking/strictly-speaking.test.js: -------------------------------------------------------------------------------- 1 | import { test, expect } from 'vitest'; 2 | 3 | class Person { 4 | constructor(name) { 5 | this.name = name; 6 | } 7 | } 8 | 9 | test('objects with the same properties are equal', () => { 10 | expect({ a: 1, b: 2 }).toEqual({ a: 1, b: 2 }); 11 | }); 12 | 13 | test('objects with different properties are not equal', () => { 14 | expect({ a: 1, b: 2 }).not.toEqual({ a: 1, b: 3 }); 15 | }); 16 | 17 | test('objects with undefined properties are equal to objects without those properties', () => { 18 | expect({ a: 1 }).toEqual({ a: 1, b: undefined }); 19 | }); 20 | 21 | test('objects with undefined properties are *not* strictly equal to objects without those properties', () => { 22 | expect({ a: 1 }).not.toStrictEqual({ a: 1, b: undefined }); 23 | }); 24 | 25 | test('instances are equal to object literals with the same properties', () => { 26 | expect(new Person('Alice')).toEqual({ name: 'Alice' }); 27 | }); 28 | 29 | test('instances are not strictly equal to object literals with the same properties', () => { 30 | expect(new Person('Alice')).not.toStrictEqual({ name: 'Alice' }); 31 | }); 32 | -------------------------------------------------------------------------------- /examples/task-list/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Task List 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/task-list/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "task-list", 3 | "version": "1.0.0", 4 | "description": "Test that you won't forget to write tests.", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "concurrently \"npm run start:client\" \"npm run start:server\" --names \"client,server\" --kill-others -c blue,green", 9 | "start:client": "vite dev", 10 | "start:server": "node server/index.js", 11 | "test": "vitest" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/stevekinney/testing-javascript.git" 16 | }, 17 | "author": "Steve Kinney ", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/stevekinney/testing-javascript/issues" 21 | }, 22 | "homepage": "https://github.com/stevekinney/testing-javascript#readme", 23 | "devDependencies": { 24 | "@playwright/test": "^1.47.2", 25 | "@testing-library/dom": "^10.4.0", 26 | "@testing-library/jest-dom": "^6.5.0", 27 | "@testing-library/react": "^16.0.1", 28 | "@types/body-parser": "^1.19.5", 29 | "@types/express": "^4.17.21", 30 | "@types/react": "^18.3.6", 31 | "@types/react-dom": "^18.3.0", 32 | "@types/testing-library__jest-dom": "^5.14.9", 33 | "@types/uuid": "^10.0.0", 34 | "@vitejs/plugin-react": "^4.3.1", 35 | "@vitest/ui": "^2.1.1", 36 | "chalk": "^5.3.0", 37 | "concurrently": "^9.0.1", 38 | "cors": "^2.8.5", 39 | "msw": "^2.4.9", 40 | "tailwind-merge": "^2.5.2", 41 | "uuid": "^10.0.0", 42 | "vite": "^5.4.6", 43 | "vitest": "^2.1.1", 44 | "wait-port": "^1.1.0" 45 | }, 46 | "dependencies": { 47 | "body-parser": "^1.20.3", 48 | "express": "^4.21.0", 49 | "lucide-react": "^0.441.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/task-list/playwright.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { defineConfig, devices } from '@playwright/test'; 3 | 4 | /** 5 | * Read environment variables from file. 6 | * https://github.com/motdotla/dotenv 7 | */ 8 | // require('dotenv').config({ path: path.resolve(__dirname, '.env') }); 9 | 10 | /** 11 | * @see https://playwright.dev/docs/test-configuration 12 | */ 13 | export default defineConfig({ 14 | testDir: './tests', 15 | /* Run tests in files in parallel */ 16 | fullyParallel: true, 17 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 18 | forbidOnly: !!process.env.CI, 19 | /* Retry on CI only */ 20 | retries: process.env.CI ? 2 : 0, 21 | /* Opt out of parallel tests on CI. */ 22 | workers: process.env.CI ? 1 : undefined, 23 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 24 | reporter: 'html', 25 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 26 | use: { 27 | /* Base URL to use in actions like `await page.goto('/')`. */ 28 | // baseURL: 'http://127.0.0.1:3000', 29 | 30 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 31 | trace: 'on-first-retry', 32 | }, 33 | 34 | /* Configure projects for major browsers */ 35 | projects: [ 36 | { 37 | name: 'chromium', 38 | use: { ...devices['Desktop Chrome'] }, 39 | }, 40 | 41 | { 42 | name: 'firefox', 43 | use: { ...devices['Desktop Firefox'] }, 44 | }, 45 | 46 | { 47 | name: 'webkit', 48 | use: { ...devices['Desktop Safari'] }, 49 | }, 50 | 51 | /* Test against mobile viewports. */ 52 | // { 53 | // name: 'Mobile Chrome', 54 | // use: { ...devices['Pixel 5'] }, 55 | // }, 56 | // { 57 | // name: 'Mobile Safari', 58 | // use: { ...devices['iPhone 12'] }, 59 | // }, 60 | 61 | /* Test against branded browsers. */ 62 | // { 63 | // name: 'Microsoft Edge', 64 | // use: { ...devices['Desktop Edge'], channel: 'msedge' }, 65 | // }, 66 | // { 67 | // name: 'Google Chrome', 68 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, 69 | // }, 70 | ], 71 | 72 | /* Run your local dev server before starting the tests */ 73 | // webServer: { 74 | // command: 'npm run start', 75 | // url: 'http://127.0.0.1:3000', 76 | // reuseExistingServer: !process.env.CI, 77 | // }, 78 | }); 79 | -------------------------------------------------------------------------------- /examples/task-list/server/index.js: -------------------------------------------------------------------------------- 1 | import * as url from 'node:url'; 2 | import express from 'express'; 3 | import bodyParser from 'body-parser'; 4 | import chalk from 'chalk'; 5 | 6 | import { 7 | getTasks, 8 | getTask, 9 | createTask, 10 | updateTask, 11 | deleteTask, 12 | } from './tasks.js'; 13 | 14 | const app = express(); 15 | app.use(bodyParser.json()); 16 | 17 | app.get('/api/tasks', (req, res) => { 18 | const tasks = getTasks(); 19 | res.json(tasks); 20 | }); 21 | 22 | app.post('/api/tasks', (req, res) => { 23 | const { title } = req.body; 24 | if (!title) { 25 | return res.status(400).json({ message: 'A title is required' }); 26 | } 27 | const task = createTask(title); 28 | res.status(201).json(task); 29 | }); 30 | 31 | app.get('/api/tasks/:id', (req, res) => { 32 | const task = getTask(req.params.id); 33 | 34 | if (!task) { 35 | return res.status(404).json({ message: 'Task not found' }); 36 | } 37 | 38 | res.json(task); 39 | }); 40 | 41 | app.patch('/api/tasks/:id', (req, res) => { 42 | const { title, completed } = req.body; 43 | 44 | const task = updateTask(req.params.id, { title, completed }); 45 | 46 | if (!task) { 47 | return res.status(404).json({ message: 'Task not found' }); 48 | } 49 | 50 | res.sendStatus(204); 51 | }); 52 | 53 | app.delete('/api/tasks/:id', (req, res) => { 54 | const task = deleteTask(req.params.id); 55 | 56 | if (!task) { 57 | return res.status(404).json({ message: 'Task not found' }); 58 | } 59 | 60 | res.sendStatus(204); // No content to send back 61 | }); 62 | 63 | if (import.meta.url.startsWith('file:')) { 64 | const modulePath = url.fileURLToPath(import.meta.url); 65 | if (process.argv[1] === modulePath) { 66 | const PORT = process.env.PORT || 3000; 67 | app.listen(PORT, () => { 68 | console.log( 69 | chalk.magenta(`Server is running on port ${chalk.green(PORT)}…`), 70 | ); 71 | }); 72 | } 73 | } 74 | 75 | export default app; 76 | -------------------------------------------------------------------------------- /examples/task-list/server/tasks.js: -------------------------------------------------------------------------------- 1 | import { v4 as id } from 'uuid'; 2 | 3 | /** @typedef {import('../src/types').Task} Task */ 4 | 5 | /** @type {Task[]} tasks - An array to store tasks. */ 6 | let tasks = []; 7 | 8 | /** 9 | * Get all tasks. 10 | * @returns {Task[]} An array of tasks. 11 | */ 12 | export const getTasks = () => tasks; 13 | 14 | /** 15 | * Create a new task. 16 | * @param {string} title - The title of the task. 17 | * @returns {Task} The newly created task. 18 | */ 19 | export const createTask = (title) => { 20 | /** @type {Task} */ 21 | const task = { 22 | id: id(), 23 | title, 24 | completed: false, 25 | createdAt: new Date(), 26 | lastModified: new Date(), 27 | }; 28 | tasks.push(task); 29 | return task; 30 | }; 31 | 32 | /** 33 | * Find a task by ID. 34 | * @param {string} id - The ID of the task to find. 35 | * @returns {Task | undefined} The found task or undefined if not found. 36 | */ 37 | export const getTask = (id) => tasks.find((task) => task.id === id); 38 | 39 | /** 40 | * Update a task by ID. 41 | * @param {string} id - The ID of the task to update. 42 | * @param {Partial>} updates - The updates to apply to the task. 43 | * @returns {Task | undefined} The updated task or undefined if not found. 44 | */ 45 | export const updateTask = (id, updates) => { 46 | const task = getTask(id); 47 | 48 | if (!task) return undefined; 49 | 50 | for (const key in updates) { 51 | if (updates[key] !== undefined) { 52 | task[key] = updates[key]; 53 | } 54 | } 55 | 56 | Object.assign(task, { lastModified: new Date() }); 57 | return task; 58 | }; 59 | 60 | /** 61 | * Delete a task by ID. 62 | * @param {string} id - The ID of the task to delete. 63 | * @returns {boolean} `true` if the task was deleted, `false` if not found. 64 | */ 65 | export const deleteTask = (id) => { 66 | const index = tasks.findIndex((task) => task.id === id); 67 | if (index === -1) return false; 68 | 69 | tasks.splice(index, 1); 70 | return true; 71 | }; 72 | -------------------------------------------------------------------------------- /examples/task-list/src/actions.test.js: -------------------------------------------------------------------------------- 1 | import { setTasks } from './actions'; 2 | 3 | describe('setTasks', () => { 4 | it('creates a set-tasks action', () => { 5 | const tasks = [{ id: '1', text: 'Task 1', completed: false }]; 6 | expect(setTasks(tasks)).toEqual({ 7 | type: 'set-tasks', 8 | payload: tasks, 9 | }); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /examples/task-list/src/actions.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { getErrorMessage } from 'get-error-message'; 3 | 4 | import { 5 | Task, 6 | SetTasksAction, 7 | AddTaskAction, 8 | UpdateTaskAction, 9 | RemoveTaskAction, 10 | SetLoadingAction, 11 | SetErrorAction, 12 | TaskAction, 13 | TaskData, 14 | } from './types'; 15 | 16 | export const setTasks = (tasks: Task[]): SetTasksAction => ({ 17 | type: 'set-tasks', 18 | payload: tasks, 19 | }); 20 | 21 | export const addTask = (task: Task): AddTaskAction => ({ 22 | type: 'add-task', 23 | payload: task, 24 | }); 25 | 26 | export const updateTask = (id: string, task: TaskData): UpdateTaskAction => ({ 27 | type: 'update-task', 28 | payload: { ...task, id }, 29 | }); 30 | 31 | export const removeTask = (id: string): RemoveTaskAction => ({ 32 | type: 'remove-task', 33 | payload: id, 34 | }); 35 | 36 | export const setLoading = (): SetLoadingAction => ({ 37 | type: 'set-loading', 38 | }); 39 | 40 | export const setError = ( 41 | error: unknown, 42 | fallback: string = 'Unknown error', 43 | ): SetErrorAction => { 44 | const message = getErrorMessage(error, fallback); 45 | return { type: 'set-error', payload: message }; 46 | }; 47 | 48 | export const bindActionCreators = (dispatch: React.Dispatch) => { 49 | return useMemo( 50 | () => ({ 51 | setTasks: (tasks: Task[]) => dispatch(setTasks(tasks)), 52 | addTask: (task: Task) => dispatch(addTask(task)), 53 | updateTask: (id: string, task: TaskData) => 54 | dispatch(updateTask(id, task)), 55 | removeTask: (id: string) => dispatch(removeTask(id)), 56 | setLoading: () => dispatch(setLoading()), 57 | setError: (error: unknown, fallback: string) => 58 | dispatch(setError(error, fallback)), 59 | }), 60 | [dispatch], 61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /examples/task-list/src/api.ts: -------------------------------------------------------------------------------- 1 | import { Task } from './types'; 2 | 3 | export const all = async (): Promise => { 4 | const response = await fetch('/api/tasks'); 5 | 6 | if (!response.ok) { 7 | throw new Error('Failed to fetch tasks'); 8 | } 9 | 10 | return response.json(); 11 | }; 12 | 13 | export const add = async (title: string): Promise => { 14 | const response = await fetch('/api/tasks', { 15 | method: 'POST', 16 | headers: { 'Content-Type': 'application/json' }, 17 | body: JSON.stringify({ title }), 18 | }); 19 | 20 | if (!response.ok) { 21 | throw new Error('Failed to add task'); 22 | } 23 | 24 | return response.json(); 25 | }; 26 | 27 | export const update = async ( 28 | id: string, 29 | updatedTask: Partial, 30 | ): Promise => { 31 | const response = await fetch(`/api/tasks/${id}`, { 32 | method: 'PATCH', 33 | headers: { 'Content-Type': 'application/json' }, 34 | body: JSON.stringify(updatedTask), 35 | }); 36 | 37 | if (!response.ok) { 38 | throw new Error('Failed to update task'); 39 | } 40 | }; 41 | 42 | export const remove = async (id: string): Promise => { 43 | const response = await fetch(`/api/tasks/${id}`, { 44 | method: 'DELETE', 45 | }); 46 | 47 | if (!response.ok) { 48 | throw new Error('Failed to delete task'); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /examples/task-list/src/components/application.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useReducer } from 'react'; 2 | 3 | import * as api from '../api'; 4 | import { initialState, taskReducer } from '../reducer'; 5 | 6 | import { CreateTask } from './create-task'; 7 | import { Tasks } from './tasks'; 8 | 9 | export const Application = () => { 10 | const [state, dispatch] = useReducer(taskReducer, initialState); 11 | const { tasks } = state; 12 | 13 | useEffect(() => { 14 | api.all().then((payload) => { 15 | dispatch({ type: 'set-tasks', payload }); 16 | }); 17 | }, [dispatch]); 18 | 19 | const addTask = (title) => { 20 | api.add(title).then((payload) => { 21 | dispatch({ type: 'add-task', payload }); 22 | }); 23 | }; 24 | 25 | const updateTask = (id, updatedTask) => { 26 | api.update(id, updatedTask).then(() => { 27 | dispatch({ type: 'update-task', payload: { id, ...updatedTask } }); 28 | }); 29 | }; 30 | 31 | const removeTask = (id) => { 32 | api.remove(id).then(() => { 33 | dispatch({ type: 'remove-task', payload: id }); 34 | }); 35 | }; 36 | 37 | return ( 38 |
39 | 40 | 41 |
42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /examples/task-list/src/components/create-task.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | export const CreateTask = ({ onSubmit }) => { 4 | const [title, setTitle] = useState(''); 5 | 6 | return ( 7 |
{ 11 | event.preventDefault(); 12 | onSubmit(title); 13 | setTitle(''); 14 | }} 15 | > 16 |
17 | 20 |
21 | setTitle(e.target.value)} 27 | placeholder="What do you need to get done?" 28 | required 29 | /> 30 | 33 |
34 |
35 |
36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /examples/task-list/src/components/date-time.jsx: -------------------------------------------------------------------------------- 1 | import { twMerge as merge } from 'tailwind-merge'; 2 | 3 | const formatDate = new Intl.DateTimeFormat(navigator.language, { 4 | dateStyle: 'short', 5 | timeStyle: 'short', 6 | }); 7 | 8 | export const DateTime = ({ 9 | date, 10 | title, 11 | className, 12 | }) => { 13 | if (typeof date === 'string') date = new Date(date); 14 | 15 | return ( 16 |
17 | 18 | {title} 19 | 20 | {formatDate.format(date)} 21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /examples/task-list/src/components/task.jsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | import { DateTime } from './date-time'; 3 | 4 | export const Task = memo(({ task, updateTask, removeTask }) => { 5 | return ( 6 |
  • 10 |
    11 | 14 | updateTask(task.id, { completed: !task.completed })} 20 | /> 21 |

    {task.title}

    22 | 29 |
    30 |
    31 | 32 | 33 |
    34 |
  • 35 | ); 36 | }); 37 | -------------------------------------------------------------------------------- /examples/task-list/src/components/tasks.jsx: -------------------------------------------------------------------------------- 1 | import { Task } from './task'; 2 | 3 | export const Tasks = ({ tasks, updateTask, removeTask }) => { 4 | return ( 5 |
      6 | {tasks.map((task) => ( 7 | 13 | ))} 14 |
    15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /examples/task-list/src/index.jsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import { Application } from './components/application'; 4 | 5 | createRoot(document.getElementById('root')).render( 6 | 7 | 8 | , 9 | ); 10 | -------------------------------------------------------------------------------- /examples/task-list/src/mocks/handlers.js: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from 'msw'; 2 | import tasks from './tasks.json'; 3 | 4 | let id = 3; 5 | 6 | const createTask = (title) => ({ 7 | id: `${id++}`, 8 | title, 9 | completed: false, 10 | createdAt: new Date('02-29-2024').toISOString(), 11 | lastModified: new Date('02-29-2024').toISOString(), 12 | }); 13 | 14 | export const handlers = []; 15 | -------------------------------------------------------------------------------- /examples/task-list/src/mocks/server.js: -------------------------------------------------------------------------------- 1 | import { setupServer } from 'msw/node'; 2 | import { handlers } from './handlers'; 3 | 4 | export const server = setupServer(...handlers); 5 | -------------------------------------------------------------------------------- /examples/task-list/src/mocks/tasks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "test-1", 4 | "title": "Get a Phone Charger", 5 | "completed": true, 6 | "createdAt": "2024-09-19T08:30:00.711Z", 7 | "lastModified": "2024-09-19T08:30:00.711Z" 8 | }, 9 | { 10 | "id": "test-2", 11 | "title": "Charge Your Phone", 12 | "completed": false, 13 | "createdAt": "2024-09-19T08:31:00.261Z", 14 | "lastModified": "2024-09-19T08:31:00.261Z" 15 | } 16 | ] 17 | -------------------------------------------------------------------------------- /examples/task-list/src/reducer.ts: -------------------------------------------------------------------------------- 1 | import type { TaskAction, TaskState } from './types'; 2 | 3 | export const initialState: TaskState = { 4 | tasks: [], 5 | loading: false, 6 | error: null, 7 | }; 8 | 9 | export const taskReducer = ( 10 | state: TaskState, 11 | action: TaskAction, 12 | ): TaskState => { 13 | switch (action.type) { 14 | case 'set-tasks': 15 | return { ...state, tasks: action.payload, loading: false }; 16 | case 'add-task': 17 | return { 18 | ...state, 19 | tasks: [...state.tasks, action.payload], 20 | loading: false, 21 | }; 22 | case 'update-task': 23 | const { id, ...payload } = action.payload; 24 | 25 | return { 26 | ...state, 27 | tasks: state.tasks.map((task) => 28 | task.id === id ? { ...task, ...payload } : task, 29 | ), 30 | loading: false, 31 | }; 32 | case 'remove-task': 33 | return { 34 | ...state, 35 | tasks: state.tasks.filter((task) => task.id !== action.payload), 36 | loading: false, 37 | }; 38 | case 'set-loading': 39 | return { ...state, loading: action.payload }; 40 | case 'set-error': 41 | return { ...state, error: action.payload, loading: false }; 42 | default: 43 | return state; 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /examples/task-list/src/types.ts: -------------------------------------------------------------------------------- 1 | export type Task = { 2 | id: string; 3 | title: string; 4 | completed: boolean; 5 | createdAt: Date; 6 | lastModified: Date; 7 | }; 8 | 9 | export type TaskData = Partial>; 10 | 11 | export type TaskState = { 12 | tasks: Task[]; 13 | loading: boolean; 14 | error: string | null; 15 | }; 16 | 17 | export type TaskAction = 18 | | SetTasksAction 19 | | AddTaskAction 20 | | UpdateTaskAction 21 | | RemoveTaskAction 22 | | SetLoadingAction 23 | | SetErrorAction; 24 | 25 | export type SetTasksAction = { 26 | type: 'set-tasks'; 27 | payload: Task[]; 28 | }; 29 | 30 | export type AddTaskAction = { 31 | type: 'add-task'; 32 | payload: Task; 33 | }; 34 | 35 | export type UpdateTaskAction = { 36 | type: 'update-task'; 37 | payload: TaskData & { id: string }; 38 | }; 39 | 40 | export type RemoveTaskAction = { 41 | type: 'remove-task'; 42 | payload: string; 43 | }; 44 | 45 | export type SetLoadingAction = { 46 | type: 'set-loading'; 47 | payload?: never; 48 | }; 49 | 50 | export type SetErrorAction = { 51 | type: 'set-error'; 52 | payload: string; 53 | }; 54 | -------------------------------------------------------------------------------- /examples/task-list/styles.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | body { 7 | @apply bg-slate-50 text-slate-900 dark:bg-slate-900 dark:text-slate-50; 8 | } 9 | 10 | label { 11 | @apply block text-sm font-medium leading-6; 12 | } 13 | 14 | input[type='text'], 15 | input[type='email'], 16 | input[type='password'] { 17 | @apply ring-primary-600 placeholder:text-primary-600 block w-full rounded-md border-0 px-2.5 py-1.5 text-sm shadow-sm ring-1 ring-inset sm:text-base sm:text-sm sm:leading-6 dark:bg-slate-800 dark:text-slate-50 dark:placeholder:text-slate-400; 18 | @apply focus:outline-none focus:ring-2; 19 | &:has(+ button[type='submit']) { 20 | @apply block w-full rounded-r-none; 21 | } 22 | } 23 | 24 | input[type='checkbox'] { 25 | @apply border-primary-500 accent-primary-600 dark:accent-primary-600 h-4 w-4 rounded dark:border-slate-700 dark:bg-slate-800; 26 | } 27 | 28 | button { 29 | @apply button; 30 | input + & { 31 | @apply -ml-px rounded-l-none; 32 | } 33 | &[type='submit'] { 34 | @apply button-primary; 35 | } 36 | } 37 | } 38 | 39 | @layer components { 40 | .button { 41 | @apply ring-primary-300 relative inline-flex items-center justify-center gap-x-1.5 whitespace-pre rounded-md px-3 py-2 text-sm font-semibold text-slate-900 ring-1 ring-inset hover:bg-slate-50 disabled:cursor-not-allowed sm:text-base; 42 | } 43 | 44 | .button-primary { 45 | @apply bg-primary-600 ring-primary-600 hover:bg-primary-700 active:bg-primary-800 disabled:bg-primary-600/50 disabled:hover:bg-primary-600/50 disabled:active:bg-primary-600/50 cursor-pointer text-white transition duration-100 ease-in-out disabled:cursor-not-allowed; 46 | } 47 | 48 | .button-small { 49 | @apply px-1.5 py-1 text-xs; 50 | } 51 | 52 | .button-destructive { 53 | @apply cursor-pointer bg-red-600 text-white ring-0 transition duration-100 ease-in-out hover:bg-red-700 active:bg-red-800 disabled:cursor-not-allowed disabled:bg-red-600/50 disabled:ring-red-700/20 disabled:hover:bg-red-600/50 disabled:active:bg-red-600/50; 54 | &.button-ghost { 55 | @apply bg-transparent text-red-600 ring-red-600 hover:bg-red-600/10 active:bg-red-600/20 dark:hover:bg-red-400/30 dark:active:bg-red-400/40; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /examples/task-list/tests/task-list.spec.js: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | /** @type {import('../start-server').DevelopmentServer} */ 4 | test.beforeEach(async ({ page }) => { 5 | await page.goto('http://localhost:5173'); 6 | }); 7 | 8 | test('it should load the page', async ({ page }) => { 9 | await expect(page).toHaveTitle('Task List'); 10 | }); 11 | 12 | test('it should add a task', async ({ page }) => { 13 | const input = page.getByLabel('Create Task'); 14 | const submit = page.getByRole('button', { name: 'Create Task' }); 15 | 16 | await input.fill('Learn Playwright'); 17 | await submit.click(); 18 | 19 | const heading = await page.getByRole('heading', { name: 'Learn Playwright' }); 20 | 21 | await expect(heading).toBeVisible(); 22 | }); 23 | -------------------------------------------------------------------------------- /examples/task-list/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import { css } from 'css-configuration'; 4 | 5 | const PORT = process.env.PORT || 3000; 6 | 7 | export default defineConfig({ 8 | plugins: [react()], 9 | css, 10 | server: { 11 | proxy: { 12 | '/api': { 13 | target: `http://localhost:${PORT}`, 14 | changeOrigin: true, 15 | secure: false, 16 | }, 17 | }, 18 | }, 19 | test: { 20 | globals: true, 21 | environment: 'happy-dom', 22 | setupFiles: ['@testing-library/jest-dom/vitest'], 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /examples/utility-belt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "utility-belt", 3 | "version": "1.0.0", 4 | "main": "src/index.js", 5 | "type": "module", 6 | "scripts": { 7 | "start": "vitest --ui", 8 | "test": "vitest" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/stevekinney/testing-javascript.git" 13 | }, 14 | "author": "Steve Kinney ", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/stevekinney/testing-javascript/issues" 18 | }, 19 | "homepage": "https://github.com/stevekinney/testing-javascript#readme", 20 | "devDependencies": { 21 | "@vitest/ui": "^2.1.1", 22 | "vite": "^5.4.5", 23 | "vitest": "^2.1.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/utility-belt/src/index.js: -------------------------------------------------------------------------------- 1 | export * from './string-to-number'; 2 | -------------------------------------------------------------------------------- /examples/utility-belt/src/string-to-number.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts a string to a number or throws an error if the string is not a number. 3 | * @param {string} value 4 | * @returns {number} The number. 5 | */ 6 | export const stringToNumber = (value) => { 7 | const number = Number(value); 8 | 9 | if (isNaN(number)) { 10 | throw new Error(`'${value}' cannot be parsed as a number.`); 11 | } 12 | 13 | return number; 14 | }; 15 | -------------------------------------------------------------------------------- /examples/utility-belt/src/string-to-number.test.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { stringToNumber } from './string-to-number'; 3 | 4 | describe('stringToNumber', () => { 5 | it('converts a string to a number', () => { 6 | expect(stringToNumber('42')).toBe(42); 7 | }); 8 | 9 | it.todo('throws an error if given a string that is not a number', () => {}); 10 | }); 11 | -------------------------------------------------------------------------------- /examples/utility-belt/vitest.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | environment: 'node', 6 | globals: true, 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "node", 7 | "paths": { 8 | "*": ["node_modules/*"], 9 | "./*": ["./*"], 10 | "./*.html": ["./*"], 11 | "./*.html?raw": ["./*"] 12 | } 13 | }, 14 | "include": ["examples/**/*.js", "examples/**/*.svelte"], 15 | "exclude": ["node_modules", "dist"] 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testing-javascript", 3 | "version": "1.0.0", 4 | "description": "A set of examples and exercises for Steve Kinney's \"Introduction to Testing\" course for Frontend Masters.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Please run the tests from inside one of the examples!\" && exit 0", 8 | "format": "prettier --write ." 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/stevekinney/testing-javascript.git" 13 | }, 14 | "keywords": [ 15 | "testing", 16 | "vitest", 17 | "unit", 18 | "testing", 19 | "javascript", 20 | "tutorial" 21 | ], 22 | "author": "Steve Kinney ", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/stevekinney/testing-javascript/issues" 26 | }, 27 | "homepage": "https://github.com/stevekinney/testing-javascript#readme", 28 | "workspaces": [ 29 | "packages/css-configuration", 30 | "packages/get-error-message", 31 | "examples/calculator", 32 | "examples/task-list", 33 | "examples/guess-the-number", 34 | "examples/basic-math", 35 | "examples/characters", 36 | "examples/accident-counter", 37 | "examples/scratchpad", 38 | "examples/utility-belt", 39 | "examples/strictly-speaking", 40 | "examples/element-factory", 41 | "examples/logjam", 42 | "examples/directory" 43 | ], 44 | "devDependencies": { 45 | "prettier": "^3.3.3", 46 | "prettier-plugin-tailwindcss": "^0.6.6" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/css-configuration/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @type import('vite').UserConfig['css'] 3 | */ 4 | export const css: import('vite').UserConfig['css']; 5 | -------------------------------------------------------------------------------- /packages/css-configuration/index.js: -------------------------------------------------------------------------------- 1 | import nesting from 'tailwindcss/nesting/index.js'; 2 | import tailwindcss from 'tailwindcss'; 3 | import autoprefixer from 'autoprefixer'; 4 | 5 | /** 6 | * @type import('vite').UserConfig['css'] 7 | */ 8 | export const css = { 9 | postcss: { 10 | plugins: [ 11 | tailwindcss({ 12 | content: ['./src/**/*.{html,js,jsx,ts,tsx}', './index.html'], 13 | theme: { 14 | extend: { 15 | container: { 16 | center: true, 17 | padding: '1rem', 18 | }, 19 | colors: { 20 | primary: { 21 | 50: '#f3faeb', 22 | 100: '#e5f3d4', 23 | 200: '#cde8ae', 24 | 300: '#acd87e', 25 | 400: '#8ec655', 26 | 500: '#6ca635', 27 | 600: '#558828', 28 | 700: '#426823', 29 | 800: '#375420', 30 | 900: '#30481f', 31 | 950: '#17270c', 32 | }, 33 | }, 34 | }, 35 | }, 36 | plugins: [], 37 | }), 38 | nesting, 39 | autoprefixer, 40 | ], 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /packages/css-configuration/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-configuration", 3 | "version": "1.0.0", 4 | "description": "Some CSS settings for Vite/PostCSS that includes Tailwind", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "types": "tsc --emitDeclarationOnly --allowJs --declaration --skipLibCheck index.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/stevekinney/testing-javascript.git" 13 | }, 14 | "author": "Steve Kinney ", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/stevekinney/testing-javascript/issues" 18 | }, 19 | "homepage": "https://github.com/stevekinney/testing-javascript#readme", 20 | "devDependencies": { 21 | "autoprefixer": "^10.4.20", 22 | "tailwindcss": "^3.4.12", 23 | "typescript": "^5.6.2", 24 | "vite": "^5.4.6" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/get-error-message/index.d.ts: -------------------------------------------------------------------------------- 1 | export type RequireOnly = Partial & 2 | Required>; 3 | export declare const isError: (error: unknown) => error is Error; 4 | export declare const getErrorMessage: ( 5 | error: unknown, 6 | defaultMessage: string, 7 | ) => string; 8 | -------------------------------------------------------------------------------- /packages/get-error-message/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @param {unknown} error The error object to check. 4 | * @returns {error is Error} 5 | */ 6 | export const isError = (error) => error instanceof Error; 7 | 8 | /** 9 | * Get the error message from an error object or return a default message. 10 | * @param {unknown} error The error object to check. 11 | * @param {string} defaultMessage The default message to return if the error is not an instance of Error. 12 | * @returns {string} The error message or the default message. 13 | */ 14 | export const getErrorMessage = (error, defaultMessage) => { 15 | return isError(error) ? error.message : defaultMessage; 16 | }; 17 | -------------------------------------------------------------------------------- /packages/get-error-message/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "get-error-message", 3 | "version": "1.0.0", 4 | "description": "A function that returns the error message from an error object.", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "types": "tsc --emitDeclarationOnly --allowJs --declaration --skipLibCheck index.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/stevekinney/testing-javascript.git" 13 | }, 14 | "author": "Steve Kinney ", 15 | "license": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/stevekinney/testing-javascript/issues" 18 | }, 19 | "homepage": "https://github.com/stevekinney/testing-javascript#readme" 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "node", 7 | "allowSyntheticDefaultImports": true, 8 | "paths": { 9 | "*": ["node_modules/*"] 10 | }, 11 | "jsx": "react-jsx", 12 | "forceConsistentCasingInFileNames": true 13 | }, 14 | "include": [ 15 | "examples/**/*.js", 16 | "examples/**/*.svelte", 17 | "examples/**/*.ts", 18 | "examples/directory/src/user.jsx" 19 | ], 20 | "exclude": ["node_modules", "dist"] 21 | } 22 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.html' { 4 | const value: string; 5 | export default value; 6 | } 7 | 8 | declare module '*.html?raw' { 9 | const value: string; 10 | export default value; 11 | } 12 | --------------------------------------------------------------------------------