/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as playwright from 'playwright'; import { ChildProcess, spawn } from 'child_process'; import { join } from 'path'; import { mkdir } from 'fs'; import { promisify } from 'util'; import { IDriver, IDisposable } from './driver'; import { URI } from 'vscode-uri'; import * as kill from 'tree-kill'; const width = 1200; const height = 800; const vscodeToPlaywrightKey: { [key: string]: string } = { cmd: 'Meta', ctrl: 'Control', shift: 'Shift', enter: 'Enter', escape: 'Escape', right: 'ArrowRight', up: 'ArrowUp', down: 'ArrowDown', left: 'ArrowLeft', home: 'Home', esc: 'Escape' }; function buildDriver(browser: playwright.Browser, page: playwright.Page): IDriver { const driver: IDriver = { _serviceBrand: undefined, getWindowIds: () => { return Promise.resolve([1]); }, capturePage: () => Promise.resolve(''), reloadWindow: (windowId) => Promise.resolve(), exitApplication: () => browser.close(), dispatchKeybinding: async (windowId, keybinding) => { const chords = keybinding.split(' '); for (let i = 0; i < chords.length; i++) { const chord = chords[i]; if (i > 0) { await timeout(100); } const keys = chord.split('+'); const keysDown: string[] = []; for (let i = 0; i < keys.length; i++) { if (keys[i] in vscodeToPlaywrightKey) { keys[i] = vscodeToPlaywrightKey[keys[i]]; } await page.keyboard.down(keys[i]); keysDown.push(keys[i]); } while (keysDown.length > 0) { await page.keyboard.up(keysDown.pop()!); } } await timeout(100); }, click: async (windowId, selector, xoffset, yoffset) => { const { x, y } = await driver.getElementXY(windowId, selector, xoffset, yoffset); await page.mouse.click(x + (xoffset ? xoffset : 0), y + (yoffset ? yoffset : 0)); }, doubleClick: async (windowId, selector) => { await driver.click(windowId, selector, 0, 0); await timeout(60); await driver.click(windowId, selector, 0, 0); await timeout(100); }, setValue: async (windowId, selector, text) => page.evaluate(`window.driver.setValue('${selector}', '${text}')`).then(undefined), getTitle: (windowId) => page.evaluate(`window.driver.getTitle()`), isActiveElement: (windowId, selector) => page.evaluate(`window.driver.isActiveElement('${selector}')`), getElements: (windowId, selector, recursive) => page.evaluate(`window.driver.getElements('${selector}', ${recursive})`), getElementXY: (windowId, selector, xoffset?, yoffset?) => page.evaluate(`window.driver.getElementXY('${selector}', ${xoffset}, ${yoffset})`), typeInEditor: (windowId, selector, text) => page.evaluate(`window.driver.typeInEditor('${selector}', '${text}')`), getTerminalBuffer: (windowId, selector) => page.evaluate(`window.driver.getTerminalBuffer('${selector}')`), writeInTerminal: (windowId, selector, text) => page.evaluate(`window.driver.writeInTerminal('${selector}', '${text}')`) }; return driver; } function timeout(ms: number): Promise { return new Promise(r => setTimeout(r, ms)); } let server: ChildProcess | undefined; let endpoint: string | undefined; let workspacePath: string | undefined; export async function launch(userDataDir: string, _workspacePath: string, codeServerPath = process.env.VSCODE_REMOTE_SERVER_PATH, extPath: string): Promise { workspacePath = _workspacePath; const agentFolder = userDataDir; await promisify(mkdir)(agentFolder); const env = { VSCODE_AGENT_FOLDER: agentFolder, VSCODE_REMOTE_SERVER_PATH: codeServerPath, ...process.env }; const args = ['--browser', 'none', '--driver', 'web', '--extensions-dir', extPath]; let serverLocation: string | undefined; if (codeServerPath) { serverLocation = join(codeServerPath, `server.${process.platform === 'win32' ? 'cmd' : 'sh'}`); console.log(`Starting built server from '${serverLocation}'`); } else { serverLocation = join(__dirname, '..', '..', '..', `resources/server/web.${process.platform === 'win32' ? 'bat' : 'sh'}`); console.log(`Starting server out of sources from '${serverLocation}'`); } server = spawn( serverLocation, args, { env } ); server.stderr?.on('data', error => console.log(`Server stderr: ${error}`)); server.stdout?.on('data', data => console.log(`Server stdout: ${data}`)); process.on('exit', teardown); process.on('SIGINT', teardown); process.on('SIGTERM', teardown); endpoint = await waitForEndpoint(); } function teardown(): void { if (server) { kill(server.pid); server = undefined; } } function waitForEndpoint(): Promise { return new Promise(r => { server!.stdout?.on('data', (d: Buffer) => { const matches = d.toString('ascii').match(/Web UI available at (.+)/); if (matches !== null) { r(matches[1]); } }); }); } export function connect(browserType: 'chromium' | 'webkit' | 'firefox' = 'chromium'): Promise<{ client: IDisposable, driver: IDriver }> { return new Promise(async (c) => { const browser = await playwright[browserType].launch({ headless: false }); const context = await browser.newContext(); const page = await context.newPage(); await page.setViewportSize({ width, height }); const payloadParam = `[["enableProposedApi",""]]`; await page.goto(`${endpoint}&folder=vscode-remote://localhost:9888${URI.file(workspacePath!).path}&payload=${payloadParam}`); const result = { client: { dispose: () => browser.close() && teardown() }, driver: buildDriver(browser, page) }; c(result); }); }