diff --git a/.github/workflows/lint-build-test.yml b/.github/workflows/lint-build-test.yml index 56d2a11e6..e9d6c0563 100644 --- a/.github/workflows/lint-build-test.yml +++ b/.github/workflows/lint-build-test.yml @@ -174,8 +174,7 @@ jobs: cache: yarn - run: yarn --immutable - run: yarn build - - run: yarn bundle ./packages/extension/src/vats/sample-vat.js - - run: yarn test:e2e + - run: yarn test:e2e:ci - name: Require clean working directory shell: bash run: | diff --git a/packages/cli/package.json b/packages/cli/package.json index c22494e34..acd20a3ad 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -50,6 +50,7 @@ "@metamask/eslint-config-nodejs": "^14.0.0", "@metamask/eslint-config-typescript": "^14.0.0", "@metamask/utils": "^9.3.0", + "@ocap/test-utils": "workspace:^", "@ts-bridge/cli": "^0.5.1", "@ts-bridge/shims": "^0.1.1", "@types/serve-handler": "^6", diff --git a/packages/cli/src/app.ts b/packages/cli/src/app.ts index 02180bdc2..5c764dc83 100755 --- a/packages/cli/src/app.ts +++ b/packages/cli/src/app.ts @@ -1,9 +1,11 @@ +import path from 'node:path'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import { createBundle } from './commands/bundle.js'; import { getServer } from './commands/serve.js'; import { defaultConfig } from './config.js'; +import type { Config } from './config.js'; await yargs(hideBin(process.argv)) .usage('$0 [options]') @@ -26,33 +28,35 @@ await yargs(hideBin(process.argv)) }, ) .command( - 'serve [-p port]', + 'serve [options]', 'Serve bundled user code by filename', (_yargs) => _yargs - .option('port', { - alias: 'p', - type: 'number', - default: defaultConfig.server.port, - }) .option('dir', { - alias: 'd', type: 'string', dir: true, required: true, - describe: 'A directory of files to bundle', + describe: 'A directory containing bundle files to serve', + }) + .option('port', { + alias: 'p', + type: 'number', + default: defaultConfig.server.port, }), async (args) => { - console.info(`serving ${args.dir} on localhost:${args.port}`); - const server = getServer({ + const appName = 'bundle server'; + const url = `http://localhost:${args.port}`; + const resolvedDir = path.resolve(args.dir); + const config: Config = { server: { port: args.port, }, - dir: args.dir, - }); + dir: resolvedDir, + }; + console.info(`starting ${appName} in ${resolvedDir} on ${url}`); + const server = getServer(config); await server.listen(); }, ) - .help('h') - .alias('h', 'help') + .help('help') .parse(); diff --git a/packages/cli/src/commands/serve.test.ts b/packages/cli/src/commands/serve.test.ts index 7c665d160..b7902bc01 100644 --- a/packages/cli/src/commands/serve.test.ts +++ b/packages/cli/src/commands/serve.test.ts @@ -11,6 +11,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { getServer } from './serve.js'; import { getTestBundles } from '../../test/bundles.js'; import { defaultConfig } from '../config.js'; +import { withTimeout } from '../utils.js'; const isBundleSourceResult = ( value: unknown, @@ -61,8 +62,9 @@ describe('serve', async () => { }, dir: root, }); + const url = `http://localhost:${port}`; const requestBundle = async (path: string): Promise => { - const resp = await nodeFetch(`http://localhost:${port}/${path}`); + const resp = await nodeFetch(`${url}/${path}`); if (resp.ok) { return resp.json(); } @@ -107,10 +109,7 @@ describe('serve', async () => { expect(receivedBundleHash).toStrictEqual(expectedBundleHash); } finally { - await Promise.race([ - new Promise((_resolve) => setTimeout(_resolve, 400)), - close(), - ]); + await withTimeout(close(), 400).catch(console.error); } }); diff --git a/packages/cli/src/utils.test.ts b/packages/cli/src/utils.test.ts new file mode 100644 index 000000000..213d51651 --- /dev/null +++ b/packages/cli/src/utils.test.ts @@ -0,0 +1,17 @@ +import '@ocap/shims/endoify'; +import { delay } from '@ocap/test-utils'; +import { describe, it, expect } from 'vitest'; + +import { withTimeout } from './utils.js'; + +describe('utils', async () => { + describe('withTimeout', () => { + it('times out within the specified duration', async () => { + const duration = 300; + const delta = 100; + const timeout = withTimeout(new Promise(() => undefined), duration); + await delay(duration + delta); + await expect(async () => await timeout).rejects.toThrow(/timed out/u); + }); + }); +}); diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts new file mode 100644 index 000000000..6acbf95bc --- /dev/null +++ b/packages/cli/src/utils.ts @@ -0,0 +1,26 @@ +/** + * Wrap a promise with a timeout rejection. + * + * @param promise - The promise to wrap with a timeout. + * @param timeout - How many ms to wait before rejecting. + * @returns A wrapped promise which rejects after timeout miliseconds. + */ +export async function withTimeout( + promise: Promise, + timeout: number, +): Promise { + return Promise.race([ + promise, + new Promise((_resolve, reject) => + setTimeout( + () => + reject( + new Error(`promise timed out after ${timeout}ms`, { + cause: promise, + }), + ), + timeout, + ), + ), + ]) as Promise; +} diff --git a/packages/cli/test/bundles.ts b/packages/cli/test/bundles.ts index 9cfaced4f..733775ce0 100644 --- a/packages/cli/test/bundles.ts +++ b/packages/cli/test/bundles.ts @@ -7,10 +7,11 @@ import { cp } from '../src/file.js'; const makeTestBundleRoot = async (): Promise => { const testRoot = resolve(import.meta.url.split(':')[1] as string, '..'); + const stageRoot = resolve(tmpdir(), 'test'); // copy bundle targets to staging area const testBundleRoot = resolve(testRoot, 'bundles'); - const stageBundleRoot = resolve(tmpdir(), 'test/bundles'); + const stageBundleRoot = resolve(stageRoot, 'bundles'); await mkdir(stageBundleRoot, { recursive: true }); for (const ext of ['.js', '.expected']) { await Promise.all( @@ -20,10 +21,7 @@ const makeTestBundleRoot = async (): Promise => { }), ); } - await cp( - join(testRoot, 'test.bundle'), - join(stageBundleRoot, '../test.bundle'), - ); + await cp(join(testRoot, 'test.bundle'), join(stageRoot, 'test.bundle')); // return the staging area, ready for testing return stageBundleRoot; diff --git a/packages/extension/package.json b/packages/extension/package.json index ec96d8beb..ea1be7226 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -19,7 +19,6 @@ "build:dev": "yarn build:vite:dev && yarn test:build", "build:vite": "vite build --config vite.config.ts", "build:vite:dev": "yarn build:vite --mode development", - "bundle": "node ../../scripts/bundle-vat.js", "changelog:validate": "../../scripts/validate-changelog.sh @ocap/extension", "clean": "rimraf --glob ./dist './*.tsbuildinfo'", "lint": "yarn lint:ts && yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", @@ -37,10 +36,10 @@ "test:dev": "yarn test --coverage false", "test:verbose": "yarn test --reporter verbose", "test:watch": "vitest --config vitest.config.ts", - "test:e2e": "playwright test", + "test:e2e": "yarn playwright test", + "test:e2e:ci": "./scripts/test-e2e-ci.sh", "test:e2e:ui": "playwright test --ui", - "test:e2e:debug": "playwright test --debug", - "start:server": "npx http-server ./src/vats -p 3000 -a localhost --cors -y" + "test:e2e:debug": "playwright test --debug" }, "dependencies": { "@endo/eventual-send": "^1.2.6", diff --git a/packages/extension/playwright.config.ts b/packages/extension/playwright.config.ts index 7735dd424..52415db8a 100644 --- a/packages/extension/playwright.config.ts +++ b/packages/extension/playwright.config.ts @@ -14,9 +14,4 @@ export default defineConfig({ }, }, ], - webServer: { - command: 'yarn start:server', - url: 'http://localhost:3000', - reuseExistingServer: true, - }, }); diff --git a/packages/extension/scripts/test-e2e-ci.sh b/packages/extension/scripts/test-e2e-ci.sh new file mode 100755 index 000000000..a7d3222f1 --- /dev/null +++ b/packages/extension/scripts/test-e2e-ci.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +set -x +set -e +set -o pipefail + +yarn ocap bundle "./src/vats" + +# Start the server in background and capture its PID +yarn ocap serve "./src/vats" & +SERVER_PID=$! + +function cleanup() { + # Kill the server if it's still running + if kill -0 $SERVER_PID 2>/dev/null; then + kill $SERVER_PID + fi +} +# Ensure we always close the server +trap cleanup EXIT + +yarn test:e2e diff --git a/vitest.config.ts b/vitest.config.ts index 9e825de6f..9e15f370a 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -35,10 +35,10 @@ export default defineConfig({ thresholds: { autoUpdate: true, 'packages/cli/**': { - statements: 75.47, - functions: 72.22, + statements: 71.66, + functions: 76.19, branches: 61.11, - lines: 75.47, + lines: 71.66, }, 'packages/errors/**': { statements: 100, diff --git a/yarn.lock b/yarn.lock index b7f1a878c..92ff3d15b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1613,6 +1613,7 @@ __metadata: "@metamask/snaps-utils": "npm:^8.3.0" "@metamask/utils": "npm:^9.3.0" "@ocap/shims": "workspace:^" + "@ocap/test-utils": "workspace:^" "@ocap/utils": "workspace:^" "@ts-bridge/cli": "npm:^0.5.1" "@ts-bridge/shims": "npm:^0.1.1"