commit 7a9ffcd827c2e0603c8486d49e190c0e5e222513
parent fc80d58fec429f7c6d9765e97f9e0f3a6358cf76
Author: Asher Morgan <59518073+ashermorgan@users.noreply.github.com>
Date: Sun, 12 May 2024 16:19:10 -0700
Merge pull request #7 from ashermorgan/e2e-tests
Add end-to-end tests
Diffstat:
12 files changed, 846 insertions(+), 6 deletions(-)
diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml
@@ -23,5 +23,7 @@ jobs:
cache: 'npm'
- run: npm ci
+ - run: npx playwright install --with-deps
- run: npm run build --if-present
- run: npm run test:unit
+ - run: npm run test:e2e
diff --git a/.gitignore b/.gitignore
@@ -21,3 +21,7 @@ pnpm-debug.log*
*.njsproj
*.sln
*.sw?
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/README.md b/README.md
@@ -23,10 +23,11 @@ Run development server
npm run dev
```
-Run linter and unit tests
+Run linter and tests
```
npm run lint
npm run test:unit
+npm run test:e2e
```
Build for production
diff --git a/package-lock.json b/package-lock.json
@@ -14,6 +14,8 @@
"vue-router": "^4.2.2"
},
"devDependencies": {
+ "@playwright/test": "^1.44.0",
+ "@types/node": "^20.12.11",
"@vitejs/plugin-vue": "^4.2.3",
"@vue/test-utils": "^2.4.0",
"eslint": "^8.39.0",
@@ -2485,6 +2487,21 @@
"node": ">= 8"
}
},
+ "node_modules/@playwright/test": {
+ "version": "1.44.0",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.0.tgz",
+ "integrity": "sha512-rNX5lbNidamSUorBhB4XZ9SQTjAqfe5M+p37Z8ic0jPFBMo5iCtQz1kRWkEMg+rYOKSlVycpQmpqjSFq7LXOfg==",
+ "dev": true,
+ "dependencies": {
+ "playwright": "1.44.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
"node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@@ -2549,10 +2566,13 @@
"dev": true
},
"node_modules/@types/node": {
- "version": "20.4.0",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.4.0.tgz",
- "integrity": "sha512-jfT7iTf/4kOQ9S7CHV9BIyRaQqHu67mOjsIQBC3BKZvzvUB6zLxEwJ6sBE3ozcvP8kF6Uk5PXN0Q+c0dfhGX0g==",
- "dev": true
+ "version": "20.12.11",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.11.tgz",
+ "integrity": "sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw==",
+ "dev": true,
+ "dependencies": {
+ "undici-types": "~5.26.4"
+ }
},
"node_modules/@types/normalize-package-data": {
"version": "2.4.4",
@@ -6265,6 +6285,36 @@
"pathe": "^1.1.0"
}
},
+ "node_modules/playwright": {
+ "version": "1.44.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.0.tgz",
+ "integrity": "sha512-F9b3GUCLQ3Nffrfb6dunPOkE5Mh68tR7zN32L4jCk4FjQamgesGay7/dAAe1WaMEGV04DkdJfcJzjoCKygUaRQ==",
+ "dev": true,
+ "dependencies": {
+ "playwright-core": "1.44.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.44.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.0.tgz",
+ "integrity": "sha512-ZTbkNpFfYcGWohvTTl+xewITm7EOuqIqex0c7dNZ+aXsbrLj0qI8XlGKfPpipjm0Wny/4Lt4CJsWJk1stVS5qQ==",
+ "dev": true,
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
"node_modules/postcss": {
"version": "8.4.32",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz",
@@ -7594,6 +7644,12 @@
"through": "^2.3.8"
}
},
+ "node_modules/undici-types": {
+ "version": "5.26.5",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+ "dev": true
+ },
"node_modules/unicode-canonical-property-names-ecmascript": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz",
diff --git a/package.json b/package.json
@@ -8,7 +8,8 @@
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore",
- "test:unit": "vitest"
+ "test:unit": "vitest",
+ "test:e2e": "npx playwright test"
},
"dependencies": {
"feather-icons": "^4.29.0",
@@ -17,6 +18,8 @@
"vue-router": "^4.2.2"
},
"devDependencies": {
+ "@playwright/test": "^1.44.0",
+ "@types/node": "^20.12.11",
"@vitejs/plugin-vue": "^4.2.3",
"@vue/test-utils": "^2.4.0",
"eslint": "^8.39.0",
diff --git a/playwright.config.js b/playwright.config.js
@@ -0,0 +1,54 @@
+// @ts-check
+const { defineConfig, devices } = require('@playwright/test');
+
+/**
+ * Read environment variables from file.
+ * https://github.com/motdotla/dotenv
+ */
+// require('dotenv').config();
+
+/**
+ * @see https://playwright.dev/docs/test-configuration
+ */
+module.exports = defineConfig({
+ testDir: './tests/e2e',
+ /* Run tests in files in parallel */
+ fullyParallel: true,
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
+ forbidOnly: !!process.env.CI,
+ /* Retry on CI only */
+ retries: process.env.CI ? 2 : 0,
+ /* Opt out of parallel tests on CI. */
+ workers: process.env.CI ? 1 : undefined,
+ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
+ reporter: 'html',
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ /* Base URL to use in actions like `await page.goto('/')`. */
+ baseURL: 'http://localhost:5173',
+
+ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
+ trace: 'on-first-retry',
+ },
+
+ /* Configure projects for major browsers */
+ projects: [
+ {
+ name: 'chromium',
+ use: { ...devices['Desktop Chrome'] },
+ },
+
+ {
+ name: 'firefox',
+ use: { ...devices['Desktop Firefox'] },
+ },
+ ],
+
+ /* Run your local dev server before starting the tests */
+ webServer: {
+ command: 'npm run dev',
+ url: 'http://localhost:5173',
+ reuseExistingServer: !process.env.CI,
+ },
+});
+
diff --git a/tests/e2e/cross-calculator.spec.js b/tests/e2e/cross-calculator.spec.js
@@ -0,0 +1,108 @@
+const { test, expect } = require('@playwright/test');
+
+test('Save and update state when navigating between calculators', async ({ page }) => {
+ // Go to pace calculator
+ await page.goto('/');
+ await page.getByRole('button', { name: 'Pace Calculator' }).click();
+
+ // Enter input pace (2 mi in 15:30)
+ await page.getByLabel('Input distance value').fill('2');
+ await page.getByLabel('Input distance unit').selectOption('Miles');
+ await page.getByLabel('Input duration hours').fill('0');
+ await page.getByLabel('Input duration minutes').fill('15');
+ await page.getByLabel('Input duration seconds').fill('30');
+
+ // Change default units (should update on other calculators too)
+ await page.getByText('Advanced Options').click();
+ await page.getByLabel('Default units').selectOption('Kilometers');
+
+ // Switch target set
+ await page.getByLabel('Selected target set').selectOption('5K Mile Splits');
+
+ // Go to race calculator
+ await page.getByRole('link', { name: 'Back' }).click();
+ await page.getByRole('button', { name: 'Race Calculator' }).click();
+
+ // Enter input race (2 mi in 10:30)
+ await page.getByLabel('Input distance value').fill('2');
+ await page.getByLabel('Input distance unit').selectOption('Miles');
+ await page.getByLabel('Input race duration hours').fill('0');
+ await page.getByLabel('Input race duration minutes').fill('10');
+ await page.getByLabel('Input race duration seconds').fill('30');
+
+ // Change prediction model
+ await page.getByText('Advanced Options').click();
+ await page.getByLabel('Prediction model').selectOption('Riegel\'s Model');
+
+ // Go to split calculator
+ await page.getByRole('link', { name: 'Back' }).click();
+ await page.getByRole('button', { name: 'Split Calculator' }).click();
+
+ // Edit target set
+ await page.getByRole('button', { name: 'Edit target set' }).click();
+ await page.getByLabel('Target set label').fill('5K 1600m Splits');
+ await page.getByLabel('Target distance value').nth(0).fill('1.6');
+ await page.getByLabel('Target distance unit').nth(0).selectOption('Kilometers');
+ await page.getByLabel('Target distance value').nth(1).fill('3.2');
+ await page.getByLabel('Target distance unit').nth(1).selectOption('Kilometers');
+ await page.getByRole('button', { name: 'Add time target' }).click();
+ await page.getByRole('button', { name: 'Close' }).click();
+
+ // Enter input 5K splits (7:00, 6:30, 6:30)
+ await page.getByLabel('Split duration minutes').nth(0).fill('7');
+ await page.getByLabel('Split duration seconds').nth(0).fill('0');
+ await page.getByLabel('Split duration minutes').nth(1).fill('6');
+ await page.getByLabel('Split duration seconds').nth(1).fill('30');
+ await page.getByLabel('Split duration minutes').nth(2).fill('6');
+ await page.getByLabel('Split duration seconds').nth(2).fill('30');
+
+ // Go to unit calculator
+ await page.getByRole('link', { name: 'Back' }).click();
+ await page.getByRole('button', { name: 'Unit Calculator' }).click();
+
+ // Convert speed and pace units (10 kph to time per mile)
+ await page.getByLabel('Selected unit category').selectOption('Speed & Pace');
+ await page.getByLabel('Input units').selectOption('Kilometers per Hour');
+ await page.getByLabel('Input value').fill('10');
+ await page.getByLabel('Output units').selectOption('Time per Mile');
+
+ // Return to pace calculator
+ await page.getByRole('link', { name: 'Back' }).click();
+ await page.getByRole('button', { name: 'Pace Calculator' }).click();
+
+ // Assert paces are correct (input pace not reset)
+ await expect(page.getByRole('row').nth(1)).toHaveText('1.6 km' + '7:42.30');
+ await expect(page.getByRole('row').nth(2)).toHaveText('2.08 km' + '10:00');
+ await expect(page.getByRole('row').nth(3)).toHaveText('3.2 km' + '15:24.60');
+ await expect(page.getByRole('row').nth(4)).toHaveText('5 km' + '24:04.68');
+ await expect(page.getByRole('row')).toHaveCount(5);
+
+ // Return to race calculator
+ await page.getByRole('link', { name: 'Back' }).click();
+ await page.getByRole('button', { name: 'Race Calculator' }).click();
+
+ // Assert race predictions are correct (input race and prediction model not reset)
+ await expect(page.getByRole('row').nth(5)).toHaveText('1 mi' + '5:02.17' + '3:08 / km');
+ await expect(page.getByRole('row').nth(10)).toHaveText('5 km' + '16:44.86' + '3:21 / km');
+ await expect(page.getByRole('row')).toHaveCount(17);
+
+ // Return to split calculator
+ await page.getByRole('link', { name: 'Back' }).click();
+ await page.getByRole('button', { name: 'Split Calculator' }).click();
+
+ // Assert times and paces are correct (split times not reset)
+ await expect(page.getByRole('row').nth(1).getByRole('cell').nth(1)).toHaveText('7:00.00');
+ await expect(page.getByRole('row').nth(1).getByRole('cell').nth(3)).toHaveText('4:23 / km');
+ await expect(page.getByRole('row').nth(2).getByRole('cell').nth(1)).toHaveText('13:30.00');
+ await expect(page.getByRole('row').nth(2).getByRole('cell').nth(3)).toHaveText('4:04 / km');
+ await expect(page.getByRole('row').nth(3).getByRole('cell').nth(1)).toHaveText('20:00.00');
+ await expect(page.getByRole('row').nth(3).getByRole('cell').nth(3)).toHaveText('3:37 / km');
+ await expect(page.getByRole('row')).toHaveCount(4);
+
+ // Return to unit calculator
+ await page.getByRole('link', { name: 'Back' }).click();
+ await page.getByRole('button', { name: 'Unit Calculator' }).click();
+
+ // Assert result is correct (state not reset)
+ await expect(page.getByLabel('Output value')).toHaveText('00:09:39.366');
+});
diff --git a/tests/e2e/pace-calculator.spec.js b/tests/e2e/pace-calculator.spec.js
@@ -0,0 +1,163 @@
+const { test, expect } = require('@playwright/test');
+
+test('Basic usage', async ({ page }) => {
+ await page.goto('/');
+
+ // Go to pace calculator
+ await page.getByRole('button', { name: 'Pace Calculator' }).click();
+ await expect(page).toHaveTitle('Pace Calculator - Running Tools');
+
+ // Enter input pace (2 mi in 15:30)
+ await page.getByLabel('Input distance value').fill('2');
+ await page.getByLabel('Input distance unit').selectOption('Miles');
+ await page.getByLabel('Input duration hours').fill('0');
+ await page.getByLabel('Input duration minutes').fill('15');
+ await page.getByLabel('Input duration seconds').fill('30');
+
+ // Assert paces are correct
+ await expect(page.getByRole('row').nth(11)).toHaveText('1 mi' + '7:45.00');
+ await expect(page.getByRole('row').nth(13)).toHaveText('1.29 mi' + '10:00');
+ await expect(page.getByRole('row')).toHaveCount(31);
+
+ // Change default units
+ await page.getByText('Advanced Options').click();
+ await page.getByLabel('Default units').selectOption('Kilometers');
+
+ // Assert paces are correct
+ await expect(page.getByRole('row').nth(11)).toHaveText('1 mi' + '7:45.00');
+ await expect(page.getByRole('row').nth(13)).toHaveText('2.08 km' + '10:00');
+ await expect(page.getByRole('row')).toHaveCount(31);
+});
+
+test('Customize target sets', async ({ page }) => {
+ // Go to pace calculator
+ await page.goto('/');
+ await page.getByRole('button', { name: 'Pace Calculator' }).click();
+
+ // Enter input pace (2 mi in 15:30)
+ await page.getByLabel('Input distance value').fill('2');
+ await page.getByLabel('Input distance unit').selectOption('Miles');
+ await page.getByLabel('Input duration hours').fill('0');
+ await page.getByLabel('Input duration minutes').fill('15');
+ await page.getByLabel('Input duration seconds').fill('30');
+
+ // Edit default target set
+ await page.getByText('Advanced Options').click();
+ await page.getByRole('button', { name: 'Edit target set' }).click();
+ await page.getByLabel('Target set label').fill('Less-common Pace Targets');
+ await page.getByLabel('Target distance value').nth(10).fill('1.01');
+ await page.getByLabel('Target distance unit').nth(10).selectOption('Miles');
+ await page.getByLabel('Target duration second').nth(0).fill('1');
+ await page.getByRole('button', { name: 'Add distance target' }).click();
+ await page.getByLabel('Target distance value').last().fill('1.5');
+ await page.getByLabel('Target distance unit').last().selectOption('Miles');
+ await page.getByRole('button', { name: 'Add time target' }).click();
+ await page.getByLabel('Target duration minutes').last().fill('19');
+ await page.getByLabel('Target duration seconds').last().fill('0');
+ await page.getByRole('button', { name: 'Close' }).click();
+
+ // Assert paces are correct
+ await expect(page.getByRole('row').nth(11)).toHaveText('1.01 mi' + '7:49.65');
+ await expect(page.getByRole('row').nth(13)).toHaveText('1.29 mi' + '10:01');
+ await expect(page.getByRole('row').nth(14)).toHaveText('1.5 mi' + '11:37.50');
+ await expect(page.getByRole('row').nth(18)).toHaveText('2.45 mi' + '19:00');
+ await expect(page.getByRole('row')).toHaveCount(33);
+
+ // Switch target set
+ await page.getByLabel('Selected target set').selectOption('5K Mile Splits');
+
+ // Assert paces are correct
+ await expect(page.getByRole('row').nth(1)).toHaveText('1 mi' + '7:45.00');
+ await expect(page.getByRole('row').nth(2)).toHaveText('2 mi' + '15:30.00');
+ await expect(page.getByRole('row').nth(3)).toHaveText('5 km' + '24:04.68');
+ await expect(page.getByRole('row')).toHaveCount(4);
+
+ // Create custom target set
+ await page.getByLabel('Selected target set').selectOption('[ Create New Target Set ]');
+ await expect(page.getByRole('row').nth(1)).toHaveText('There aren\'t any targets in this set yet.');
+ await expect(page.getByRole('row')).toHaveCount(2);
+
+ // Edit new target set
+ await page.getByRole('button', { name: 'Edit target set' }).click();
+ await expect(page.getByLabel('Target set label')).toHaveValue('New target set');
+ await page.getByLabel('Target set label').fill('800m Splits');
+ await page.getByRole('button', { name: 'Add distance target' }).click();
+ await page.getByLabel('Target distance value').nth(0).fill('0.4');
+ await page.getByLabel('Target distance unit').nth(0).selectOption('Kilometers');
+ await page.getByRole('button', { name: 'Add distance target' }).click();
+ await page.getByLabel('Target distance value').nth(1).fill('800');
+ await page.getByLabel('Target distance unit').nth(1).selectOption('Meters');
+ await page.getByRole('button', { name: 'Close' }).click();
+
+ // Assert paces are correct
+ await expect(page.getByRole('row').nth(1)).toHaveText('0.4 km' + '1:55.57');
+ await expect(page.getByRole('row').nth(2)).toHaveText('800 m' + '3:51.15');
+ await expect(page.getByRole('row')).toHaveCount(3);
+
+ // Delete custom target set
+ await page.getByRole('button', { name: 'Edit target set' }).click();
+ await expect(page.getByLabel('Target set label')).toHaveValue('800m Splits');
+ await page.getByRole('button', { name: 'Delete target set' }).click();
+
+ // Assert paces are correct (back to default target set)
+ await expect(page.getByRole('row').nth(11)).toHaveText('1.01 mi' + '7:49.65');
+ await expect(page.getByRole('row').nth(13)).toHaveText('1.29 mi' + '10:01');
+ await expect(page.getByRole('row').nth(14)).toHaveText('1.5 mi' + '11:37.50');
+ await expect(page.getByRole('row').nth(18)).toHaveText('2.45 mi' + '19:00');
+ await expect(page.getByRole('row')).toHaveCount(33);
+
+ // Revert target set
+ await page.getByRole('button', { name: 'Edit target set' }).click();
+ await expect(page.getByLabel('Target set label')).toHaveValue('Less-common Pace Targets');
+ await page.getByRole('button', { name: 'Revert target set' }).click();
+ await page.getByRole('button', { name: 'Close' }).click();
+
+ // Assert paces are correct
+ await expect(page.getByRole('row').nth(11)).toHaveText('1 mi' + '7:45.00');
+ await expect(page.getByRole('row').nth(13)).toHaveText('1.29 mi' + '10:00');
+ await expect(page.getByRole('row')).toHaveCount(31);
+
+ // Assert title was reset
+ await page.getByRole('button', { name: 'Edit target set' }).click();
+ await expect(page.getByLabel('Target set label')).toHaveValue('Common Pace Targets');
+});
+
+test('Save settings across page reloads', async ({ page }) => {
+ // Go to pace calculator
+ await page.goto('/');
+ await page.getByRole('button', { name: 'Pace Calculator' }).click();
+
+ // Enter input pace (2 mi in 15:30)
+ await page.getByLabel('Input distance value').fill('2');
+ await page.getByLabel('Input distance unit').selectOption('Miles');
+ await page.getByLabel('Input duration hours').fill('0');
+ await page.getByLabel('Input duration minutes').fill('15');
+ await page.getByLabel('Input duration seconds').fill('30');
+
+ // Create custom target set
+ await page.getByText('Advanced Options').click();
+ await page.getByLabel('Selected target set').selectOption('[ Create New Target Set ]');
+
+ // Edit new target set
+ await page.getByRole('button', { name: 'Edit target set' }).click();
+ await expect(page.getByLabel('Target set label')).toHaveValue('New target set');
+ await page.getByLabel('Target set label').fill('Less-common Pace Targets');
+ await page.getByRole('button', { name: 'Add distance target' }).click();
+ await page.getByLabel('Target distance value').last().fill('1.01');
+ await page.getByLabel('Target distance unit').last().selectOption('Miles');
+ await page.getByRole('button', { name: 'Add time target' }).click();
+ await page.getByLabel('Target duration minutes').last().fill('19');
+ await page.getByLabel('Target duration seconds').last().fill('0');
+ await page.getByRole('button', { name: 'Close' }).click();
+
+ // Change default units
+ await page.getByLabel('Default units').selectOption('Kilometers');
+
+ // Reload page
+ await page.reload();
+
+ // Assert paces are correct (custom targets and default units not reset)
+ await expect(page.getByRole('row').nth(1)).toHaveText('1.01 mi' + '7:49.65');
+ await expect(page.getByRole('row').nth(2)).toHaveText('3.95 km' + '19:00');
+ await expect(page.getByRole('row')).toHaveCount(3);
+});
diff --git a/tests/e2e/race-calculator.spec.js b/tests/e2e/race-calculator.spec.js
@@ -0,0 +1,189 @@
+const { test, expect } = require('@playwright/test');
+
+test('Basic usage', async ({ page }) => {
+ // Go to race calculator
+ await page.goto('/');
+ await page.getByRole('button', { name: 'Race Calculator' }).click();
+ await expect(page).toHaveTitle('Race Calculator - Running Tools');
+
+ // Enter input race (2 mi in 10:30)
+ await page.getByLabel('Input distance value').fill('2');
+ await page.getByLabel('Input distance unit').selectOption('Miles');
+ await page.getByLabel('Input race duration hours').fill('0');
+ await page.getByLabel('Input race duration minutes').fill('10');
+ await page.getByLabel('Input race duration seconds').fill('30');
+
+ // Assert race predictions are correct
+ await expect(page.getByRole('row').nth(5)).toHaveText('1 mi' + '4:55.53' + '4:56 / mi');
+ await expect(page.getByRole('row').nth(10)).toHaveText('5 km' + '16:47.58' + '5:24 / mi');
+ await expect(page.getByRole('row')).toHaveCount(17);
+
+ // Assert race statistics are correct
+ await page.getByText('Race Statistics').click();
+ await expect(page.getByText('Purdy Points:')).toContainText(': 680.1');
+ await expect(page.getByText('V̇O₂:')).toContainText(': 61.0 ml/kg/min (100.5% of max)');
+ await expect(page.getByText('V̇O₂ Max:')).toContainText(': 60.7 ml/kg/min');
+
+ // Change default units
+ await page.getByText('Advanced Options').click();
+ await page.getByLabel('Default units').selectOption('Kilometers');
+
+ // Assert race predictions are correct
+ await expect(page.getByRole('row').nth(5)).toHaveText('1 mi' + '4:55.53' + '3:04 / km');
+ await expect(page.getByRole('row').nth(10)).toHaveText('5 km' + '16:47.58' + '3:22 / km');
+ await expect(page.getByRole('row')).toHaveCount(17);
+
+ // Change prediction model
+ await page.getByLabel('Prediction model').selectOption('Riegel\'s Model');
+
+ // Assert race predictions are correct
+ await expect(page.getByRole('row').nth(5)).toHaveText('1 mi' + '5:02.17' + '3:08 / km');
+ await expect(page.getByRole('row').nth(10)).toHaveText('5 km' + '16:44.86' + '3:21 / km');
+ await expect(page.getByRole('row')).toHaveCount(17);
+
+ // Change Riegel exponent
+ await page.getByLabel('Riegel Exponent').fill('1.12');
+
+ // Assert race predictions are correct
+ await expect(page.getByRole('row').nth(5)).toHaveText('1 mi' + '4:49.86' + '3:00 / km');
+ await expect(page.getByRole('row').nth(10)).toHaveText('5 km' + '17:11.77' + '3:26 / km');
+ await expect(page.getByRole('row')).toHaveCount(17);
+});
+
+test('Customize target sets', async ({ page }) => {
+ // Go to race calculator
+ await page.goto('/');
+ await page.getByRole('button', { name: 'Race Calculator' }).click();
+
+ // Enter input race (2 mi in 10:30)
+ await page.getByLabel('Input distance value').fill('2');
+ await page.getByLabel('Input distance unit').selectOption('Miles');
+ await page.getByLabel('Input race duration hours').fill('0');
+ await page.getByLabel('Input race duration minutes').fill('10');
+ await page.getByLabel('Input race duration seconds').fill('30');
+
+ // Edit default target set
+ await page.getByText('Advanced Options').click();
+ await page.getByRole('button', { name: 'Edit target set' }).click();
+ await page.getByLabel('Target set label').fill('Less-common Race Targets');
+ await page.getByLabel('Target distance value').nth(4).fill('1.01');
+ await page.getByRole('button', { name: 'Add distance target' }).click();
+ await page.getByLabel('Target distance value').last().fill('1.5');
+ await page.getByLabel('Target distance unit').last().selectOption('Miles');
+ await page.getByRole('button', { name: 'Add time target' }).click();
+ await page.getByLabel('Target duration minutes').last().fill('19');
+ await page.getByLabel('Target duration seconds').last().fill('0');
+ await page.getByRole('button', { name: 'Close' }).click();
+
+ // Assert race predictions are correct
+ await expect(page.getByRole('row').nth(5)).toHaveText('1.01 mi' + '4:58.81' + '4:56 / mi');
+ await expect(page.getByRole('row').nth(6)).toHaveText('1.5 mi' + '7:41.60' + '5:08 / mi');
+ await expect(page.getByRole('row').nth(12)).toHaveText('3.49 mi' + '19:00' + '5:27 / mi');
+ await expect(page.getByRole('row')).toHaveCount(19);
+
+ // Switch target set
+ await page.getByLabel('Selected target set').selectOption('5K Mile Splits');
+
+ // Assert race predictions are correct
+ await expect(page.getByRole('row').nth(1)).toHaveText('1 mi' + '4:55.53' + '4:56 / mi');
+ await expect(page.getByRole('row').nth(2)).toHaveText('2 mi' + '10:30.00' + '5:15 / mi');
+ await expect(page.getByRole('row').nth(3)).toHaveText('5 km' + '16:47.58' + '5:24 / mi');
+ await expect(page.getByRole('row')).toHaveCount(4);
+
+ // Create custom target set
+ await page.getByLabel('Selected target set').selectOption('[ Create New Target Set ]');
+ await expect(page.getByRole('row').nth(1)).toHaveText('There aren\'t any targets in this set yet.');
+ await expect(page.getByRole('row')).toHaveCount(2);
+
+ // Edit new target set
+ await page.getByRole('button', { name: 'Edit target set' }).click();
+ await expect(page.getByLabel('Target set label')).toHaveValue('New target set');
+ await page.getByLabel('Target set label').fill('XC Race Targets');
+ await page.getByRole('button', { name: 'Add distance target' }).click();
+ await page.getByLabel('Target distance value').nth(0).fill('5');
+ await page.getByLabel('Target distance unit').nth(0).selectOption('Kilometers');
+ await page.getByRole('button', { name: 'Add distance target' }).click();
+ await page.getByLabel('Target distance value').nth(1).fill('10');
+ await page.getByLabel('Target distance unit').nth(1).selectOption('Kilometers');
+ await page.getByRole('button', { name: 'Close' }).click();
+
+ // Assert race predictions are correct
+ await expect(page.getByRole('row').nth(1)).toHaveText('5 km' + '16:47.58' + '5:24 / mi');
+ await expect(page.getByRole('row').nth(2)).toHaveText('10 km' + '34:53.84' + '5:37 / mi');
+ await expect(page.getByRole('row')).toHaveCount(3);
+
+ // Delete custom target set
+ await page.getByRole('button', { name: 'Edit target set' }).click();
+ await expect(page.getByLabel('Target set label')).toHaveValue('XC Race Targets');
+ await page.getByRole('button', { name: 'Delete target set' }).click();
+
+ // Switch to default target set
+ await page.getByLabel('Selected target set').selectOption('Less-common Race Targets');
+
+ // Assert race predictions are correct (back to default target set)
+ await expect(page.getByRole('row').nth(5)).toHaveText('1.01 mi' + '4:58.81' + '4:56 / mi');
+ await expect(page.getByRole('row').nth(6)).toHaveText('1.5 mi' + '7:41.60' + '5:08 / mi');
+ await expect(page.getByRole('row').nth(12)).toHaveText('3.49 mi' + '19:00' + '5:27 / mi');
+ await expect(page.getByRole('row')).toHaveCount(19);
+
+ // Revert target set
+ await page.getByRole('button', { name: 'Edit target set' }).click();
+ await expect(page.getByLabel('Target set label')).toHaveValue('Less-common Race Targets');
+ await page.getByRole('button', { name: 'Revert target set' }).click();
+ await page.getByRole('button', { name: 'Close' }).click();
+
+ // Assert paces are correct
+ await expect(page.getByRole('row').nth(5)).toHaveText('1 mi' + '4:55.53' + '4:56 / mi');
+ await expect(page.getByRole('row').nth(10)).toHaveText('5 km' + '16:47.58' + '5:24 / mi');
+ await expect(page.getByRole('row')).toHaveCount(17);
+
+ // Assert title was reset
+ await page.getByRole('button', { name: 'Edit target set' }).click();
+ await expect(page.getByLabel('Target set label')).toHaveValue('Common Race Targets');
+});
+
+test('Save settings across page reloads', async ({ page }) => {
+ // Go to race calculator
+ await page.goto('/');
+ await page.getByRole('button', { name: 'Race Calculator' }).click();
+
+ // Enter input race (2 mi in 10:30)
+ await page.getByLabel('Input distance value').fill('2');
+ await page.getByLabel('Input distance unit').selectOption('Miles');
+ await page.getByLabel('Input race duration hours').fill('0');
+ await page.getByLabel('Input race duration minutes').fill('10');
+ await page.getByLabel('Input race duration seconds').fill('30');
+
+ // Create custom target set
+ await page.getByText('Advanced Options').click();
+ await page.getByLabel('Selected target set').selectOption('[ Create New Target Set ]');
+
+ // Edit new target set
+ await page.getByRole('button', { name: 'Edit target set' }).click();
+ await expect(page.getByLabel('Target set label')).toHaveValue('New target set');
+ await page.getByLabel('Target set label').fill('Less-common Race Targets');
+ await page.getByRole('button', { name: 'Add distance target' }).click();
+ await page.getByLabel('Target distance value').last().fill('1.01');
+ await page.getByLabel('Target distance unit').last().selectOption('Miles');
+ await page.getByRole('button', { name: 'Add time target' }).click();
+ await page.getByLabel('Target duration minutes').last().fill('19');
+ await page.getByLabel('Target duration seconds').last().fill('0');
+ await page.getByRole('button', { name: 'Close' }).click();
+
+ // Change default units
+ await page.getByLabel('Default units').selectOption('Kilometers');
+
+ // Change prediction model
+ await page.getByLabel('Prediction model').selectOption('Riegel\'s Model');
+
+ // Change Riegel exponent
+ await page.getByLabel('Riegel Exponent').fill('1.12');
+
+ // Reload page
+ await page.reload();
+
+ // Assert race predictions are correct (custom targets, default units, and model settings not reset)
+ await expect(page.getByRole('row').nth(1)).toHaveText('1.01 mi' + '4:53.11' + '3:00 / km');
+ await expect(page.getByRole('row').nth(2)).toHaveText('5.47 km' + '19:00' + '3:29 / km');
+ await expect(page.getByRole('row')).toHaveCount(3);
+});
diff --git a/tests/e2e/split-calculator.spec.js b/tests/e2e/split-calculator.spec.js
@@ -0,0 +1,168 @@
+const { test, expect } = require('@playwright/test');
+
+test('Basic usage', async ({ page }) => {
+ // Go to split calculator
+ await page.goto('/');
+ await page.getByRole('button', { name: 'Split Calculator' }).click();
+ await expect(page).toHaveTitle('Split Calculator - Running Tools');
+
+ // Enter input 5K splits (7:00, 6:30, 6:30)
+ await page.getByLabel('Split duration minutes').nth(0).fill('7');
+ await page.getByLabel('Split duration seconds').nth(0).fill('0');
+ await page.getByLabel('Split duration minutes').nth(1).fill('6');
+ await page.getByLabel('Split duration seconds').nth(1).fill('30');
+ await page.getByLabel('Split duration minutes').nth(2).fill('6');
+ await page.getByLabel('Split duration seconds').nth(2).fill('30');
+
+ // Assert times and paces are correct
+ await expect(page.getByRole('row').nth(1).getByRole('cell').nth(1)).toHaveText('7:00.00');
+ await expect(page.getByRole('row').nth(1).getByRole('cell').nth(3)).toHaveText('7:00 / mi');
+ await expect(page.getByRole('row').nth(2).getByRole('cell').nth(1)).toHaveText('13:30.00');
+ await expect(page.getByRole('row').nth(2).getByRole('cell').nth(3)).toHaveText('6:30 / mi');
+ await expect(page.getByRole('row').nth(3).getByRole('cell').nth(1)).toHaveText('20:00.00');
+ await expect(page.getByRole('row').nth(3).getByRole('cell').nth(3)).toHaveText('5:52 / mi');
+ await expect(page.getByRole('row')).toHaveCount(4);
+
+ // Change default units
+ await page.getByLabel('Default units').selectOption('Kilometers');
+
+ // Assert paces are correct
+ await expect(page.getByRole('row').nth(1).getByRole('cell').nth(3)).toHaveText('4:21 / km');
+ await expect(page.getByRole('row').nth(2).getByRole('cell').nth(3)).toHaveText('4:02 / km');
+ await expect(page.getByRole('row').nth(3).getByRole('cell').nth(3)).toHaveText('3:39 / km');
+});
+
+test('Customize target sets', async ({ page }) => {
+ // Go to split calculator
+ await page.goto('/');
+ await page.getByRole('button', { name: 'Split Calculator' }).click();
+
+ // Enter input 5K splits (7:00, 6:30, 6:30)
+ await page.getByLabel('Split duration minutes').nth(0).fill('7');
+ await page.getByLabel('Split duration seconds').nth(0).fill('0');
+ await page.getByLabel('Split duration minutes').nth(1).fill('6');
+ await page.getByLabel('Split duration seconds').nth(1).fill('30');
+ await page.getByLabel('Split duration minutes').nth(2).fill('6');
+ await page.getByLabel('Split duration seconds').nth(2).fill('30');
+
+ // Edit target set
+ await page.getByRole('button', { name: 'Edit target set' }).click();
+ await page.getByLabel('Target set label').fill('5K 1600m Splits');
+ await page.getByLabel('Target distance value').nth(0).fill('1.6');
+ await page.getByLabel('Target distance unit').nth(0).selectOption('Kilometers');
+ await page.getByLabel('Target distance value').nth(1).fill('3.2');
+ await page.getByLabel('Target distance unit').nth(1).selectOption('Kilometers');
+ await page.getByRole('button', { name: 'Add distance target' }).click();
+ await page.getByLabel('Target distance value').nth(3).fill('4.8');
+ await page.getByLabel('Target distance unit').nth(3).selectOption('Kilometers');
+ await page.getByRole('button', { name: 'Add time target' }).click();
+ await page.getByRole('button', { name: 'Close' }).click();
+
+ // Assert times and paces are correct (new distances are processed)
+ await expect(page.getByRole('row').nth(1).getByRole('cell').nth(1)).toHaveText('7:00.00');
+ await expect(page.getByRole('row').nth(1).getByRole('cell').nth(3)).toHaveText('7:02 / mi');
+ await expect(page.getByRole('row').nth(2).getByRole('cell').nth(1)).toHaveText('13:30.00');
+ await expect(page.getByRole('row').nth(2).getByRole('cell').nth(3)).toHaveText('6:32 / mi');
+ await expect(page.getByRole('row').nth(3).getByRole('cell').nth(1)).toHaveText('13:30.00');
+ await expect(page.getByRole('row').nth(3).getByRole('cell').nth(3)).toHaveText('0:00 / mi');
+ await expect(page.getByRole('row').nth(4).getByRole('cell').nth(1)).toHaveText('20:00.00');
+ await expect(page.getByRole('row').nth(4).getByRole('cell').nth(3)).toHaveText('52:18 / mi');
+ await expect(page.getByRole('row')).toHaveCount(5);
+
+ // Update third and fourth splits
+ await page.getByLabel('Split duration minutes').nth(2).fill('6');
+ await page.getByLabel('Split duration seconds').nth(2).fill('0');
+ await page.getByLabel('Split duration minutes').nth(3).fill('0');
+ await page.getByLabel('Split duration seconds').nth(3).fill('30');
+ await page.getByLabel('Split duration seconds').nth(3).blur();
+
+ // Assert times and paces are correct (new input splits are processed)
+ await expect(page.getByRole('row').nth(1).getByRole('cell').nth(1)).toHaveText('7:00.00');
+ await expect(page.getByRole('row').nth(1).getByRole('cell').nth(3)).toHaveText('7:02 / mi');
+ await expect(page.getByRole('row').nth(2).getByRole('cell').nth(1)).toHaveText('13:30.00');
+ await expect(page.getByRole('row').nth(2).getByRole('cell').nth(3)).toHaveText('6:32 / mi');
+ await expect(page.getByRole('row').nth(3).getByRole('cell').nth(1)).toHaveText('19:30.00');
+ await expect(page.getByRole('row').nth(3).getByRole('cell').nth(3)).toHaveText('6:02 / mi');
+ await expect(page.getByRole('row').nth(4).getByRole('cell').nth(1)).toHaveText('20:00.00');
+ await expect(page.getByRole('row').nth(4).getByRole('cell').nth(3)).toHaveText('4:01 / mi');
+ await expect(page.getByRole('row')).toHaveCount(5);
+
+ // Create custom target set
+ await page.getByLabel('Selected target set').selectOption('[ Create New Target Set ]');
+ await expect(page.getByRole('row').nth(1)).toHaveText('There aren\'t any targets in this set yet.');
+ await expect(page.getByRole('row')).toHaveCount(2);
+
+ // Edit custom target set
+ await page.getByRole('button', { name: 'Edit target set' }).click();
+ await expect(page.getByLabel('Target set label')).toHaveValue('New target set');
+ await page.getByLabel('Target set label').fill('800m Splits');
+ await page.getByRole('button', { name: 'Add distance target' }).click();
+ await page.getByLabel('Target distance value').nth(0).fill('0.4');
+ await page.getByLabel('Target distance unit').nth(0).selectOption('Kilometers');
+ await page.getByRole('button', { name: 'Add distance target' }).click();
+ await page.getByLabel('Target distance value').nth(1).fill('800');
+ await page.getByLabel('Target distance unit').nth(1).selectOption('Meters');
+ await page.getByRole('button', { name: 'Add time target' }).click();
+ await page.getByRole('button', { name: 'Close' }).click();
+
+ // Assert times and paces are correct (input splits initialized to zero)
+ await expect(page.getByRole('row').nth(1).getByRole('cell').nth(1)).toHaveText('0:00.00');
+ await expect(page.getByRole('row').nth(1).getByRole('cell').nth(3)).toHaveText('0:00 / mi');
+ await expect(page.getByRole('row').nth(2).getByRole('cell').nth(1)).toHaveText('0:00.00');
+ await expect(page.getByRole('row').nth(2).getByRole('cell').nth(3)).toHaveText('0:00 / mi');
+ await expect(page.getByRole('row')).toHaveCount(3);
+
+ // Switch target set
+ await page.getByLabel('Selected target set').selectOption('5K 1600m Splits');
+
+ // Assert times and paces are correct (input splits are not reset)
+ await expect(page.getByRole('row').nth(1).getByRole('cell').nth(1)).toHaveText('7:00.00');
+ await expect(page.getByRole('row').nth(1).getByRole('cell').nth(3)).toHaveText('7:02 / mi');
+ await expect(page.getByRole('row').nth(2).getByRole('cell').nth(1)).toHaveText('13:30.00');
+ await expect(page.getByRole('row').nth(2).getByRole('cell').nth(3)).toHaveText('6:32 / mi');
+ await expect(page.getByRole('row').nth(3).getByRole('cell').nth(1)).toHaveText('19:30.00');
+ await expect(page.getByRole('row').nth(3).getByRole('cell').nth(3)).toHaveText('6:02 / mi');
+ await expect(page.getByRole('row').nth(4).getByRole('cell').nth(1)).toHaveText('20:00.00');
+ await expect(page.getByRole('row').nth(4).getByRole('cell').nth(3)).toHaveText('4:01 / mi');
+ await expect(page.getByRole('row')).toHaveCount(5);
+});
+
+test('Save settings and state across page reloads', async ({ page }) => {
+ // Go to split calculator
+ await page.goto('/');
+ await page.getByRole('button', { name: 'Split Calculator' }).click();
+
+ // Create custom target set
+ await page.getByLabel('Selected target set').selectOption('[ Create New Target Set ]');
+
+ // Edit new target set
+ await page.getByRole('button', { name: 'Edit target set' }).click();
+ await expect(page.getByLabel('Target set label')).toHaveValue('New target set');
+ await page.getByLabel('Target set label').fill('800m Splits');
+ await page.getByRole('button', { name: 'Add distance target' }).click();
+ await page.getByLabel('Target distance value').nth(0).fill('0.4');
+ await page.getByLabel('Target distance unit').nth(0).selectOption('Kilometers');
+ await page.getByRole('button', { name: 'Add distance target' }).click();
+ await page.getByLabel('Target distance value').nth(1).fill('800');
+ await page.getByLabel('Target distance unit').nth(1).selectOption('Meters');
+ await page.getByRole('button', { name: 'Close' }).click();
+
+ // Enter 800m (0:55, 1:05)
+ await page.getByLabel('Split duration minutes').nth(0).fill('0');
+ await page.getByLabel('Split duration seconds').nth(0).fill('55');
+ await page.getByLabel('Split duration minutes').nth(1).fill('1');
+ await page.getByLabel('Split duration seconds').nth(1).fill('5');
+
+ // Change default units
+ await page.getByLabel('Default units').selectOption('Kilometers');
+
+ // Reload page
+ await page.reload();
+
+ // Assert paces are correct (custom targets, split times, and default units not reset)
+ await expect(page.getByRole('row').nth(1).getByRole('cell').nth(1)).toHaveText('0:55.00');
+ await expect(page.getByRole('row').nth(1).getByRole('cell').nth(3)).toHaveText('2:18 / km');
+ await expect(page.getByRole('row').nth(2).getByRole('cell').nth(1)).toHaveText('2:00.00');
+ await expect(page.getByRole('row').nth(2).getByRole('cell').nth(3)).toHaveText('2:43 / km');
+ await expect(page.getByRole('row')).toHaveCount(3);
+});
diff --git a/tests/e2e/unit-calculator.spec.js b/tests/e2e/unit-calculator.spec.js
@@ -0,0 +1,91 @@
+const { test, expect } = require('@playwright/test');
+
+test('Basic usage', async ({ page }) => {
+ // Go to unit calculator
+ await page.goto('/');
+ await page.getByRole('button', { name: 'Unit Calculator' }).click();
+ await expect(page).toHaveTitle('Unit Calculator - Running Tools');
+
+ // Convert distance units (5000m to mi)
+ await page.getByLabel('Input units').selectOption('Meters');
+ await page.getByLabel('Input value').fill('5000');
+ await page.getByLabel('Output units').selectOption('Miles');
+ await expect(page.getByLabel('Output value')).toHaveText('3.107');
+
+ // Convert speed and pace units (0:04:32/km to mph)
+ await page.getByLabel('Selected unit category').selectOption('Speed & Pace');
+ await page.getByLabel('Input units').selectOption('Time per Kilometer');
+ await page.getByLabel('Input time hours').fill('0');
+ await page.getByLabel('Input time minutes').fill('4');
+ await page.getByLabel('Input time seconds').fill('32');
+ await page.getByLabel('Output units').selectOption('Miles per Hour');
+ await expect(page.getByLabel('Output value')).toHaveText('8.224');
+
+ // Convert speed and pace units (10 kph to time per mile)
+ await page.getByLabel('Input units').selectOption('Kilometers per Hour');
+ await page.getByLabel('Input value').fill('10');
+ await page.getByLabel('Output units').selectOption('Time per Mile');
+ await expect(page.getByLabel('Output value')).toHaveText('00:09:39.366');
+
+ // Convert time units (83.76 min to hh:mm:ss)
+ await page.getByLabel('Selected unit category').selectOption('Time');
+ await page.getByLabel('Input units').selectOption('Minutes');
+ await page.getByLabel('Input value').fill('83.76');
+ await page.getByLabel('Output units').selectOption('hh:mm:ss');
+ await expect(page.getByLabel('Output value')).toHaveText('01:23:45.600');
+
+ // Convert time units (6:54:32.100 to seconds)
+ await page.getByLabel('Selected unit category').selectOption('Time');
+ await page.getByLabel('Input units').selectOption('hh:mm:ss');
+ await page.getByLabel('Input time hours').fill('6');
+ await page.getByLabel('Input time minutes').fill('54');
+ await page.getByLabel('Input time seconds').fill('32.1');
+ await page.getByLabel('Output units').selectOption('seconds');
+ await expect(page.getByLabel('Output value')).toHaveText('24872.100');
+
+ // Return to speed and pace category
+ await page.getByLabel('Selected unit category').selectOption('Speed & Pace');
+ await expect(page.getByLabel('Output value')).toHaveText('00:09:39.366');
+});
+
+test('Save state across page reloads', async ({ page }) => {
+ // Go to unit calculator
+ await page.goto('/');
+ await page.getByRole('button', { name: 'Unit Calculator' }).click();
+
+ // Convert distance units (5000m to mi)
+ await page.getByLabel('Input units').selectOption('Meters');
+ await page.getByLabel('Input value').fill('5000');
+ await page.getByLabel('Output units').selectOption('Miles');
+ await expect(page.getByLabel('Output value')).toHaveText('3.107');
+
+ // Convert time units (6:54:32.100 to seconds)
+ await page.getByLabel('Selected unit category').selectOption('Time');
+ await page.getByLabel('Input units').selectOption('hh:mm:ss');
+ await page.getByLabel('Input time hours').fill('6');
+ await page.getByLabel('Input time minutes').fill('54');
+ await page.getByLabel('Input time seconds').fill('32.1');
+ await page.getByLabel('Output units').selectOption('seconds');
+ await expect(page.getByLabel('Output value')).toHaveText('24872.100');
+
+ // Convert speed and pace units (10 kph to time per mile)
+ await page.getByLabel('Selected unit category').selectOption('Speed & Pace');
+ await page.getByLabel('Input units').selectOption('Kilometers per Hour');
+ await page.getByLabel('Input value').fill('10');
+ await page.getByLabel('Output units').selectOption('Time per Mile');
+ await expect(page.getByLabel('Output value')).toHaveText('00:09:39.366');
+
+ // Reload page
+ await page.reload();
+
+ // Assert distance result is correct (state not reset)
+ await expect(page.getByLabel('Output value')).toHaveText('3.107');
+
+ // Assert time result is correct (state not reset)
+ await page.getByLabel('Selected unit category').selectOption('Time');
+ await expect(page.getByLabel('Output value')).toHaveText('24872.100');
+
+ // Assert speed & pace result is correct (state not reset)
+ await page.getByLabel('Selected unit category').selectOption('Speed & Pace');
+ await expect(page.getByLabel('Output value')).toHaveText('00:09:39.366');
+});
diff --git a/vite.config.js b/vite.config.js
@@ -59,5 +59,6 @@ export default defineConfig({
base: process.env.BASE_URL ? process.env.BASE_URL : '/',
test: {
environment: 'jsdom',
+ include: ['tests/unit/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
},
});