diff --git a/.github/workflows/web-host.yml b/.github/workflows/web-host.yml index 80e88a8..f210f37 100644 --- a/.github/workflows/web-host.yml +++ b/.github/workflows/web-host.yml @@ -22,6 +22,17 @@ jobs: run: npm ci - name: Build run: npm run web-host:build + - name: Install Playwright + run: npx playwright install --with-deps + working-directory: ./packages/web-host + - name: e2e tests (playwright) + run: WAIT_FOR_SERVER_AT_URL=http://localhost:4173/webassembly-component-model-experiments/ npm run test:e2e:all:preview + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: ./packages/web-host/playwright-report/ + retention-days: 30 - name: Cache build artifacts id: cache-build-www-host uses: actions/cache@v4 diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 7a435bc..92c6c23 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -4,6 +4,7 @@ "bytecodealliance.wit-idl", "skellock.just", "biomejs.biome", - "bradlc.vscode-tailwindcss" + "bradlc.vscode-tailwindcss", + "ms-playwright.playwright" ] } diff --git a/README.md b/README.md index 9e1dc2c..8b35c6f 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,10 @@ rustup target add wasm32-unknown-unknown wasm32-wasip1 ``` ```bash +# Install project dependencies (web part) npm install +# Install Playwright browsers (e2e tests for web-host) +npx playwright install ``` ### pluginlab (rust) @@ -277,6 +280,27 @@ Will do the same as the dev command, small changes: You can then run `npm run web-host:preview` to preview the build. +#### Test + +The project is configured to run e2e tests on the `web-host` using [playwright](./packages/web-host/playwright.config.ts), the test files are in [`packages/web-host/tests`](./packages/web-host/tests). + +To run the tests against your local dev server (launched with `npm run dev`) + +- `npm run test:e2e:all`: will run all the tests in headless mode +- `npm run test:e2e:ui`: will open the playwright ui to run the tests + +To run the tests against a preview server (build with `npm run build` and launched with `npm run preview`): + +- `npm run test:e2e:all:preview`: will run all the tests in headless mode +- `npm run test:e2e:ui:preview`: will open the playwright ui to run the tests + +Specific to github actions: + +In [`.github/workflows/web-host.yml`](./.github/workflows/web-host.yml), after the build step, the tests are run against the preview server. + +To be sure that the preview server is up and running before running the tests, we use the [`webServer.command` option](https://playwright.dev/docs/test-webserver) of [playwright.config.ts](./packages/web-host/playwright.config.ts) to run `WAIT_FOR_SERVER_AT_URL=http://localhost:4173/webassembly-component-model-experiments/ npm run test:e2e:all:preview` + + ### plugins (TypeScript) You can write plugins in rust in [`crates/plugin-*`](./crates), you can also write plugins in TypeScript in [`packages/plugin-*`](./packages), thanks to `jco componentize` (based on [componentize-js](https://github.com/bytecodealliance/componentize-js)). diff --git a/package-lock.json b/package-lock.json index 9e6bf94..c46c0d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1823,6 +1823,22 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@playwright/test": { + "version": "1.54.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.1.tgz", + "integrity": "sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.54.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rolldown/binding-darwin-arm64": { "version": "1.0.0-beta.19", "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.19.tgz", @@ -5515,6 +5531,53 @@ "node": ">=0.10.0" } }, + "node_modules/playwright": { + "version": "1.54.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.1.tgz", + "integrity": "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.54.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.54.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.1.tgz", + "integrity": "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "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/plugin-echo": { "resolved": "packages/plugin-echo", "link": true @@ -6710,6 +6773,7 @@ }, "devDependencies": { "@bytecodealliance/jco": "^1.11.2", + "@playwright/test": "^1.54.1", "@tailwindcss/vite": "^4.1.11", "@types/node": "^24.0.4", "@types/react": "^19.1.8", diff --git a/package.json b/package.json index f164950..dcd98c9 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,11 @@ "lint": "biome check .", "lint:fix": "biome check --write .", "format": "biome format --write .", + "test:e2e:all": "npm run test:e2e:all --workspace=packages/web-host", + "test:e2e:ui": "npm run test:e2e:ui --workspace=packages/web-host", + "test:e2e:all:preview": "npm run test:e2e:all:preview --workspace=packages/web-host", + "test:e2e:ui:preview": "npm run test:e2e:ui:preview --workspace=packages/web-host", + "test:e2e:report": "npm run test:e2e:report --workspace=packages/web-host", "typecheck": "npm run typecheck --workspace=*", "web-host:typecheck": "npm run typecheck --workspace=packages/web-host", "web-host:build": "npm run build --workspace=packages/web-host", diff --git a/packages/web-host/.gitignore b/packages/web-host/.gitignore index a149c6c..9cef249 100644 --- a/packages/web-host/.gitignore +++ b/packages/web-host/.gitignore @@ -26,3 +26,9 @@ dist-ssr # Project specific public/plugins/*.wasm src/wasm/generated + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/packages/web-host/package.json b/packages/web-host/package.json index b39f9f9..c432f34 100644 --- a/packages/web-host/package.json +++ b/packages/web-host/package.json @@ -1,56 +1,62 @@ { - "name": "web-host", - "private": true, - "version": "0.0.0", - "type": "module", - "description": "Web host for Terminal REPL with plugin system (using WebAssembly Component Model)", - "scripts": { - "predev": "npm run prebuild", - "predev:debug": "npm run wit-types && just build-plugins && just build-repl-logic-guest && npm run prepareWasmFiles:debug && npm run wasm:transpile", - "dev": "vite --host", - "dev:debug": "vite --host", - "prebuild": "npm run wit-types && just build-plugins-release && just build-repl-logic-guest-release && npm run prepareWasmFiles:release && npm run wasm:transpile && npm run prepareVirtualFs", - "build": "tsc -b && vite build", - "preview": "vite preview --host", - "lint": "biome check .", - "lint:fix": "biome check --write .", - "typecheck": "tsc --noEmit -p tsconfig.app.json", - "prepareWasmFiles:release": "node --experimental-strip-types --no-warnings ./clis/prepareWasmFiles.ts --mode release", - "prepareWasmFiles:debug": "node --experimental-strip-types --no-warnings ./clis/prepareWasmFiles.ts --mode debug", - "prepareVirtualFs": "node --experimental-strip-types --no-warnings ./clis/prepareFilesystem.ts --path fixtures/filesystem --format ts > src/wasm/virtualFs.ts; biome format --write ./src/wasm/virtualFs.ts", - "wasm:transpile:plugin-echo": "jco transpile --no-nodejs-compat --no-namespaced-exports public/plugins/plugin_echo.wasm -o ./src/wasm/generated/plugin_echo/transpiled", - "wasm:transpile:plugin-weather": "jco transpile --no-nodejs-compat --no-namespaced-exports public/plugins/plugin_weather.wasm -o ./src/wasm/generated/plugin_weather/transpiled", - "wasm:transpile:plugin-greet": "jco transpile --no-nodejs-compat --no-namespaced-exports public/plugins/plugin_greet.wasm -o ./src/wasm/generated/plugin_greet/transpiled", - "wasm:transpile:plugin-ls": "jco transpile --no-nodejs-compat --no-namespaced-exports public/plugins/plugin_ls.wasm -o ./src/wasm/generated/plugin_ls/transpiled", - "wasm:transpile:plugin-cat": "jco transpile --no-nodejs-compat --no-namespaced-exports public/plugins/plugin_cat.wasm -o ./src/wasm/generated/plugin_cat/transpiled", - "wasm:transpile:repl-logic-guest": "jco transpile --no-nodejs-compat --no-namespaced-exports public/plugins/repl_logic_guest.wasm -o ./src/wasm/generated/repl_logic_guest/transpiled", - "wasm:transpile": "npm run wasm:transpile:plugin-echo && npm run wasm:transpile:plugin-weather && npm run wasm:transpile:plugin-greet && npm run wasm:transpile:plugin-ls && npm run wasm:transpile:plugin-cat && npm run wasm:transpile:repl-logic-guest", - "wit-types:host-api": "jco types --world-name host-api --out-dir ./src/types/generated ../../crates/pluginlab/wit", - "wit-types:plugin-api": "jco types --world-name plugin-api --out-dir ./src/types/generated ../../crates/pluginlab/wit", - "wit-types": "npm run wit-types:clean && npm run wit-types:host-api && npm run wit-types:plugin-api && biome format --write ./src/types/generated", - "wit-types:clean": "rm -rf ./src/types/generated" - }, - "dependencies": { - "clsx": "^2.1.1", - "lucide-react": "^0.525.0", - "qrcode.react": "^4.2.0", - "react": "^19.1.0", - "react-dom": "^19.1.0", - "tailwind-merge": "^3.3.1", - "zustand": "^5.0.6" - }, - "devDependencies": { - "@bytecodealliance/jco": "^1.11.2", - "@tailwindcss/vite": "^4.1.11", - "@types/node": "^24.0.4", - "@types/react": "^19.1.8", - "@types/react-dom": "^19.1.6", - "@vitejs/plugin-react": "^4.5.2", - "commander": "^12.1.0", - "globals": "^16.2.0", - "tailwindcss": "^4.1.11", - "typescript": "~5.8.3", - "typescript-eslint": "^8.34.1", - "vite": "^7.0.0" - } + "name": "web-host", + "private": true, + "version": "0.0.0", + "type": "module", + "description": "Web host for Terminal REPL with plugin system (using WebAssembly Component Model)", + "scripts": { + "predev": "npm run prebuild", + "predev:debug": "npm run wit-types && just build-plugins && just build-repl-logic-guest && npm run prepareWasmFiles:debug && npm run wasm:transpile", + "dev": "vite --host", + "dev:debug": "vite --host", + "prebuild": "npm run wit-types && just build-plugins-release && just build-repl-logic-guest-release && npm run prepareWasmFiles:release && npm run wasm:transpile && npm run prepareVirtualFs", + "build": "tsc -b && vite build", + "preview": "vite preview --host", + "lint": "biome check .", + "lint:fix": "biome check --write .", + "typecheck": "tsc --noEmit -p tsconfig.app.json", + "prepareWasmFiles:release": "node --experimental-strip-types --no-warnings ./clis/prepareWasmFiles.ts --mode release", + "prepareWasmFiles:debug": "node --experimental-strip-types --no-warnings ./clis/prepareWasmFiles.ts --mode debug", + "prepareVirtualFs": "node --experimental-strip-types --no-warnings ./clis/prepareFilesystem.ts --path fixtures/filesystem --format ts > src/wasm/virtualFs.ts; biome format --write ./src/wasm/virtualFs.ts", + "test:e2e:all": "playwright test", + "test:e2e:ui": "playwright test --ui", + "test:e2e:all:preview": "BASE_URL=http://localhost:4173/webassembly-component-model-experiments npm run test:e2e:all", + "test:e2e:ui:preview": "BASE_URL=http://localhost:4173/webassembly-component-model-experiments npm run test:e2e:ui", + "test:e2e:report": "playwright show-report", + "wasm:transpile:plugin-echo": "jco transpile --no-nodejs-compat --no-namespaced-exports public/plugins/plugin_echo.wasm -o ./src/wasm/generated/plugin_echo/transpiled", + "wasm:transpile:plugin-weather": "jco transpile --no-nodejs-compat --no-namespaced-exports public/plugins/plugin_weather.wasm -o ./src/wasm/generated/plugin_weather/transpiled", + "wasm:transpile:plugin-greet": "jco transpile --no-nodejs-compat --no-namespaced-exports public/plugins/plugin_greet.wasm -o ./src/wasm/generated/plugin_greet/transpiled", + "wasm:transpile:plugin-ls": "jco transpile --no-nodejs-compat --no-namespaced-exports public/plugins/plugin_ls.wasm -o ./src/wasm/generated/plugin_ls/transpiled", + "wasm:transpile:plugin-cat": "jco transpile --no-nodejs-compat --no-namespaced-exports public/plugins/plugin_cat.wasm -o ./src/wasm/generated/plugin_cat/transpiled", + "wasm:transpile:repl-logic-guest": "jco transpile --no-nodejs-compat --no-namespaced-exports public/plugins/repl_logic_guest.wasm -o ./src/wasm/generated/repl_logic_guest/transpiled", + "wasm:transpile": "npm run wasm:transpile:plugin-echo && npm run wasm:transpile:plugin-weather && npm run wasm:transpile:plugin-greet && npm run wasm:transpile:plugin-ls && npm run wasm:transpile:plugin-cat && npm run wasm:transpile:repl-logic-guest", + "wit-types:host-api": "jco types --world-name host-api --out-dir ./src/types/generated ../../crates/pluginlab/wit", + "wit-types:plugin-api": "jco types --world-name plugin-api --out-dir ./src/types/generated ../../crates/pluginlab/wit", + "wit-types": "npm run wit-types:clean && npm run wit-types:host-api && npm run wit-types:plugin-api && biome format --write ./src/types/generated", + "wit-types:clean": "rm -rf ./src/types/generated" + }, + "dependencies": { + "clsx": "^2.1.1", + "lucide-react": "^0.525.0", + "qrcode.react": "^4.2.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "tailwind-merge": "^3.3.1", + "zustand": "^5.0.6" + }, + "devDependencies": { + "@bytecodealliance/jco": "^1.11.2", + "@playwright/test": "^1.54.1", + "@tailwindcss/vite": "^4.1.11", + "@types/node": "^24.0.4", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "@vitejs/plugin-react": "^4.5.2", + "commander": "^12.1.0", + "globals": "^16.2.0", + "tailwindcss": "^4.1.11", + "typescript": "~5.8.3", + "typescript-eslint": "^8.34.1", + "vite": "^7.0.0" + } } diff --git a/packages/web-host/playwright.config.ts b/packages/web-host/playwright.config.ts new file mode 100644 index 0000000..9f6fec6 --- /dev/null +++ b/packages/web-host/playwright.config.ts @@ -0,0 +1,94 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./tests", + /* 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: + process.env.BASE_URL || + "http://localhost:5173/webassembly-component-model-experiments/", + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + }, + ...(process.env.WAIT_FOR_SERVER_AT_URL + ? { + webServer: { + command: "npm run preview -- --strictPort", + url: process.env.WAIT_FOR_SERVER_AT_URL, + reuseExistingServer: !process.env.CI, + }, + } + : {}), + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + { + name: "Mobile Chrome", + use: { ...devices["Pixel 5"] }, + }, + + { + name: "firefox", + use: { ...devices["Desktop Firefox"] }, + }, + + { + name: "webkit", + use: { ...devices["Desktop Safari"] }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://localhost:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/packages/web-host/src/components/Repl.tsx b/packages/web-host/src/components/Repl.tsx index 4ae287d..58a9dd6 100644 --- a/packages/web-host/src/components/Repl.tsx +++ b/packages/web-host/src/components/Repl.tsx @@ -92,6 +92,7 @@ export function Repl({ className="border border-gray-300 rounded-md p-2 w-full pr-10" onFocus={() => setInputFocus(true)} onBlur={() => setInputFocus(false)} + placeholder="Type a command..." />