Share code between electron and browser drivers

This commit is contained in:
Daniel Imms 2019-07-27 13:39:35 -07:00
parent f9dc56ccfe
commit 65cd41503b
9 changed files with 213 additions and 530 deletions

View file

@ -64,7 +64,6 @@
"@types/minimist": "^1.2.0",
"@types/mocha": "2.2.39",
"@types/node": "^10.12.12",
"@types/puppeteer": "^1.12.4",
"@types/semver": "^5.5.0",
"@types/sinon": "^1.16.36",
"@types/webpack": "^4.4.10",

View file

@ -0,0 +1,166 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { getTopLeftOffset } from 'vs/base/browser/dom';
// TODO: Allow this
// tslint:disable-next-line: import-patterns
import { Terminal } from 'xterm';
import { coalesce } from 'vs/base/common/arrays';
import { IElement, IWindowDriver } from 'vs/platform/driver/common/driver';
function serializeElement(element: Element, recursive: boolean): IElement {
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: IElement[] = [];
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 as HTMLElement);
return {
tagName: element.tagName,
className: element.className,
textContent: element.textContent || '',
attributes,
children,
left,
top
};
}
export abstract class BaseWindowDriver implements IWindowDriver {
constructor() { }
// TODO: This doesn't work in browser driver
abstract click(selector: string, xoffset?: number, yoffset?: number): Promise<void>;
abstract doubleClick(selector: string): Promise<void>;
async setValue(selector: string, text: string): Promise<void> {
const element = document.querySelector(selector);
if (!element) {
return Promise.reject(new Error(`Element not found: ${selector}`));
}
const inputElement = element as HTMLInputElement;
inputElement.value = text;
const event = new Event('input', { bubbles: true, cancelable: true });
inputElement.dispatchEvent(event);
}
async getTitle(): Promise<string> {
return document.title;
}
async isActiveElement(selector: string): Promise<boolean> {
const element = document.querySelector(selector);
if (element !== document.activeElement) {
const chain: string[] = [];
let el = document.activeElement;
while (el) {
const tagName = el.tagName;
const id = el.id ? `#${el.id}` : '';
const classes = coalesce(el.className.split(/\s+/g).map(c => c.trim())).map(c => `.${c}`).join('');
chain.unshift(`${tagName}${id}${classes}`);
el = el.parentElement;
}
throw new Error(`Active element not found. Current active element is '${chain.join(' > ')}'. Looking for ${selector}`);
}
return true;
}
async getElements(selector: string, recursive: boolean): Promise<IElement[]> {
const query = document.querySelectorAll(selector);
const result: IElement[] = [];
for (let i = 0; i < query.length; i++) {
const element = query.item(i);
result.push(serializeElement(element, recursive));
}
return result;
}
async typeInEditor(selector: string, text: string): Promise<void> {
const element = document.querySelector(selector);
if (!element) {
throw new Error(`Editor not found: ${selector}`);
}
const textarea = element as HTMLTextAreaElement;
const start = textarea.selectionStart;
const newStart = start + text.length;
const value = textarea.value;
const newValue = value.substr(0, start) + text + value.substr(start);
textarea.value = newValue;
textarea.setSelectionRange(newStart, newStart);
const event = new Event('input', { 'bubbles': true, 'cancelable': true });
textarea.dispatchEvent(event);
}
async getTerminalBuffer(selector: string): Promise<string[]> {
const element = document.querySelector(selector);
if (!element) {
throw new Error(`Terminal not found: ${selector}`);
}
const xterm: Terminal = (element as any).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;
}
async writeInTerminal(selector: string, text: string): Promise<void> {
const element = document.querySelector(selector);
if (!element) {
throw new Error(`Element not found: ${selector}`);
}
const xterm: Terminal = (element as any).xterm;
if (!xterm) {
throw new Error(`Xterm not found: ${selector}`);
}
xterm._core._coreService.triggerDataEvent(text);
}
abstract async openDevTools(): Promise<void>;
}

View file

