Path: blob/main/src/vs/workbench/services/driver/browser/driver.ts
5251 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> {44// eslint-disable-next-line no-restricted-syntax45const element = mainWindow.document.querySelector(selector);4647if (!element) {48return Promise.reject(new Error(`Element not found: ${selector}`));49}5051const inputElement = element as HTMLInputElement;52inputElement.value = text;5354const event = new Event('input', { bubbles: true, cancelable: true });55inputElement.dispatchEvent(event);56}5758async isActiveElement(selector: string): Promise<boolean> {59// eslint-disable-next-line no-restricted-syntax60const element = mainWindow.document.querySelector(selector);6162if (element !== mainWindow.document.activeElement) {63const chain: string[] = [];64let el = mainWindow.document.activeElement;6566while (el) {67const tagName = el.tagName;68const id = el.id ? `#${el.id}` : '';69const classes = coalesce(el.className.split(/\s+/g).map(c => c.trim())).map(c => `.${c}`).join('');70chain.unshift(`${tagName}${id}${classes}`);7172el = el.parentElement;73}7475throw new Error(`Active element not found. Current active element is '${chain.join(' > ')}'. Looking for ${selector}`);76}7778return true;79}8081async getElements(selector: string, recursive: boolean): Promise<IElement[]> {82// eslint-disable-next-line no-restricted-syntax83const query = mainWindow.document.querySelectorAll(selector);84const result: IElement[] = [];85for (let i = 0; i < query.length; i++) {86const element = query.item(i);87result.push(this.serializeElement(element, recursive));88}8990return result;91}9293private serializeElement(element: Element, recursive: boolean): IElement {94const attributes = Object.create(null);9596for (let j = 0; j < element.attributes.length; j++) {97const attr = element.attributes.item(j);98if (attr) {99attributes[attr.name] = attr.value;100}101}102103const children: IElement[] = [];104105if (recursive) {106for (let i = 0; i < element.children.length; i++) {107const child = element.children.item(i);108if (child) {109children.push(this.serializeElement(child, true));110}111}112}113114const { left, top } = getTopLeftOffset(element as HTMLElement);115116return {117tagName: element.tagName,118className: element.className,119textContent: element.textContent || '',120attributes,121children,122left,123top124};125}126127async getElementXY(selector: string, xoffset?: number, yoffset?: number): Promise<{ x: number; y: number }> {128const offset = typeof xoffset === 'number' && typeof yoffset === 'number' ? { x: xoffset, y: yoffset } : undefined;129return this._getElementXY(selector, offset);130}131132async typeInEditor(selector: string, text: string): Promise<void> {133// eslint-disable-next-line no-restricted-syntax134const element = mainWindow.document.querySelector(selector);135136if (!element) {137throw new Error(`Editor not found: ${selector}`);138}139if (isHTMLDivElement(element)) {140// Edit context is enabled141const editContext = element.editContext;142if (!editContext) {143throw new Error(`Edit context not found: ${selector}`);144}145const selectionStart = editContext.selectionStart;146const selectionEnd = editContext.selectionEnd;147const event = new TextUpdateEvent('textupdate', {148updateRangeStart: selectionStart,149updateRangeEnd: selectionEnd,150text,151selectionStart: selectionStart + text.length,152selectionEnd: selectionStart + text.length,153compositionStart: 0,154compositionEnd: 0155});156editContext.dispatchEvent(event);157} else if (isHTMLTextAreaElement(element)) {158const start = element.selectionStart;159const newStart = start + text.length;160const value = element.value;161const newValue = value.substr(0, start) + text + value.substr(start);162163element.value = newValue;164element.setSelectionRange(newStart, newStart);165166const event = new Event('input', { 'bubbles': true, 'cancelable': true });167element.dispatchEvent(event);168}169}170171async getEditorSelection(selector: string): Promise<{ selectionStart: number; selectionEnd: number }> {172// eslint-disable-next-line no-restricted-syntax173const element = mainWindow.document.querySelector(selector);174if (!element) {175throw new Error(`Editor not found: ${selector}`);176}177if (isHTMLDivElement(element)) {178const editContext = element.editContext;179if (!editContext) {180throw new Error(`Edit context not found: ${selector}`);181}182return { selectionStart: editContext.selectionStart, selectionEnd: editContext.selectionEnd };183} else if (isHTMLTextAreaElement(element)) {184return { selectionStart: element.selectionStart, selectionEnd: element.selectionEnd };185} else {186throw new Error(`Unknown type of element: ${selector}`);187}188}189190async getTerminalBuffer(selector: string): Promise<string[]> {191// eslint-disable-next-line no-restricted-syntax192const element = mainWindow.document.querySelector(selector);193194if (!element) {195throw new Error(`Terminal not found: ${selector}`);196}197198// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any199const xterm = (element as any).xterm;200201if (!xterm) {202throw new Error(`Xterm not found: ${selector}`);203}204205const lines: string[] = [];206for (let i = 0; i < xterm.buffer.active.length; i++) {207lines.push(xterm.buffer.active.getLine(i)!.translateToString(true));208}209210return lines;211}212213async writeInTerminal(selector: string, text: string): Promise<void> {214// eslint-disable-next-line no-restricted-syntax215const element = mainWindow.document.querySelector(selector);216217if (!element) {218throw new Error(`Element not found: ${selector}`);219}220221// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any222const xterm = (element as any).xterm as (XtermTerminal | undefined);223224if (!xterm) {225throw new Error(`Xterm not found: ${selector}`);226}227228xterm.input(text);229}230231getLocaleInfo(): Promise<ILocaleInfo> {232return Promise.resolve({233language: language,234locale: locale235});236}237238getLocalizedStrings(): Promise<ILocalizedStrings> {239return Promise.resolve({240open: localizedStrings.open,241close: localizedStrings.close,242find: localizedStrings.find243});244}245246protected async _getElementXY(selector: string, offset?: { x: number; y: number }): Promise<{ x: number; y: number }> {247// eslint-disable-next-line no-restricted-syntax248const element = mainWindow.document.querySelector(selector);249250if (!element) {251return Promise.reject(new Error(`Element not found: ${selector}`));252}253254const { left, top } = getTopLeftOffset(element as HTMLElement);255const { width, height } = getClientArea(element as HTMLElement);256let x: number, y: number;257258if (offset) {259x = left + offset.x;260y = top + offset.y;261} else {262x = left + (width / 2);263y = top + (height / 2);264}265266x = Math.round(x);267y = Math.round(y);268269return { x, y };270}271}272273export function registerWindowDriver(instantiationService: IInstantiationService): void {274Object.assign(mainWindow, { driver: instantiationService.createInstance(BrowserWindowDriver) });275}276277278