diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 827e210..81bf48b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,9 +21,15 @@ jobs: - name: Install Dependencies run: npm ci + - name: Install Browser + run: npx playwright install --with-deps chromium --only-shell + - name: Run Tests run: npm test + - name: Run Browser Tests + run: npm run test:browser + types: runs-on: ubuntu-latest diff --git a/package-lock.json b/package-lock.json index 6da875f..70c8f49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,11 +37,13 @@ "@types/semver": "^7.7.1", "@types/spdx-license-ids": "^3.0.0", "@types/swagger-ui-react": "^5.18.0", + "@vitest/browser-playwright": "^4.1.2", "eslint": "^10.6.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-jsdoc": "^63.0.10", "husky": "^9.1.7", "lint-staged": "^17.0.8", + "playwright": "^1.62.0", "prettier": "^3.9.1", "typescript": "^6.0.3", "typescript-eslint": "^8.62.0", @@ -405,6 +407,13 @@ "node": ">=6.9.0" } }, + "node_modules/@blazediff/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@blazediff/core/-/core-1.9.1.tgz", + "integrity": "sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==", + "dev": true, + "license": "MIT" + }, "node_modules/@cloudflare/kv-asset-handler": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz", @@ -1897,6 +1906,13 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, "node_modules/@poppinss/colors": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", @@ -3388,6 +3404,53 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@vitest/browser": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.2.tgz", + "integrity": "sha512-CwdIf90LNf1Zitgqy63ciMAzmyb4oIGs8WZ40VGYrWkssQKeEKr32EzO8MKUrDPPcPVHFI9oQ5ni2Hp24NaNRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@blazediff/core": "1.9.1", + "@vitest/mocker": "4.1.2", + "@vitest/utils": "4.1.2", + "magic-string": "^0.30.21", + "pngjs": "^7.0.0", + "sirv": "^3.0.2", + "tinyrainbow": "^3.1.0", + "ws": "^8.19.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.1.2" + } + }, + "node_modules/@vitest/browser-playwright": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.2.tgz", + "integrity": "sha512-N0Z2HzMLvMR6k/tWPTS6Q/DaRscrkax/f2f9DIbNQr+Cd1l4W4wTf/I6S983PAMr0tNqqoTL+xNkLh9M5vbkLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/browser": "4.1.2", + "@vitest/mocker": "4.1.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "playwright": "*", + "vitest": "4.1.2" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": false + } + } + }, "node_modules/@vitest/expect": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", @@ -5853,6 +5916,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -6177,6 +6250,63 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.0.tgz", + "integrity": "sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.0.tgz", + "integrity": "sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -6806,6 +6936,21 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/slice-ansi": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", @@ -7158,6 +7303,16 @@ "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/tree-sitter": { "version": "0.21.1", "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.21.1.tgz", @@ -8000,6 +8155,12 @@ "@babel/helper-validator-identifier": "^7.28.5" } }, + "@blazediff/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@blazediff/core/-/core-1.9.1.tgz", + "integrity": "sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==", + "dev": true + }, "@cloudflare/kv-asset-handler": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz", @@ -8810,6 +8971,12 @@ "integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==", "dev": true }, + "@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true + }, "@poppinss/colors": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", @@ -9951,6 +10118,33 @@ } } }, + "@vitest/browser": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.2.tgz", + "integrity": "sha512-CwdIf90LNf1Zitgqy63ciMAzmyb4oIGs8WZ40VGYrWkssQKeEKr32EzO8MKUrDPPcPVHFI9oQ5ni2Hp24NaNRQ==", + "dev": true, + "requires": { + "@blazediff/core": "1.9.1", + "@vitest/mocker": "4.1.2", + "@vitest/utils": "4.1.2", + "magic-string": "^0.30.21", + "pngjs": "^7.0.0", + "sirv": "^3.0.2", + "tinyrainbow": "^3.1.0", + "ws": "^8.19.0" + } + }, + "@vitest/browser-playwright": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.2.tgz", + "integrity": "sha512-N0Z2HzMLvMR6k/tWPTS6Q/DaRscrkax/f2f9DIbNQr+Cd1l4W4wTf/I6S983PAMr0tNqqoTL+xNkLh9M5vbkLg==", + "dev": true, + "requires": { + "@vitest/browser": "4.1.2", + "@vitest/mocker": "4.1.2", + "tinyrainbow": "^3.1.0" + } + }, "@vitest/expect": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", @@ -11484,6 +11678,12 @@ "brace-expansion": "^2.0.2" } }, + "mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true + }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -11703,6 +11903,37 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true }, + "playwright": { + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.0.tgz", + "integrity": "sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==", + "dev": true, + "requires": { + "fsevents": "2.3.2", + "playwright-core": "1.62.0" + }, + "dependencies": { + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + } + } + }, + "playwright-core": { + "version": "1.62.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.0.tgz", + "integrity": "sha512-nsNRyq0r2zsG8AcRHWknc9QRA5XCueC7gWMrs+Gx2tlZn9hcl8zudfh00lhJPY1DE7NmZ6bDsT9g2yey8mXljA==", + "dev": true + }, + "pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true + }, "possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -12096,6 +12327,17 @@ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true }, + "sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "requires": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + } + }, "slice-ansi": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", @@ -12358,6 +12600,12 @@ "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" }, + "totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true + }, "tree-sitter": { "version": "0.21.1", "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.21.1.tgz", diff --git a/package.json b/package.json index e69d82b..412f5a4 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "format": "prettier --check .", "format:fix": "prettier --write .", "test": "vitest run", + "test:browser": "vitest run --config vitest.browser.config.ts", "types": "tsc --noEmit", "dev": "wrangler dev" }, @@ -59,11 +60,13 @@ "@types/semver": "^7.7.1", "@types/spdx-license-ids": "^3.0.0", "@types/swagger-ui-react": "^5.18.0", + "@vitest/browser-playwright": "^4.1.2", "eslint": "^10.6.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-jsdoc": "^63.0.10", "husky": "^9.1.7", "lint-staged": "^17.0.8", + "playwright": "^1.62.0", "prettier": "^3.9.1", "typescript": "^6.0.3", "typescript-eslint": "^8.62.0", diff --git a/src/routes/human.browser.spec.ts b/src/routes/human.browser.spec.ts new file mode 100644 index 0000000..ab5ddae --- /dev/null +++ b/src/routes/human.browser.spec.ts @@ -0,0 +1,70 @@ +import { afterEach, describe, expect, it } from 'vitest'; +import { page } from 'vitest/browser'; + +const openHumanRoute = (path: string) => { + const title = `Human output: ${path}`; + const frame = document.createElement('iframe'); + frame.title = title; + frame.src = `/__worker${path}`; + document.body.appendChild(frame); + + return page.frameLocator(page.getByTitle(title)); +}; + +afterEach(() => { + document.body.replaceChildren(); +}); + +describe('human-readable routes', () => { + it('renders the API documentation with accessible navigation', async () => { + const frame = openHumanRoute('/api?output=human'); + + await expect.element(frame.getByRole('navigation')).toBeInTheDocument(); + await expect + .element(frame.getByRole('link', { name: 'cdnjs' }).first()) + .toBeInTheDocument(); + await expect + .element(frame.getByText('Loading OpenAPI specification...')) + .not.toBeInTheDocument(); + await expect + .element(frame.getByRole('heading', { name: 'Libraries' })) + .toBeInTheDocument(); + }); + + it('renders an immutable library version and hydrates its file filter', async () => { + const frame = openHumanRoute( + '/libraries/backbone.js/1.1.0?output=human', + ); + + await expect + .element( + frame.getByRole('heading', { + name: 'backbone.js', + exact: true, + }), + ) + .toBeInTheDocument(); + + const version = frame.getByRole('combobox', { name: 'Version:' }); + const filter = frame.getByRole('combobox', { name: 'Filter:' }); + await expect.element(version).toHaveValue('1.1.0'); + await expect.element(filter).toHaveDisplayValue('All assets'); + + const script = frame.getByRole('link', { + name: 'backbone-min.js', + exact: true, + }); + const sourceMap = frame.getByRole('link', { + name: 'backbone-min.map', + exact: true, + }); + await expect.element(script).toBeInTheDocument(); + await expect.element(sourceMap).toBeInTheDocument(); + + await filter.selectOptions('Source Maps'); + + await expect.element(filter).toHaveDisplayValue('Source Maps'); + await expect.element(sourceMap).toBeInTheDocument(); + await expect.element(script).not.toBeInTheDocument(); + }); +}); diff --git a/vite.client.config.ts b/vite.client.config.ts index d0a6c35..a98243f 100644 --- a/vite.client.config.ts +++ b/vite.client.config.ts @@ -1,6 +1,6 @@ import { existsSync, globSync, mkdirSync, rmSync } from 'node:fs'; import { basename, extname, resolve } from 'node:path'; -import { defineConfig } from 'vite'; +import { defineConfig, normalizePath } from 'vite'; const outputDirectory = resolve('dist-client'); const virtualEntryPrefix = 'virtual:island-entry:'; @@ -29,7 +29,7 @@ const islandEntryByName = new Map( islandEntries.map((entry) => [entry.name, entry]), ); const islandEntryByPath = new Map( - islandEntries.map((entry) => [entry.path, entry]), + islandEntries.map((entry) => [normalizePath(entry.path), entry]), ); const parseCreateIslandDeclaration = (source: string) => { @@ -77,7 +77,7 @@ export default defineConfig({ }, // Strip the SSR wrapper from island modules in the client build. transform(code, id) { - const entry = islandEntryByPath.get(id); + const entry = islandEntryByPath.get(normalizePath(id)); if (!entry) { return null; } @@ -153,13 +153,12 @@ export default defineConfig({ assetFileNames: 'islands/assets/[name]-[hash][extname]', // Share the core React hydration code across all islands as a separate chunk. manualChunks(id) { - const normalizedId = id.replaceAll('\\', '/'); + const normalizedId = normalizePath(id); if ( normalizedId.includes('/node_modules/react/') || normalizedId.includes('/node_modules/react-dom/') || - normalizedId === - hydrationRuntimePath.replaceAll('\\', '/') + normalizedId === normalizePath(hydrationRuntimePath) ) { return 'hydration-runtime'; } diff --git a/vitest.browser.config.ts b/vitest.browser.config.ts new file mode 100644 index 0000000..a2d3cb5 --- /dev/null +++ b/vitest.browser.config.ts @@ -0,0 +1,119 @@ +import { playwright } from '@vitest/browser-playwright'; +import { createServer } from 'node:net'; +import { defineConfig } from 'vitest/config'; + +const host = '127.0.0.1'; + +const findAvailablePorts = async (count: number) => { + const ports = new Set(); + + while (ports.size < count) { + const port = await new Promise((resolve, reject) => { + const server = createServer(); + server.unref(); + server.once('error', reject); + server.listen(0, host, () => { + const address = server.address(); + if (!address || typeof address === 'string') { + server.close(); + reject( + new Error('Unable to allocate a browser test port.'), + ); + return; + } + + server.close((error) => { + if (error) { + reject(error); + return; + } + + resolve(address.port); + }); + }); + }); + + ports.add(port); + } + + return [...ports]; +}; + +const readConfiguredPorts = (keys: string[]) => { + const ports = keys.map((key) => { + const port = Number(process.env[key]); + return Number.isInteger(port) && port > 0 && port <= 65_535 + ? port + : undefined; + }); + + return ports.every((port): port is number => port !== undefined) + ? ports + : null; +}; + +const externalApiUrl = process.env.VITEST_EXTERNAL_API_URL?.replace(/\/+$/, ''); +const portEnvironmentKeys = externalApiUrl + ? ['VITEST_BROWSER_API_PORT'] + : [ + 'VITEST_BROWSER_API_PORT', + 'VITEST_BROWSER_WORKER_PORT', + 'VITEST_BROWSER_INSPECTOR_PORT', + ]; +const ports = + readConfiguredPorts(portEnvironmentKeys) ?? + (await findAvailablePorts(portEnvironmentKeys.length)); +portEnvironmentKeys.forEach((key, index) => { + process.env[key] = String(ports[index]); +}); + +const [browserPort, workerPort, inspectorPort] = ports; + +if (!browserPort) { + throw new Error('Unable to allocate the browser server port.'); +} + +if (!externalApiUrl && (!workerPort || !inspectorPort)) { + throw new Error('Unable to allocate the local Worker ports.'); +} + +const workerTarget = externalApiUrl ?? `http://${host}:${String(workerPort)}`; + +export default defineConfig({ + server: { + host, + proxy: { + '/__worker': { + target: workerTarget, + changeOrigin: true, + rewrite: (path: string) => path.replace(/^\/__worker/, ''), + }, + '/islands': { + target: workerTarget, + changeOrigin: true, + }, + }, + }, + test: { + expect: { + poll: { + timeout: 10_000, + }, + }, + include: ['src/**/*.browser.spec.ts'], + globalSetup: './vitest.browser.setup.ts', + testTimeout: 30_000, + browser: { + api: { + host, + port: browserPort, + strictPort: true, + }, + enabled: true, + headless: true, + screenshotFailures: false, + provider: playwright({ actionTimeout: 10_000 }), + instances: [{ browser: 'chromium' }], + }, + }, +}); diff --git a/vitest.browser.setup.ts b/vitest.browser.setup.ts new file mode 100644 index 0000000..5498c7d --- /dev/null +++ b/vitest.browser.setup.ts @@ -0,0 +1,40 @@ +import { unstable_dev } from 'wrangler'; + +const host = '127.0.0.1'; + +/** + * Start a local Worker for the browser test project. + */ +export default async function setup() { + if (process.env.VITEST_EXTERNAL_API_URL) return; + + const workerPort = Number(process.env.VITEST_BROWSER_WORKER_PORT); + const inspectorPort = Number(process.env.VITEST_BROWSER_INSPECTOR_PORT); + if (!workerPort || !inspectorPort) { + throw new Error('Browser test Worker ports were not configured.'); + } + + const worker = await unstable_dev('src/index.ts', { + config: './wrangler.toml', + ip: host, + port: workerPort, + inspectorPort, + local: true, + logLevel: 'error', + experimental: { + disableExperimentalWarning: true, + showInteractiveDevSession: false, + watch: false, + }, + }); + + const response = await worker.fetch('/api'); + if (!response.ok) { + await worker.stop(); + throw new Error( + `Local Worker health check failed with status ${String(response.status)}.`, + ); + } + + return () => worker.stop(); +} diff --git a/vitest.config.ts b/vitest.config.ts index baef361..78a3fb6 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,8 +1,9 @@ import { cloudflareTest } from '@cloudflare/vitest-pool-workers'; -import { defineConfig } from 'vitest/config'; +import { configDefaults, defineConfig } from 'vitest/config'; export default defineConfig({ test: { + exclude: [...configDefaults.exclude, '**/*.browser.spec.ts'], globalSetup: './vitest.setup.ts', silent: 'passed-only', reporters: [