├── .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 | [](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 |
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 | Company |
22 | {user.company.name} |
23 |
24 |
25 | Username |
26 | {user.username} |
27 |
28 |
29 | Email |
30 | {user.email} |
31 |
32 |
33 | Phone |
34 | {user.phone} |
35 |
36 |
37 | Website |
38 | {user.website} |
39 |
40 |
41 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------