diff --git a/src/vs/platform/driver/browser/baseDriver.ts b/src/vs/platform/driver/browser/baseDriver.ts index df59dd6e879..38b5626a500 100644 --- a/src/vs/platform/driver/browser/baseDriver.ts +++ b/src/vs/platform/driver/browser/baseDriver.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getTopLeftOffset } from 'vs/base/browser/dom'; +import { getTopLeftOffset, getClientArea } from 'vs/base/browser/dom'; import { coalesce } from 'vs/base/common/arrays'; import { IElement, IWindowDriver } from 'vs/platform/driver/common/driver'; @@ -45,7 +45,6 @@ export abstract class BaseWindowDriver implements IWindowDriver { constructor() { } - // TODO: This doesn't work in browser driver abstract click(selector: string, xoffset?: number, yoffset?: number): Promise; abstract doubleClick(selector: string): Promise; @@ -101,6 +100,11 @@ export abstract class BaseWindowDriver implements IWindowDriver { return result; } + async getElementXY(selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number; y: number; }> { + const offset = typeof xoffset === 'number' && typeof yoffset === 'number' ? { x: xoffset, y: yoffset } : undefined; + return this._getElementXY(selector, offset); + } + async typeInEditor(selector: string, text: string): Promise { const element = document.querySelector(selector); @@ -159,5 +163,30 @@ export abstract class BaseWindowDriver implements IWindowDriver { xterm._core._coreService.triggerDataEvent(text); } + protected 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 }; + } + abstract async openDevTools(): Promise; } diff --git a/src/vs/platform/driver/common/driver.ts b/src/vs/platform/driver/common/driver.ts index 299628846a4..29fde875994 100644 --- a/src/vs/platform/driver/common/driver.ts +++ b/src/vs/platform/driver/common/driver.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// TODO: Change smoketest build to read off common instead +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; // !! Do not remove the following START and END markers, they are parsed by the smoketest build @@ -32,12 +32,16 @@ export interface IDriver { getTitle(windowId: number): Promise; isActiveElement(windowId: number, selector: string): Promise; getElements(windowId: number, selector: string, recursive?: boolean): Promise; + getElementXY(windowId: number, selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number; y: number; }>; typeInEditor(windowId: number, selector: string, text: string): Promise; getTerminalBuffer(windowId: number, selector: string): Promise; writeInTerminal(windowId: number, selector: string, text: string): Promise; } //*END +export const ID = 'driverService'; +export const IDriver = createDecorator(ID); + export interface IWindowDriver { click(selector: string, xoffset?: number | undefined, yoffset?: number | undefined): Promise; doubleClick(selector: string): Promise; @@ -45,6 +49,7 @@ export interface IWindowDriver { getTitle(): Promise; isActiveElement(selector: string): Promise; getElements(selector: string, recursive: boolean): Promise; + getElementXY(selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number; y: number; }>; typeInEditor(selector: string, text: string): Promise; getTerminalBuffer(selector: string): Promise; writeInTerminal(selector: string, text: string): Promise; diff --git a/src/vs/platform/driver/electron-browser/driver.ts b/src/vs/platform/driver/electron-browser/driver.ts index 931b898d55f..09e2d6b3379 100644 --- a/src/vs/platform/driver/electron-browser/driver.ts +++ b/src/vs/platform/driver/electron-browser/driver.ts @@ -7,7 +7,6 @@ import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; 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 { timeout } from 'vs/base/common/async'; @@ -30,31 +29,6 @@ class WindowDriver extends BaseWindowDriver { return this._click(selector, 2); } - 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 { const { x, y } = await this._getElementXY(selector, offset); diff --git a/src/vs/platform/driver/electron-main/driver.ts b/src/vs/platform/driver/electron-main/driver.ts index 2b341fb2ed6..8096b8f6abd 100644 --- a/src/vs/platform/driver/electron-main/driver.ts +++ b/src/vs/platform/driver/electron-main/driver.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IDriver, DriverChannel, IElement, WindowDriverChannelClient, IWindowDriverRegistry, WindowDriverRegistryChannel, IWindowDriver, IDriverOptions } from 'vs/platform/driver/node/driver'; +import { DriverChannel, WindowDriverChannelClient, IWindowDriverRegistry, WindowDriverRegistryChannel, IDriverOptions } from 'vs/platform/driver/node/driver'; import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; import { serve as serveNet } from 'vs/base/parts/ipc/node/ipc.net'; import { combinedDisposable, IDisposable } from 'vs/base/common/lifecycle'; @@ -17,6 +17,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { ScanCodeBinding } from 'vs/base/common/scanCode'; import { KeybindingParser } from 'vs/base/common/keybindingParser'; import { timeout } from 'vs/base/common/async'; +import { IDriver, IElement, IWindowDriver } from 'vs/platform/driver/common/driver'; function isSilentKeyCode(keyCode: KeyCode) { return keyCode < KeyCode.KEY_0; @@ -163,6 +164,11 @@ export class Driver implements IDriver, IWindowDriverRegistry { return await windowDriver.getElements(selector, recursive); } + async getElementXY(windowId: number, selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number; y: number; }> { + const windowDriver = await this.getWindowDriver(windowId); + return await windowDriver.getElementXY(selector, xoffset, yoffset); + } + async typeInEditor(windowId: number, selector: string, text: string): Promise { const windowDriver = await this.getWindowDriver(windowId); await windowDriver.typeInEditor(selector, text); diff --git a/src/vs/platform/driver/node/driver.ts b/src/vs/platform/driver/node/driver.ts index e77790a5183..2c525c0e694 100644 --- a/src/vs/platform/driver/node/driver.ts +++ b/src/vs/platform/driver/node/driver.ts @@ -5,45 +5,9 @@ import { Client } from 'vs/base/parts/ipc/common/ipc.net'; import { connect as connectNet } from 'vs/base/parts/ipc/node/ipc.net'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; import { Event } from 'vs/base/common/event'; - -export const ID = 'driverService'; -export const IDriver = createDecorator(ID); - -// !! Do not remove the following START and END markers, they are parsed by the smoketest build - -//*START -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; - capturePage(windowId: number): Promise; - reloadWindow(windowId: number): Promise; - exitApplication(): Promise; - dispatchKeybinding(windowId: number, keybinding: string): Promise; - click(windowId: number, selector: string, xoffset?: number | undefined, yoffset?: number | undefined): Promise; - doubleClick(windowId: number, selector: string): Promise; - setValue(windowId: number, selector: string, text: string): Promise; - getTitle(windowId: number): Promise; - isActiveElement(windowId: number, selector: string): Promise; - getElements(windowId: number, selector: string, recursive?: boolean): Promise; - typeInEditor(windowId: number, selector: string, text: string): Promise; - getTerminalBuffer(windowId: number, selector: string): Promise; - writeInTerminal(windowId: number, selector: string, text: string): Promise; -} -//*END +import { IDriver, IElement, IWindowDriver } from 'vs/platform/driver/common/driver'; export class DriverChannel implements IServerChannel { @@ -66,6 +30,7 @@ export class DriverChannel implements IServerChannel { case 'getTitle': return this.driver.getTitle(arg[0]); case 'isActiveElement': return this.driver.isActiveElement(arg[0], arg[1]); case 'getElements': return this.driver.getElements(arg[0], arg[1], arg[2]); + case 'getElementXY': return this.driver.getElementXY(arg[0], arg[1], arg[2]); case 'typeInEditor': return this.driver.typeInEditor(arg[0], arg[1], arg[2]); case 'getTerminalBuffer': return this.driver.getTerminalBuffer(arg[0], arg[1]); case 'writeInTerminal': return this.driver.writeInTerminal(arg[0], arg[1], arg[2]); @@ -125,6 +90,10 @@ export class DriverChannelClient implements IDriver { return this.channel.call('getElements', [windowId, selector, recursive]); } + getElementXY(windowId: number, selector: string, xoffset: number | undefined, yoffset: number | undefined): Promise<{ x: number, y: number }> { + return this.channel.call('getElementXY', [windowId, selector, xoffset, yoffset]); + } + typeInEditor(windowId: number, selector: string, text: string): Promise { return this.channel.call('typeInEditor', [windowId, selector, text]); } @@ -180,18 +149,6 @@ export class WindowDriverRegistryChannelClient implements IWindowDriverRegistry } } -export interface IWindowDriver { - click(selector: string, xoffset?: number | undefined, yoffset?: number | undefined): Promise; - doubleClick(selector: string): Promise; - setValue(selector: string, text: string): Promise; - getTitle(): Promise; - isActiveElement(selector: string): Promise; - getElements(selector: string, recursive: boolean): Promise; - typeInEditor(selector: string, text: string): Promise; - getTerminalBuffer(selector: string): Promise; - writeInTerminal(selector: string, text: string): Promise; -} - export class WindowDriverChannel implements IServerChannel { constructor(private driver: IWindowDriver) { } @@ -208,6 +165,7 @@ export class WindowDriverChannel implements IServerChannel { case 'getTitle': return this.driver.getTitle(); case 'isActiveElement': return this.driver.isActiveElement(arg); case 'getElements': return this.driver.getElements(arg[0], arg[1]); + case 'getElementXY': return this.driver.getElementXY(arg[0], arg[1], arg[2]); case 'typeInEditor': return this.driver.typeInEditor(arg[0], arg[1]); case 'getTerminalBuffer': return this.driver.getTerminalBuffer(arg); case 'writeInTerminal': return this.driver.writeInTerminal(arg[0], arg[1]); @@ -247,6 +205,10 @@ export class WindowDriverChannelClient implements IWindowDriver { return this.channel.call('getElements', [selector, recursive]); } + getElementXY(selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number, y: number }> { + return this.channel.call('getElementXY', [selector, xoffset, yoffset]); + } + typeInEditor(selector: string, text: string): Promise { return this.channel.call('typeInEditor', [selector, text]); } diff --git a/test/smoke/src/vscode/puppeteerDriver.ts b/test/smoke/src/vscode/puppeteerDriver.ts index 2e2f2280dd6..7205ce266a6 100644 --- a/test/smoke/src/vscode/puppeteerDriver.ts +++ b/test/smoke/src/vscode/puppeteerDriver.ts @@ -26,7 +26,7 @@ const vscodeToPuppeteerKey = { }; function buildDriver(browser: puppeteer.Browser, page: puppeteer.Page): IDriver { - return { + const driver = { _serviceBrand: undefined, getWindowIds: () => { return Promise.resolve([1]); @@ -57,121 +57,25 @@ function buildDriver(browser: puppeteer.Browser, page: puppeteer.Page): IDriver await timeout(100); }, click: async (windowId, selector, xoffset, yoffset) => { - const { x, y } = 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 getClientArea(element) { - // Try with DOM clientWidth / clientHeight - if (element !== document.body) { - return { width: element.clientWidth, height: element.clientHeight }; - } - - // Try innerWidth / innerHeight - if (window.innerWidth && window.innerHeight) { - return { width: window.innerWidth, height: window.innerHeight }; - } - - // Try with document.body.clientWidth / document.body.clientHeight - if (document.body && document.body.clientWidth && document.body.clientHeight) { - return { width: document.body.clientWidth, height: document.body.clientHeight }; - } - - // Try with document.documentElement.clientWidth / document.documentElement.clientHeight - if (document.documentElement && document.documentElement.clientWidth && document.documentElement.clientHeight) { - return { width: document.documentElement.clientWidth, height: document.documentElement.clientHeight }; - } - - throw new Error('Unable to figure out browser width and height'); - } - 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 - }; - } - const element = document.querySelector('${selector}'); - - if (!element) { - throw new Error('Element not found: ${selector}'); - } - - const { left, top } = getTopLeftOffset(element); - const { width, height } = getClientArea(element); - let x, y; - - x = left + (width / 2); - y = top + (height / 2); - - x = Math.round(x); - y = Math.round(y); - - return { x, y }; - })(); - `); + 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 this.click(windowId, selector, 0, 0); + await driver.click(windowId, selector, 0, 0); await timeout(60); - await this.click(windowId, selector, 0, 0); + await driver.click(windowId, selector, 0, 0); await timeout(100); }, 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})`), + 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 { @@ -275,6 +179,7 @@ export interface IDriver { getTitle(windowId: number): Promise; isActiveElement(windowId: number, selector: string): Promise; getElements(windowId: number, selector: string, recursive?: boolean): Promise; + getElementXY(selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number; y: number; }>; typeInEditor(windowId: number, selector: string, text: string): Promise; getTerminalBuffer(windowId: number, selector: string): Promise; writeInTerminal(windowId: number, selector: string, text: string): Promise; diff --git a/test/smoke/tools/copy-driver-definition.js b/test/smoke/tools/copy-driver-definition.js index 2af7a3acd2a..fdebfcc6b0a 100644 --- a/test/smoke/tools/copy-driver-definition.js +++ b/test/smoke/tools/copy-driver-definition.js @@ -7,7 +7,7 @@ const fs = require('fs'); const path = require('path'); const root = path.dirname(path.dirname(path.dirname(__dirname))); -const driverPath = path.join(root, 'src/vs/platform/driver/node/driver.ts'); +const driverPath = path.join(root, 'src/vs/platform/driver/common/driver.ts'); let contents = fs.readFileSync(driverPath, 'utf8'); contents = /\/\/\*START([\s\S]*)\/\/\*END/mi.exec(contents)[1].trim(); @@ -47,4 +47,4 @@ export function connect(outPath: string, handle: string): Promise<{ client: IDis const srcPath = path.join(path.dirname(__dirname), 'src/vscode'); const outDriverPath = path.join(srcPath, 'driver.d.ts'); -fs.writeFileSync(outDriverPath, contents); \ No newline at end of file +fs.writeFileSync(outDriverPath, contents);