Puppeteer exploration

This commit is contained in:
Daniel Imms 2019-06-12 14:37:12 -07:00
parent 94b23bc6c4
commit c41ad18414
9 changed files with 359 additions and 56 deletions

View file

@ -45,6 +45,7 @@
"native-watchdog": "1.0.0",
"node-pty": "0.9.0-beta17",
"onigasm-umd": "^2.2.2",
"puppeteer": "^1.17.0",
"semver": "^5.5.0",
"spdlog": "^0.9.0",
"sudo-prompt": "8.2.0",

View file

@ -6,8 +6,8 @@
"postinstall": "npm run compile",
"compile": "npm run copy-driver && npm run copy-driver-definition && tsc",
"watch": "concurrently \"npm run watch-driver\" \"npm run watch-driver-definition\" \"tsc --watch\"",
"copy-driver": "cpx src/vscode/driver.js out/vscode",
"watch-driver": "cpx src/vscode/driver.js out/vscode -w",
"copy-driver": "cpx src/vscode/puppeteer-driver.js out/vscode",
"watch-driver": "cpx src/vscode/puppeteer-driver.js out/vscode -w",
"copy-driver-definition": "node tools/copy-driver-definition.js",
"watch-driver-definition": "watch \"node tools/copy-driver-definition.js\" ../../src/vs/platform/driver/node",
"mocha": "mocha"

View file

@ -137,11 +137,11 @@ export class Application {
}
await this.code.waitForWindowIds(ids => ids.length > 0);
await this.code.waitForElement('.monaco-workbench');
// await this.code.waitForElement('.monaco-workbench');
if (this.remote) {
await this.code.waitForElement('.monaco-workbench .statusbar-item.statusbar-entry a[title="Editing on TestResolver"]');
}
// if (this.remote) {
// await this.code.waitForElement('.monaco-workbench .statusbar-item.statusbar-entry a[title="Editing on TestResolver"]');
// }
// wait a bit, since focus might be stolen off widgets
// as soon as they open (e.g. quick open)

View file

