Path: blob/main/src/vs/workbench/services/driver/browser/driver.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { getClientArea, getTopLeftOffset, isHTMLDivElement, isHTMLTextAreaElement } from '../../../../base/browser/dom.js';6import { mainWindow } from '../../../../base/browser/window.js';7import { coalesce } from '../../../../base/common/arrays.js';8import { language, locale } from '../../../../base/common/platform.js';9import { IEnvironmentService } from '../../../../platform/environment/common/environment.js';10import { IFileService } from '../../../../platform/files/common/files.js';11import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';12import localizedStrings from '../../../../platform/languagePacks/common/localizedStrings.js';13import { ILogFile, getLogs } from '../../../../platform/log/browser/log.js';14import { ILogService } from '../../../../platform/log/common/log.js';15import { Registry } from '../../../../platform/registry/common/platform.js';16import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from '../../../common/contributions.js';17import { IWindowDriver, IElement, ILocaleInfo, ILocalizedStrings } from '../common/driver.js';18import { ILifecycleService, LifecyclePhase } from '../../lifecycle/common/lifecycle.js';19import type { Terminal as XtermTerminal } from '@xterm/xterm';2021export class BrowserWindowDriver implements IWindowDriver {2223constructor(24@IFileService private readonly fileService: IFileService,25@IEnvironmentService private readonly environmentService: IEnvironmentService,26@ILifecycleService private readonly lifecycleService: ILifecycleService,27@ILogService private readonly logService: ILogService28) {29}3031async getLogs(): Promise<ILogFile[]> {32return getLogs(this.fileService, this.environmentService);33}3435async whenWorkbenchRestored(): Promise<void> {36this.logService.info('[driver] Waiting for restored lifecycle phase...');37await this.lifecycleService.when(LifecyclePhase.Restored);38this.logService.info('[driver] Restored lifecycle phase reached. Waiting for contributions...');39await Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).whenRestored;40this.logService.info('[driver] Workbench contributions created.');41}4243async setValue(selector: string, text: string): Promise<void> {44const element = mainWindow.document.querySelector(selector);4546if (!element) {47return Promise.reject(new Error(`Element not found: ${selector}`));48}4950const inputElement = element as HTMLInputElement;51inputElement.value = text;5253const event = new Event('input', { bubbles: true, cancelable: true });54inputElement.dispatchEvent(event);55}5657async isActiveElement(selector: string): Promise<boolean> {58const element = mainWindow.document.querySelector(selector);5960if (element !== mainWindow.document.activeElement) {61const chain: string[] = [];62let el = mainWindow.document.activeElement;6364while (el) {65const tagName = el.tagName;66const id = el.id ? `#${el.id}` : '';67const classes = coalesce(el.className.split(/\s+/g).map(c => c.trim())).map(c => `.${c}`).join('');68chain.unshift(`${tagName}${id}${classes}`);6970el = el.parentElement;71}7273throw new Error(`Active element not found. Current active element is '${chain.join(' > ')}'. Looking for ${selector}`);74}7576return true;77}7879async getElements(selector: string, recursive: boolean): Promise<IElement[]> {80const query = mainWindow.document.querySelectorAll(selector);81const result: IElement[] = [];82for (let i = 0; i < query.length; i++) {83const element = query.item(i);84result.push(this.serializeElement(element, recursive));85}8687return result;88}8990private serializeElement(element: Element, recursive: boolean): IElement {91const attributes = Object.create(null);9293for (let j = 0; j < element.attributes.length; j++) {94const attr = element.attributes.item(j);95if (attr) {96attributes[attr.name] = attr.value;97}98}99100const children: IElement[] = [];101102if (recursive) {103for (let i = 0; i < element.children.length; i++) {104const child = element.children.item(i);105if (child) {106children.push(this.serializeElement(child, true));107}108}109}110111const { left, top } = getTopLeftOffset(element as HTMLElement);112113return {114tagName: element.tagName,115className: element.className,116textContent: element.textContent || '',117attributes,118children,119left,120top121};122}123124async getElementXY(selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number; y: number }> {125const offset = typeof xoffset === 'number' && typeof yoffset === 'number' ? { x: xoffset, y: yoffset } : undefined;126return this._getElementXY(selector, offset);127}128129async typeInEditor(selector: string, text: string): Promise<void> {130const element = mainWindow.document.querySelector(selector);131132if (!element) {133throw new Error(`Editor not found: ${selector}`);134}135if (isHTMLDivElement(element)) {136// Edit context is enabled137const editContext = element.editContext;138if (!editContext) {139throw new Error(`Edit context not found: ${selector}`);140}141const selectionStart = editContext.selectionStart;142const selectionEnd = editContext.selectionEnd;143const event = new TextUpdateEvent('textupdate', {144updateRangeStart: selectionStart,145updateRangeEnd: selectionEnd,146text,147selectionStart: selectionStart + text.length,148selectionEnd: selectionStart + text.length,149compositionStart: 0,150compositionEnd: 0151});152editContext.dispatchEvent(event);153} else if (isHTMLTextAreaElement(element)) {154const start = element.selectionStart;155const newStart = start + text.length;156const value = element.value;157const newValue = value.substr(0, start) + text + value.substr(start);158159element.value = newValue;160element.setSelectionRange(newStart, newStart);161162const event = new Event('input', { 'bubbles': true, 'cancelable': true });163element.dispatchEvent(event);164}165}166167async getEditorSelection(selector: string): Promise<{ selectionStart: number; selectionEnd: number }> {168const element = mainWindow.document.querySelector(selector);169if (!element) {170throw new Error(`Editor not found: ${selector}`);171}172if (isHTMLDivElement(element)) {173const editContext = element.editContext;174if (!editContext) {175throw new Error(`Edit context not found: ${selector}`);176}177return { selectionStart: editContext.selectionStart, selectionEnd: editContext.selectionEnd };178} else if (isHTMLTextAreaElement(element)) {179return { selectionStart: element.selectionStart, selectionEnd: element.selectionEnd };180} else {181throw new Error(`Unknown type of element: ${selector}`);182}183}184185async getTerminalBuffer(selector: string): Promise<string[]> {186const element = mainWindow.document.querySelector(selector);187188if (!element) {189throw new Error(`Terminal not found: ${selector}`);190}191192const xterm = (element as any).xterm;193194if (!xterm) {195throw new Error(`Xterm not found: ${selector}`);196}197198const lines: string[] = [];199for (let i = 0; i < xterm.buffer.active.length; i++) {200lines.push(xterm.buffer.active.getLine(i)!.translateToString(true));201}202203return lines;204}205206async writeInTerminal(selector: string, text: string): Promise<void> {207const element = mainWindow.document.querySelector(selector);208209if (!element) {210throw new Error(`Element not found: ${selector}`);211}212213const xterm = (element as any).xterm as (XtermTerminal | undefined);214215if (!xterm) {216throw new Error(`Xterm not found: ${selector}`);217}218219xterm.input(text);220}221222getLocaleInfo(): Promise<ILocaleInfo> {223return Promise.resolve({224language: language,225locale: locale226});227}228229getLocalizedStrings(): Promise<ILocalizedStrings> {230return Promise.resolve({231open: localizedStrings.open,232close: localizedStrings.close,233find: localizedStrings.find234});235}236237protected async _getElementXY(selector: string, offset?: { x: number; y: number }): Promise<{ x: number; y: number }> {238const element = mainWindow.document.querySelector(selector);239240if (!element) {241return Promise.reject(new Error(`Element not found: ${selector}`));242}243244const { left, top } = getTopLeftOffset(element as HTMLElement);245const { width, height } = getClientArea(element as HTMLElement);246let x: number, y: number;247248if (offset) {249x = left + offset.x;250y = top + offset.y;251} else {252x = left + (width / 2);253y = top + (height / 2);254}255256x = Math.round(x);257y = Math.round(y);258259return { x, y };260}261}262263export function registerWindowDriver(instantiationService: IInstantiationService): void {264Object.assign(mainWindow, { driver: instantiationService.createInstance(BrowserWindowDriver) });265}266267268