@ -4,209 +4,17 @@
*--------------------------------------------------------------------------------------------*/
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { getTopLeftOffset } from 'vs/base/browser/dom';
// TODO: Allow this
// tslint:disable-next-line: import-patterns
import { Terminal } from 'xterm';
import { coalesce } from 'vs/base/common/arrays';
import { IElement, IWindowDriver } from 'vs/platform/driver/common/driver';
import { BaseWindowDriver } from 'vs/platform/driver/browser/baseDriver';
function serializeElement(element: Element, recursive: boolean): IElement {
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;
}
class BrowserWindowDriver extends BaseWindowDriver {
click(selector: string, xoffset?: number | undefined, yoffset?: number | undefined): Promise<void> {
throw new Error('Method not implemented.');
}
const children: IElement[] = [];
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 as HTMLElement);
return {
tagName: element.tagName,
className: element.className,
textContent: element.textContent || '',
attributes,
children,
left,
top
};
}
class BrowserWindowDriver implements IWindowDriver {
constructor() { }
click(selector: string, xoffset?: number, yoffset?: number): Promise<void> {
const offset = typeof xoffset === 'number' && typeof yoffset === 'number' ? { x: xoffset, y: yoffset } : undefined;
return this._click(selector, 1, offset);
}
doubleClick(selector: string): Promise<void> {
return this._click(selector, 2);
throw new Error('Method not implemented.');
}
// private async _getElementXY(selector: string, offset?: { x: number, y: number }): Promise<{ x: number; y: number; }> {
// const element = document.querySelector(selector);
// if (!element) {
// return Promise.reject(new Error(`Element not found: ${selector}`));
// }
// const { left, top } = getTopLeftOffset(element as HTMLElement);
// const { width, height } = getClientArea(element as HTMLElement);
// let x: number, y: number;
// if (offset) {
// x = left + offset.x;
// y = top + offset.y;
// } else {
// x = left + (width / 2);
// y = top + (height / 2);
// }
// x = Math.round(x);
// y = Math.round(y);
// return { x, y };
// }
private async _click(selector: string, clickCount: number, offset?: { x: number, y: number }): Promise<void> {
console.log('NYI');
// const { x, y } = await this._getElementXY(selector, offset);
// const webContents: electron.WebContents = (electron as any).remote.getCurrentWebContents();
// webContents.sendInputEvent({ type: 'mouseDown', x, y, button: 'left', clickCount } as any);
// await timeout(10);
// webContents.sendInputEvent({ type: 'mouseUp', x, y, button: 'left', clickCount } as any);
// await timeout(100);
}
async setValue(selector: string, text: string): Promise<void> {
const element = document.querySelector(selector);
if (!element) {
return Promise.reject(new Error(`Element not found: ${selector}`));
}
const inputElement = element as HTMLInputElement;
inputElement.value = text;
const event = new Event('input', { bubbles: true, cancelable: true });
inputElement.dispatchEvent(event);
}
async getTitle(): Promise<string> {
return document.title;
}
async isActiveElement(selector: string): Promise<boolean> {
const element = document.querySelector(selector);
if (element !== document.activeElement) {
const chain: string[] = [];
let el = document.activeElement;
while (el) {
const tagName = el.tagName;
const id = el.id ? `#${el.id}` : '';
const classes = coalesce(el.className.split(/\s+/g).map(c => c.trim())).map(c => `.${c}`).join('');
chain.unshift(`${tagName}${id}${classes}`);
el = el.parentElement;
}
throw new Error(`Active element not found. Current active element is '${chain.join(' > ')}'. Looking for ${selector}`);
}
return true;
}
async getElements(selector: string, recursive: boolean): Promise<IElement[]> {
const query = document.querySelectorAll(selector);
const result: IElement[] = [];
for (let i = 0; i < query.length; i++) {
const element = query.item(i);
result.push(serializeElement(element, recursive));
}
return result;
}
async typeInEditor(selector: string, text: string): Promise<void> {
const element = document.querySelector(selector);
if (!element) {
throw new Error(`Editor not found: ${selector}`);
}
const textarea = element as HTMLTextAreaElement;
const start = textarea.selectionStart;
const newStart = start + text.length;
const value = textarea.value;
const newValue = value.substr(0, start) + text + value.substr(start);
textarea.value = newValue;
textarea.setSelectionRange(newStart, newStart);
const event = new Event('input', { 'bubbles': true, 'cancelable': true });
textarea.dispatchEvent(event);
}
async getTerminalBuffer(selector: string): Promise<string[]> {
const element = document.querySelector(selector);
if (!element) {
throw new Error(`Terminal not found: ${selector}`);
}
const xterm: Terminal = (element as any).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;
}
async writeInTerminal(selector: string, text: string): Promise<void> {
const element = document.querySelector(selector);
if (!element) {
throw new Error(`Element not found: ${selector}`);
}
const xterm: Terminal = (element as any).xterm;
if (!xterm) {
throw new Error(`Xterm not found: ${selector}`);
}
xterm._core._coreService.triggerDataEvent(text);
}
async openDevTools(): Promise<void> {
// await this.windowService.openDevTools({ mode: 'detach' });
openDevTools(): Promise<void> {
throw new Error('Method not implemented.');
}
}

View file

@ -4,55 +4,22 @@
*--------------------------------------------------------------------------------------------*/
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { IWindowDriver, IElement, WindowDriverChannel, WindowDriverRegistryChannelClient } from 'vs/platform/driver/node/driver';
import { WindowDriverChannel, WindowDriverRegistryChannelClient } from 'vs/platform/driver/node/driver';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService';
import { getTopLeftOffset, getClientArea } from 'vs/base/browser/dom';
import * as electron from 'electron';
import { IWindowService } from 'vs/platform/windows/common/windows';
import { Terminal } from 'xterm';
import { timeout } from 'vs/base/common/async';
import { coalesce } from 'vs/base/common/arrays';
import { BaseWindowDriver } from 'vs/platform/driver/browser/baseDriver';
function serializeElement(element: Element, recursive: boolean): IElement {
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: IElement[] = [];
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 as HTMLElement);
return {
tagName: element.tagName,
className: element.className,
textContent: element.textContent || '',
attributes,
children,
left,
top
};
}
class WindowDriver implements IWindowDriver {
class WindowDriver extends BaseWindowDriver {
constructor(
@IWindowService private readonly windowService: IWindowService
) { }
) {
super();
}
click(selector: string, xoffset?: number, yoffset?: number): Promise<void> {
const offset = typeof xoffset === 'number' && typeof yoffset === 'number' ? { x: xoffset, y: yoffset } : undefined;
@ -99,116 +66,6 @@ class WindowDriver implements IWindowDriver {
await timeout(100);
}
async setValue(selector: string, text: string): Promise<void> {
const element = document.querySelector(selector);
if (!element) {
return Promise.reject(new Error(`Element not found: ${selector}`));
}
const inputElement = element as HTMLInputElement;
inputElement.value = text;
const event = new Event('input', { bubbles: true, cancelable: true });
inputElement.dispatchEvent(event);
}
async getTitle(): Promise<string> {
return document.title;
}
async isActiveElement(selector: string): Promise<boolean> {
const element = document.querySelector(selector);
if (element !== document.activeElement) {
const chain: string[] = [];
let el = document.activeElement;
while (el) {
const tagName = el.tagName;
const id = el.id ? `#${el.id}` : '';
const classes = coalesce(el.className.split(/\s+/g).map(c => c.trim())).map(c => `.${c}`).join('');
chain.unshift(`${tagName}${id}${classes}`);
el = el.parentElement;
}
throw new Error(`Active element not found. Current active element is '${chain.join(' > ')}'. Looking for ${selector}`);
}
return true;
}
async getElements(selector: string, recursive: boolean): Promise<IElement[]> {
const query = document.querySelectorAll(selector);
const result: IElement[] = [];
for (let i = 0; i < query.length; i++) {
const element = query.item(i);
result.push(serializeElement(element, recursive));
}
return result;
}
async typeInEditor(selector: string, text: string): Promise<void> {
const element = document.querySelector(selector);
if (!element) {
throw new Error(`Editor not found: ${selector}`);
}
const textarea = element as HTMLTextAreaElement;
const start = textarea.selectionStart;
const newStart = start + text.length;
const value = textarea.value;
const newValue = value.substr(0, start) + text + value.substr(start);
textarea.value = newValue;
textarea.setSelectionRange(newStart, newStart);
const event = new Event('input', { 'bubbles': true, 'cancelable': true });
textarea.dispatchEvent(event);
}
async getTerminalBuffer(selector: string): Promise<string[]> {
const element = document.querySelector(selector);
if (!element) {
throw new Error(`Terminal not found: ${selector}`);
}
const xterm: Terminal = (element as any).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;
}
async writeInTerminal(selector: string, text: string): Promise<void> {
const element = document.querySelector(selector);
if (!element) {
throw new Error(`Element not found: ${selector}`);
}
const xterm: Terminal = (element as any).xterm;
if (!xterm) {
throw new Error(`Xterm not found: ${selector}`);
}
xterm._core._coreService.triggerDataEvent(text);
}
async openDevTools(): Promise<void> {
await this.windowService.openDevTools({ mode: 'detach' });
}

View file

@ -18,6 +18,7 @@
"@types/mocha": "2.2.41",
"@types/ncp": "2.0.1",
"@types/node": "^10.14.8",
"@types/puppeteer": "^1.19.0",
"@types/rimraf": "2.0.2",
"@types/webdriverio": "4.6.1",
"concurrently": "^3.5.1",

View file

@ -236,7 +236,9 @@ after(async function () {
await new Promise((c, e) => rimraf(testDataPath, { maxBusyTries: 10 }, err => err ? e(err) : c()));
});
setupDataMigrationTests(stableCodePath, testDataPath);
if (!opts.web) {
setupDataMigrationTests(stableCodePath, testDataPath);
}
describe('Running Code', () => {
before(async function () {
@ -271,6 +273,7 @@ describe('Running Code', () => {
}
if (opts.web) {
console.log('setup term tests only');
setupTerminalTests();
return;
}
@ -290,4 +293,6 @@ describe('Running Code', () => {
setupDataLocalizationTests();
});
setupLaunchTests();
if (!opts.web) {
setupLaunchTests();
}

View file

@ -139,181 +139,28 @@ function buildDriver(browser: puppeteer.Browser, page: puppeteer.Page): IDriver
`);
await page.mouse.click(x + (xoffset ? xoffset : 0), y + (yoffset ? yoffset : 0));
},
doubleClick: (windowId, selector) => Promise.resolve(),
setValue: async (windowId, selector, text) => {
return page.evaluate(`
(function() {
const element = document.querySelector('${selector}');
if (!element) {
throw new Error('Element not found: ${selector}');
}
const inputElement = element;
inputElement.value = '${text}';
const event = new Event('input', { bubbles: true, cancelable: true });
inputElement.dispatchEvent(event);
return true;
})();
`);
doubleClick: async (windowId, selector) => {
await this.click(windowId, selector, 0, 0);
await timeout(60);
await this.click(windowId, selector, 0, 0);
await timeout(100);
},
getTitle: (windowId) => page.title(),
isActiveElement: (windowId, selector) => {
return page.evaluate(`document.querySelector('${selector}') === document.activeElement`);
},
getElements: (windowId, selector, recursive) => {
return 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: (windowId, selector) => {
return page.evaluate(`
(function () {
const element = document.querySelector('${selector}');
if (!element) {
throw new Error('Terminal not found: ${selector}');
}
const xterm = element.xterm;
if (!xterm) {
throw new Error('Xterm not found: ${selector}');
}
const lines = [];
for (let i = 0; i < xterm.buffer.length; i++) {
lines.push(xterm.buffer.getLine(i).translateToString(true));
}
return lines;
})();
`);
},
writeInTerminal: (windowId, selector, text) => {
return page.evaluate(`
(function () {
const element = document.querySelector('${selector}');
if (!element) {
throw new Error('Element not found: ${selector}');
}
const xterm = element.xterm;
if (!xterm) {
throw new Error('Xterm not found: ${selector}');
}
xterm._core._coreService.triggerDataEvent('${text}');
})();
`);
}
setValue: async (windowId, selector, text) => page.evaluate(`window.driver.setValue('${selector}', '${text}')`),
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})`),
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}')`)
};
}
function timeout(ms: number): Promise<void> {
return new Promise<void>(r => setTimeout(r, ms));
}
// function runInDriver(call: string, args: (string | boolean)[]): Promise<any> {}
let args;
export function launch(_args): void {
@ -389,4 +236,4 @@ export interface IDriver {
export interface IDisposable {
dispose(): void;
}
}

View file

@ -54,6 +54,13 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.14.8.tgz#fe444203ecef1162348cd6deb76c62477b2cc6e9"
integrity sha512-I4+DbJEhLEg4/vIy/2gkWDvXBOOtPKV9EnLhYjMoqxcRW+TTZtUftkHktz/a8suoD5mUL7m6ReLrkPvSsCQQmw==
"@types/puppeteer@^1.19.0":
version "1.19.0"
resolved "https://registry.yarnpkg.com/@types/puppeteer/-/puppeteer-1.19.0.tgz#59f0050bae019cee7c3af2bb840a25892a3078b6"
integrity sha512-Db9LWOuTm2bR/qgPE7PQCmnsCQ6flHdULuIDWTks8YdQ/SGHKg5WGWG54gl0734NDKCTF5MbqAp2qWuvBiyQ3Q==
dependencies:
"@types/node" "*"
"@types/rimraf@2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-2.0.2.tgz#7f0fc3cf0ff0ad2a99bb723ae1764f30acaf8b6e"

View file

@ -137,13 +137,6 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.12.tgz#e15a9d034d9210f00320ef718a50c4a799417c47"
integrity sha512-Pr+6JRiKkfsFvmU/LK68oBRCQeEg36TyAbPhc2xpez24OOZZCuoIhWGTd39VZy6nGafSbxzGouFPTFD/rR1A0A==
"@types/puppeteer@^1.12.4":
version "1.12.4"
resolved "https://registry.yarnpkg.com/@types/puppeteer/-/puppeteer-1.12.4.tgz#8388efdb0b30a54a7e7c4831ca0d709191d77ff1"
integrity sha512-aaGbJaJ9TuF9vZfTeoh876sBa+rYJWPwtsmHmYr28pGr42ewJnkDTq2aeSKEmS39SqUdkwLj73y/d7rBSp7mDQ==
dependencies:
"@types/node" "*"
"@types/semver@^5.4.0", "@types/semver@^5.5.0":
version "5.5.0"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45"