@ -6,8 +6,9 @@
import { Application } from '../../application';
export function setup() {
describe('Terminal', () => {
describe.only('Terminal', () => {
it(`opens terminal, runs 'echo' and verifies the output`, async function () {
this.timeout(60 * 5000);
const app = this.app as Application;
const expected = new Date().getTime().toString();

View file

@ -132,13 +132,13 @@ if (testCodePath) {
process.env.VSCODE_CLI = '1';
}
if (!fs.existsSync(electronPath || '')) {
fail(`Can't find Code at ${electronPath}.`);
}
if (typeof stablePath === 'string' && !fs.existsSync(stablePath)) {
fail(`Can't find Stable Code at ${stablePath}.`);
}
// if (!fs.existsSync(electronPath || '')) {
// fail(`Can't find Code at ${electronPath}.`);
// }
console.log(stablePath);
// if (typeof stablePath === 'string' && !fs.existsSync(stablePath)) {
// fail(`Can't find Stable Code at ${stablePath}.`);
// }
const userDataDir = path.join(testDataPath, 'd');
@ -234,7 +234,7 @@ setupDataMigrationTests(stableCodePath, testDataPath);
describe('Running Code', () => {
before(async function () {
const app = new Application(this.defaultOptions);
await app!.start();
await app!.start(false);
this.app = app;
});

View file

@ -9,44 +9,44 @@ import * as os from 'os';
import * as fs from 'fs';
import * as mkdirp from 'mkdirp';
import { tmpName } from 'tmp';
import { IDriver, connect as connectDriver, IDisposable, IElement, Thenable } from './driver';
import { IDriver, connect as connectDriver, IDisposable, IElement, Thenable } from './puppeteer-driver';
import { Logger } from '../logger';
import { ncp } from 'ncp';
const repoPath = path.join(__dirname, '../../../..');
function getDevElectronPath(): string {
const buildPath = path.join(repoPath, '.build');
const product = require(path.join(repoPath, 'product.json'));
// function getDevElectronPath(): string {
// const buildPath = path.join(repoPath, '.build');
// const product = require(path.join(repoPath, 'product.json'));
switch (process.platform) {
case 'darwin':
return path.join(buildPath, 'electron', `${product.nameLong}.app`, 'Contents', 'MacOS', 'Electron');
case 'linux':
return path.join(buildPath, 'electron', `${product.applicationName}`);
case 'win32':
return path.join(buildPath, 'electron', `${product.nameShort}.exe`);
default:
throw new Error('Unsupported platform.');
}
}
// switch (process.platform) {
// case 'darwin':
// return path.join(buildPath, 'electron', `${product.nameLong}.app`, 'Contents', 'MacOS', 'Electron');
// case 'linux':
// return path.join(buildPath, 'electron', `${product.applicationName}`);
// case 'win32':
// return path.join(buildPath, 'electron', `${product.nameShort}.exe`);
// default:
// throw new Error('Unsupported platform.');
// }
// }
function getBuildElectronPath(root: string): string {
switch (process.platform) {
case 'darwin':
return path.join(root, 'Contents', 'MacOS', 'Electron');
case 'linux': {
const product = require(path.join(root, 'resources', 'app', 'product.json'));
return path.join(root, product.applicationName);
}
case 'win32': {
const product = require(path.join(root, 'resources', 'app', 'product.json'));
return path.join(root, `${product.nameShort}.exe`);
}
default:
throw new Error('Unsupported platform.');
}
}
// function getBuildElectronPath(root: string): string {
// switch (process.platform) {
// case 'darwin':
// return path.join(root, 'Contents', 'MacOS', 'Electron');
// case 'linux': {
// const product = require(path.join(root, 'resources', 'app', 'product.json'));
// return path.join(root, product.applicationName);
// }
// case 'win32': {
// const product = require(path.join(root, 'resources', 'app', 'product.json'));
// return path.join(root, `${product.nameShort}.exe`);
// }
// default:
// throw new Error('Unsupported platform.');
// }
// }
function getDevOutPath(): string {
return path.join(repoPath, 'out');
@ -61,7 +61,7 @@ function getBuildOutPath(root: string): string {
}
}
async function connect(child: cp.ChildProcess, outPath: string, handlePath: string, logger: Logger): Promise<Code> {
async function connect(child: cp.ChildProcess | undefined, outPath: string, handlePath: string, logger: Logger): Promise<Code> {
let errCount = 0;
while (true) {
@ -69,8 +69,9 @@ async function connect(child: cp.ChildProcess, outPath: string, handlePath: stri
const { client, driver } = await connectDriver(outPath, handlePath);
return new Code(client, driver, logger);
} catch (err) {
console.log('err', err);
if (++errCount > 50) {
child.kill();
// child.kill();
throw err;
}
@ -107,7 +108,7 @@ async function createDriverHandle(): Promise<string> {
export async function spawn(options: SpawnOptions): Promise<Code> {
const codePath = options.codePath;
const electronPath = codePath ? getBuildElectronPath(codePath) : getDevElectronPath();
// const electronPath = codePath ? getBuildElectronPath(codePath) : getDevElectronPath();
const outPath = codePath ? getBuildOutPath(codePath) : getDevOutPath();
const handle = await createDriverHandle();
@ -163,13 +164,14 @@ export async function spawn(options: SpawnOptions): Promise<Code> {
args.push(...options.extraArgs);
}
const spawnOptions: cp.SpawnOptions = { env };
// const spawnOptions: cp.SpawnOptions = { env };
const child = cp.spawn(electronPath, args, spawnOptions);
// const child = cp.spawn(electronPath, args, spawnOptions);
instances.add(child);
child.once('exit', () => instances.delete(child));
// instances.add(child);
// child.once('exit', () => instances.delete(child));
const child = undefined;
return connect(child, outPath, handle, options.logger);
}

View file

@ -0,0 +1,56 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* Thenable is a common denominator between ES6 promises, Q, jquery.Deferred, WinJS.Promise,
* and others. This API makes no assumption about what promise library is being used which
* enables reusing existing code without migrating to a specific promise implementation. Still,
* we recommend the use of native promises which are available in this editor.
*/
interface Thenable<T> {
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
* @param onrejected The callback to execute when the Promise is rejected.
* @returns A Promise for the completion of which ever callback is executed.
*/
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => TResult | Thenable<TResult>): Thenable<TResult>;
then<TResult>(onfulfilled?: (value: T) => TResult | Thenable<TResult>, onrejected?: (reason: any) => void): Thenable<TResult>;
}
export interface IElement {
tagName: string;
className: string;
textContent: string;
attributes: { [name: string]: string; };
children: IElement[];
top: number;
left: number;
}
export interface IDriver {
_serviceBrand: any;
getWindowIds(): Promise<number[]>;
capturePage(windowId: number): Promise<string>;
reloadWindow(windowId: number): Promise<void>;
exitApplication(): Promise<void>;
dispatchKeybinding(windowId: number, keybinding: string): Promise<void>;
click(windowId: number, selector: string, xoffset?: number | undefined, yoffset?: number | undefined): Promise<void>;
doubleClick(windowId: number, selector: string): Promise<void>;
setValue(windowId: number, selector: string, text: string): Promise<void>;
getTitle(windowId: number): Promise<string>;
isActiveElement(windowId: number, selector: string): Promise<boolean>;
getElements(windowId: number, selector: string, recursive?: boolean): Promise<IElement[]>;
typeInEditor(windowId: number, selector: string, text: string): Promise<void>;
getTerminalBuffer(windowId: number, selector: string): Promise<string[]>;
writeInTerminal(windowId: number, selector: string, text: string): Promise<void>;
}
export interface IDisposable {
dispose(): void;
}
export function connect(outPath: string, handle: string): Promise<{ client: IDisposable, driver: IDriver }>;

View file

@ -0,0 +1,202 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const puppeteer = require('puppeteer');
// export function connect(outPath: string, handle: string): Promise<{ client: IDisposable, driver: IDriver }>
const width = 800;
const height = 600;
function buildDriver(browser, page) {
return {
_serviceBrand: undefined,
getWindowIds: () => {
return Promise.resolve([1]);
},
capturePage: () => Promise.result(''),
reloadWindow: (windowId) => Promise.resolve(),
exitApplication: () => browser.close(),
dispatchKeybinding: async (windowId, keybinding) => {
console.log('ctrl+p');
await page.keyboard.down('Control');
await page.keyboard.press('p');
await page.keyboard.up('Control');
await page.waitForSelector('.jkasndknjadsf');
},
click: (windowId, selector, xoffset, yoffset) => Promise.resolve(),
doubleClick: (windowId, selector) => Promise.resolve(),
setValue: (windowId, selector, text) => Promise.resolve(),
getTitle: (windowId) => page.title(),
isActiveElement: (windowId, selector) => {
page.evaluate(`document.querySelector('${selector}') === document.activeElement`);
},
getElements: async (windowId, selector, recursive) => {
return await page.evaluate(`
(function() {
function convertToPixels(element, value) {
return parseFloat(value) || 0;
}
function getDimension(element, cssPropertyName, jsPropertyName) {
let computedStyle = getComputedStyle(element);
let value = '0';
if (computedStyle) {
if (computedStyle.getPropertyValue) {
value = computedStyle.getPropertyValue(cssPropertyName);
} else {
// IE8
value = (computedStyle).getAttribute(jsPropertyName);
}
}
return convertToPixels(element, value);
}
function getBorderLeftWidth(element) {
return getDimension(element, 'border-left-width', 'borderLeftWidth');
}
function getBorderRightWidth(element) {
return getDimension(element, 'border-right-width', 'borderRightWidth');
}
function getBorderTopWidth(element) {
return getDimension(element, 'border-top-width', 'borderTopWidth');
}
function getBorderBottomWidth(element) {
return getDimension(element, 'border-bottom-width', 'borderBottomWidth');
}
function getTopLeftOffset(element) {
// Adapted from WinJS.Utilities.getPosition
// and added borders to the mix
let offsetParent = element.offsetParent, top = element.offsetTop, left = element.offsetLeft;
while ((element = element.parentNode) !== null && element !== document.body && element !== document.documentElement) {
top -= element.scrollTop;
let c = getComputedStyle(element);
if (c) {
left -= c.direction !== 'rtl' ? element.scrollLeft : -element.scrollLeft;
}
if (element === offsetParent) {
left += getBorderLeftWidth(element);
top += getBorderTopWidth(element);
top += element.offsetTop;
left += element.offsetLeft;
offsetParent = element.offsetParent;
}
}
return {
left: left,
top: top
};
}
function serializeElement(element, recursive) {
const attributes = Object.create(null);
for (let j = 0; j < element.attributes.length; j++) {
const attr = element.attributes.item(j);
if (attr) {
attributes[attr.name] = attr.value;
}
}
const children = [];
if (recursive) {
for (let i = 0; i < element.children.length; i++) {
const child = element.children.item(i);
if (child) {
children.push(serializeElement(child, true));
}
}
}
const { left, top } = getTopLeftOffset(element);
return {
tagName: element.tagName,
className: element.className,
textContent: element.textContent || '',
attributes,
children,
left,
top
};
}
const query = document.querySelectorAll('${selector}');
const result = [];
for (let i = 0; i < query.length; i++) {
const element = query.item(i);
result.push(serializeElement(element, ${recursive}));
}
return result;
})();
`);
},
typeInEditor: (windowId, selector, text) => Promise.resolve(),
getTerminalBuffer: async (windowId, selector) => {
return await page.evaluate(`
(function () {
const element = document.querySelector(selector);
if (!element) {
throw new Error('Terminal not found: ${selector}'');
}
const xterm: Terminal = element.xterm;
if (!xterm) {
throw new Error('Xterm not found: ${selector}');
}
const lines: string[] = [];
for (let i = 0; i < xterm.buffer.length; i++) {
lines.push(xterm.buffer.getLine(i).translateToString(true));
}
return lines;
})();
`);
},
writeInTerminal: async (windowId, selector, text) => {
page.evaluate(`
const element = document.querySelector(selector);
if (!element) {
throw new Error('Element not found: ${selector}');
}
const xterm: Terminal = element.xterm;
if (!xterm) {
throw new Error('Xterm not found: ${selector}');
}
xterm._core.handler(text);
`);
}
}
}
exports.connect = function (outPath, handle) {
return new Promise(async (c) => {
const browser = await puppeteer.launch({
headless: false,
slowMo: 80,
args: [`--window-size=${width},${height}`]
});
const page = (await browser.pages())[0];
await page.setViewport({ width, height });
await page.goto('http://127.0.0.1:8000');
const result = {
client: { dispose: () => {} },
driver: buildDriver(browser, page)
}
c(result);
});
};

View file

@ -1729,7 +1729,7 @@ concat-stream@1.6.0, concat-stream@^1.5.2:
readable-stream "^2.2.2"
typedarray "^0.0.6"
concat-stream@^1.5.0, concat-stream@^1.6.0:
concat-stream@1.6.2, concat-stream@^1.5.0, concat-stream@^1.6.0:
version "1.6.2"
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
@ -2104,7 +2104,7 @@ debug@^3.2.6:
dependencies:
ms "^2.1.1"
debug@^4.0.1:
debug@^4.0.1, debug@^4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
@ -3109,6 +3109,16 @@ extract-zip@^1.6.5:
mkdirp "0.5.0"
yauzl "2.4.1"
extract-zip@^1.6.6:
version "1.6.7"
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.7.tgz#a840b4b8af6403264c8db57f4f1a74333ef81fe9"
integrity sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=
dependencies:
concat-stream "1.6.2"
debug "2.6.9"
mkdirp "0.5.1"
yauzl "2.4.1"
extsprintf@1.3.0, extsprintf@^1.2.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
@ -5772,6 +5782,11 @@ mime@^1.4.1, mime@^1.6.0:
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
mime@^2.0.3:
version "2.4.4"
resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5"
integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==
mimic-fn@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18"
@ -7201,7 +7216,7 @@ progress@^1.1.8:
resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be"
integrity sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=
progress@^2.0.0:
progress@^2.0.0, progress@^2.0.1:
version "2.0.3"
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
@ -7224,6 +7239,11 @@ proxy-addr@~2.0.2:
forwarded "~0.1.2"
ipaddr.js "1.5.2"
proxy-from-env@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee"
integrity sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=
prr@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
@ -7298,6 +7318,20 @@ punycode@^2.1.0:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
puppeteer@^1.17.0:
version "1.17.0"
resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-1.17.0.tgz#371957d227a2f450fa74b78e78a2dadb2be7f14f"
integrity sha512-3EXZSximCzxuVKpIHtyec8Wm2dWZn1fc5tQi34qWfiUgubEVYHjUvr0GOJojqf3mifI6oyKnCdrGxaOI+lWReA==
dependencies:
debug "^4.1.0"
extract-zip "^1.6.6"
https-proxy-agent "^2.2.1"
mime "^2.0.3"
progress "^2.0.1"
proxy-from-env "^1.0.0"
rimraf "^2.6.1"
ws "^6.1.0"
q@^1.0.1, q@^1.1.2:
version "1.5.1"
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
@ -9842,6 +9876,13 @@ ws@^3.3.3:
safe-buffer "~5.1.0"
ultron "~1.1.0"
ws@^6.1.0:
version "6.2.1"
resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb"
integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==
dependencies:
async-limiter "~1.0.0"
xml-name-validator@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-1.0.0.tgz#dcf82ee092322951ef8cc1ba596c9cbfd14a83f1"