From 4ab355018b2e38a79f27e2849828f9f64f786469 Mon Sep 17 00:00:00 2001 From: Jason Pickens Date: Mon, 15 Dec 2025 12:57:42 +1300 Subject: [PATCH 01/17] Add test architecture --- docs/TEST_EXECUTION_ARCHITECTURE.md | 839 ++++++++++++++++++++++++++++ 1 file changed, 839 insertions(+) create mode 100644 docs/TEST_EXECUTION_ARCHITECTURE.md diff --git a/docs/TEST_EXECUTION_ARCHITECTURE.md b/docs/TEST_EXECUTION_ARCHITECTURE.md new file mode 100644 index 0000000..703f792 --- /dev/null +++ b/docs/TEST_EXECUTION_ARCHITECTURE.md @@ -0,0 +1,839 @@ +# VSCode Test Web - Test Execution Architecture + +## Overview + +This document provides a comprehensive explanation of how tests are executed in `@vscode/test-web`, covering the entire pipeline from Node.js test runner through Playwright, the browser, web workers, bundled test scripts, and the vscode global API. + +--- + +## Table of Contents + +1. [High-Level Architecture](#high-level-architecture) +2. [Test Execution Flow](#test-execution-flow) +3. [Component Details](#component-details) +4. [Sample Test Walkthrough](#sample-test-walkthrough) +5. [Key Files and Their Roles](#key-files-and-their-roles) +6. [Communication Channels](#communication-channels) + +--- + +## High-Level Architecture + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ Node.js Test Process │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ runTest.ts - Entry point that calls runTests() │ │ +│ │ • Configures test paths │ │ +│ │ • Sets browser type (chromium/firefox/webkit) │ │ +│ │ • Specifies workspace folder │ │ +│ └───────────────────────────────────────────────────────────────────┘ │ +│ ↓ │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ src/server/index.ts - runTests() function │ │ +│ │ • Downloads VSCode build (if needed) │ │ +│ │ • Starts Koa web server on localhost:3000 │ │ +│ │ • Launches Playwright browser │ │ +│ │ • Exposes codeAutomationLog() and codeAutomationExit() │ │ +│ └───────────────────────────────────────────────────────────────────┘ │ +│ ↓ │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ Playwright Browser Control │ │ +│ │ • Opens browser in headless/headed mode │ │ +│ │ • Navigates to http://localhost:3000 │ │ +│ │ • Injects communication bridge functions │ │ +│ └───────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ + ↓ HTTP +┌─────────────────────────────────────────────────────────────────────────┐ +│ Koa Web Server │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ src/server/main.ts + app.ts + workbench.ts │ │ +│ │ • Serves VSCode static files │ │ +│ │ • Serves extension development files │ │ +│ │ • Serves bundled test files │ │ +│ │ • Configures workbench with extensionTestsPath │ │ +│ └───────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ + ↓ HTML/JS +┌─────────────────────────────────────────────────────────────────────────┐ +│ Browser Window (Playwright) │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ VSCode Web Workbench UI │ │ +│ │ src/browser/main.ts │ │ +│ │ • Loads workbench configuration from data-settings attribute │ │ +│ │ • Initializes VSCode UI in browser │ │ +│ │ • Creates WorkspaceProvider │ │ +│ │ • Sets up virtual file system (if folderPath provided) │ │ +│ └───────────────────────────────────────────────────────────────────┘ │ +│ ↓ │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ Extension Host Web Worker │ │ +│ │ • Runs in separate worker thread │ │ +│ │ • Loads extension code (from extensionDevelopmentPath) │ │ +│ │ • Exposes vscode.* API │ │ +│ │ • When extensionTestsPath is set: │ │ +│ │ - Loads bundled test/suite/index.js │ │ +│ │ - Executes test suite in worker context │ │ +│ └───────────────────────────────────────────────────────────────────┘ │ +│ ↓ │ +│ ┌───────────────────────────────────────────────────────────────────┐ │ +│ │ Test Execution (Inside Worker) │ │ +│ │ • test/suite/index.js runs (bundled with webpack) │ │ +│ │ • Mocha test framework initialized (mocha.setup) │ │ +│ │ • Test files loaded via require.context │ │ +│ │ • Tests execute with full vscode.* API access │ │ +│ │ • Results communicated via window.codeAutomationLog() │ │ +│ │ • Test completion via window.codeAutomationExit(code) │ │ +│ └───────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Test Execution Flow + +### Phase 1: Node.js Test Initialization + +**File: `sample/src/web/test/runTest.ts`** + +```typescript +import { runTests } from '../../../..'; // @vscode/test-web + +async function main() { + // 1. Define paths + const extensionDevelopmentPath = path.resolve(__dirname, '../../../'); + const extensionTestsPath = path.resolve(__dirname, './suite/index'); + const folderPath = path.resolve(__dirname, '../../../test-workspace'); + + // 2. Call runTests() - This is the main entry point + await runTests({ + browserType: 'chromium', + extensionDevelopmentPath, // Where the extension lives + extensionTestsPath, // Where the test runner lives + folderPath, // Workspace to open + waitForDebugger: undefined // Optional debugging port + }); +} +``` + +**What happens:** +- The test runner script is invoked (e.g., `node sample/dist/web/test/runTest.js`) +- Paths are resolved to absolute locations +- `runTests()` is called with configuration + +### Phase 2: Server Setup & Browser Launch + +**File: `src/server/index.ts`** + +```typescript +export async function runTests(options: Options): Promise { + // 1. Download/locate VSCode build + const build = await downloadAndUnzipVSCode( + testRunnerDataDir, + quality === 'stable' ? 'stable' : 'insider', + commit + ); + + // 2. Create server configuration + const config: IConfig = { + extensionDevelopmentPath: options.extensionDevelopmentPath, + extensionTestsPath: options.extensionTestsPath, // CRITICAL: tells VSCode to run tests + build: build, + folderUri: options.folderUri, + folderMountPath: options.folderPath, + printServerLog: options.printServerLog, + extensionPaths: options.extensionPaths, + extensionIds: options.extensionIds, + coi: !!options.coi, + esm: !!options.esm, + }; + + // 3. Start web server + const host = options.host ?? 'localhost'; + const port = options.port ?? 3000; + const server = await runServer(host, port, config); + + // 4. Set up communication bridge functions + const configPage = async (page: playwright.Page, browser: playwright.Browser) => { + // Expose function for logging from worker to Node.js + await page.exposeFunction('codeAutomationLog', (type: Severity, args: unknown[]) => { + console[type](...args); + }); + + // Expose function for test completion from worker to Node.js + await page.exposeFunction('codeAutomationExit', async (code: number) => { + await browser.close(); + server.close(); + if (code === 0) { + resolve(); // Tests passed + } else { + reject(new Error('Test failed')); // Tests failed + } + }); + }; + + // 5. Launch browser with Playwright + const endpoint = `http://${host}:${port}`; + const context = await openBrowser(endpoint, options, configPage); +} +``` + +**Key Points:** +- The `extensionTestsPath` in config tells VSCode to run tests automatically +- `codeAutomationLog()` and `codeAutomationExit()` are exposed via `page.exposeFunction()` +- These functions create a bridge between the browser worker and Node.js + +### Phase 3: Workbench Configuration + +**File: `src/server/workbench.ts`** + +```typescript +async function getWorkbenchOptions( + ctx: { protocol: string; host: string }, + config: IConfig +): Promise { + const options: IWorkbenchOptions = {}; + + // Configure extension development + if (config.extensionDevelopmentPath) { + const developmentOptions: IDevelopmentOptions = (options.developmentOptions = {}); + + developmentOptions.extensions = await scanForExtensions( + config.extensionDevelopmentPath, + { scheme: ctx.protocol, authority: ctx.host, path: '/static/devextensions' } + ); + + // Configure test path - This is how VSCode knows to run tests! + if (config.extensionTestsPath) { + let relativePath = path.relative( + config.extensionDevelopmentPath, + config.extensionTestsPath + ); + developmentOptions.extensionTestsPath = { + scheme: ctx.protocol, + authority: ctx.host, + path: path.posix.join('/static/devextensions', relativePath), + }; + } + } + + // Configure workspace + if (config.folderMountPath) { + options.folderUri = URI.parse(fsProviderFolderUri); + // Enable file system provider extension + options.additionalBuiltinExtensions.push({ + scheme: ctx.protocol, + authority: ctx.host, + path: fsProviderExtensionPrefix + }); + } + + return options; +} +``` + +**Key Points:** +- The `extensionTestsPath` is converted to a URI accessible via the web server +- VSCode workbench receives this configuration via `data-settings` attribute in HTML +- When VSCode sees `extensionTestsPath`, it automatically loads and runs tests + +### Phase 4: Browser Initialization + +**File: `src/browser/main.ts`** + +```typescript +(function () { + // 1. Extract configuration from HTML element + const configElement = window.document.getElementById('vscode-workbench-web-configuration'); + const configElementAttribute = configElement.getAttribute('data-settings'); + const config: IWorkbenchConstructionOptions = JSON.parse(configElementAttribute); + + // 2. Create workbench + create(window.document.body, { + ...config, + workspaceProvider: WorkspaceProvider.create(config), + urlCallbackProvider: new LocalStorageURLCallbackProvider(config.callbackRoute) + }); +})(); +``` + +**What happens:** +- Browser loads the main HTML page from the server +- The HTML contains a `