mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Add @playwright/test support
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
"src/headless/tsconfig.json",
|
||||
"test/api/tsconfig.json",
|
||||
"test/benchmark/tsconfig.json",
|
||||
"test/playwright/tsconfig.json",
|
||||
"addons/xterm-addon-attach/src/tsconfig.json",
|
||||
"addons/xterm-addon-attach/test/tsconfig.json",
|
||||
"addons/xterm-addon-canvas/src/tsconfig.json",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import WebSocket = require('ws');
|
||||
import { openTerminal, pollFor, launchBrowser } from '../../../out-test/api/TestUtils';
|
||||
import { Browser, Page } from 'playwright';
|
||||
import { Browser, Page } from '@playwright/test';
|
||||
|
||||
const APP = 'http://127.0.0.1:3001/test';
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { openTerminal, launchBrowser } from '../../../out-test/api/TestUtils';
|
||||
import { Browser, Page } from 'playwright';
|
||||
import { Browser, Page } from '@playwright/test';
|
||||
|
||||
const APP = 'http://127.0.0.1:3001/test';
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { openTerminal, launchBrowser, pollFor } from '../../../out-test/api/TestUtils';
|
||||
import { Browser, Page } from 'playwright';
|
||||
import { Browser, Page } from '@playwright/test';
|
||||
import { IImageAddonOptions } from '../src/Types';
|
||||
import { FINALIZER, introducer, sixelEncode } from 'sixel';
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
@@ -7,7 +7,7 @@ import { assert } from 'chai';
|
||||
import { readFile } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
import { openTerminal, writeSync, launchBrowser, timeout } from '../../../out-test/api/TestUtils';
|
||||
import { Browser, Page } from 'playwright';
|
||||
import { Browser, Page } from '@playwright/test';
|
||||
|
||||
const APP = 'http://127.0.0.1:3001/test';
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { openTerminal, writeSync, launchBrowser } from '../../../out-test/api/TestUtils';
|
||||
import { Browser, Page } from 'playwright';
|
||||
import { Browser, Page } from '@playwright/test';
|
||||
|
||||
const APP = 'http://127.0.0.1:3001/test';
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { openTerminal, launchBrowser } from '../../../out-test/api/TestUtils';
|
||||
import { Browser, Page } from 'playwright';
|
||||
import { Browser, Page } from '@playwright/test';
|
||||
|
||||
const APP = 'http://127.0.0.1:3001/test';
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { openTerminal, pollFor, writeSync, launchBrowser } from '../../../out-test/api/TestUtils';
|
||||
import { Browser, Page } from 'playwright';
|
||||
import { Browser, Page } from '@playwright/test';
|
||||
|
||||
const APP = 'http://127.0.0.1:3001/test';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { Browser, Page } from 'playwright';
|
||||
import { Browser, Page } from '@playwright/test';
|
||||
import { ITheme } from 'xterm';
|
||||
import { getBrowserType, launchBrowser, openTerminal, pollFor, writeSync } from '../../../out-test/api/TestUtils';
|
||||
import { ITerminalOptions } from '../../../src/common/Types';
|
||||
|
||||
+12
-5
@@ -13,7 +13,6 @@ import { Terminal } from '../out/browser/public/Terminal';
|
||||
import { AttachAddon } from '../addons/xterm-addon-attach/out/AttachAddon';
|
||||
import { CanvasAddon } from '../addons/xterm-addon-canvas/out/CanvasAddon';
|
||||
import { FitAddon } from '../addons/xterm-addon-fit/out/FitAddon';
|
||||
import { ImageAddon, IImageAddonOptions } from '../addons/xterm-addon-image/out/ImageAddon';
|
||||
import { SearchAddon, ISearchOptions } from '../addons/xterm-addon-search/out/SearchAddon';
|
||||
import { SerializeAddon } from '../addons/xterm-addon-serialize/out/SerializeAddon';
|
||||
import { WebLinksAddon } from '../addons/xterm-addon-web-links/out/WebLinksAddon';
|
||||
@@ -21,6 +20,14 @@ import { WebglAddon } from '../addons/xterm-addon-webgl/out/WebglAddon';
|
||||
import { Unicode11Addon } from '../addons/xterm-addon-unicode11/out/Unicode11Addon';
|
||||
import { LigaturesAddon } from '../addons/xterm-addon-ligatures/out/LigaturesAddon';
|
||||
|
||||
// Playwright/WebKit on Windows does not support WebAssembly https://stackoverflow.com/q/62311688/1156119
|
||||
import type { ImageAddonType, IImageAddonOptionsType } from '../addons/xterm-addon-image/out/ImageAddon';
|
||||
let ImageAddon: ImageAddonType | undefined; // eslint-disable-line @typescript-eslint/naming-convention
|
||||
if (!navigator.userAgent.includes('AppleWebKit')) {
|
||||
const imageAddon = require('../addons/xterm-addon-image/out/ImageAddon');
|
||||
ImageAddon = imageAddon.ImageAddon;
|
||||
}
|
||||
|
||||
// Use webpacked version (yarn package)
|
||||
// import { Terminal } from '../lib/xterm';
|
||||
// import { AttachAddon } from 'xterm-addon-attach';
|
||||
@@ -42,7 +49,7 @@ export interface IWindowWithTerminal extends Window {
|
||||
Terminal?: typeof TerminalType; // eslint-disable-line @typescript-eslint/naming-convention
|
||||
AttachAddon?: typeof AttachAddon; // eslint-disable-line @typescript-eslint/naming-convention
|
||||
FitAddon?: typeof FitAddon; // eslint-disable-line @typescript-eslint/naming-convention
|
||||
ImageAddon?: typeof ImageAddon; // eslint-disable-line @typescript-eslint/naming-convention
|
||||
ImageAddon?: typeof ImageAddonType; // eslint-disable-line @typescript-eslint/naming-convention
|
||||
SearchAddon?: typeof SearchAddon; // eslint-disable-line @typescript-eslint/naming-convention
|
||||
SerializeAddon?: typeof SerializeAddon; // eslint-disable-line @typescript-eslint/naming-convention
|
||||
WebLinksAddon?: typeof WebLinksAddon; // eslint-disable-line @typescript-eslint/naming-convention
|
||||
@@ -68,7 +75,7 @@ interface IDemoAddon<T extends AddonType> {
|
||||
T extends 'attach' ? typeof AttachAddon :
|
||||
T extends 'canvas' ? typeof CanvasAddon :
|
||||
T extends 'fit' ? typeof FitAddon :
|
||||
T extends 'image' ? typeof ImageAddon :
|
||||
T extends 'image' ? typeof ImageAddonType :
|
||||
T extends 'search' ? typeof SearchAddon :
|
||||
T extends 'serialize' ? typeof SerializeAddon :
|
||||
T extends 'webLinks' ? typeof WebLinksAddon :
|
||||
@@ -80,7 +87,7 @@ interface IDemoAddon<T extends AddonType> {
|
||||
T extends 'attach' ? AttachAddon :
|
||||
T extends 'canvas' ? CanvasAddon :
|
||||
T extends 'fit' ? FitAddon :
|
||||
T extends 'image' ? ImageAddon :
|
||||
T extends 'image' ? ImageAddonType :
|
||||
T extends 'search' ? SearchAddon :
|
||||
T extends 'serialize' ? SerializeAddon :
|
||||
T extends 'webLinks' ? WebLinksAddon :
|
||||
@@ -1256,7 +1263,7 @@ Test BG-colored Erase (BCE):
|
||||
|
||||
|
||||
function initImageAddonExposed(): void {
|
||||
const DEFAULT_OPTIONS: IImageAddonOptions = (addons.image.instance as any)._defaultOpts;
|
||||
const DEFAULT_OPTIONS: IImageAddonOptionsType = (addons.image.instance as any)._defaultOpts;
|
||||
const limitStorageElement = document.querySelector<HTMLInputElement>('#image-storagelimit');
|
||||
limitStorageElement.valueAsNumber = addons.image.instance.storageLimit;
|
||||
addDomListener(limitStorageElement, 'change', () => {
|
||||
|
||||
+3
-1
@@ -37,6 +37,8 @@
|
||||
"test-api-chromium": "node ./bin/test_api.js --browser=chromium --timeout=20000",
|
||||
"test-api-firefox": "node ./bin/test_api.js --browser=firefox --timeout=20000",
|
||||
"test-api-webkit": "node ./bin/test_api.js --browser=webkit --timeout=20000",
|
||||
"test-playwright": "playwright test -c ./out-test/playwright/playwright.config.js --workers 4",
|
||||
"test-playwright-debug": "playwright test -c ./out-test/playwright/playwright.config.js --headed --workers 1 --timeout 30000",
|
||||
"test-unit": "node ./bin/test.js",
|
||||
"test-unit-coverage": "node ./bin/test.js --coverage",
|
||||
"test-unit-dev": "cross-env NODE_PATH='./out' mocha",
|
||||
@@ -53,6 +55,7 @@
|
||||
"vtfeatures": "node bin/extract_vtfeatures.js src/**/*.ts src/*.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.37.1",
|
||||
"@types/chai": "^4.2.22",
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/deep-equal": "^1.0.1",
|
||||
@@ -80,7 +83,6 @@
|
||||
"mustache": "^4.2.0",
|
||||
"node-pty": "^0.10.1",
|
||||
"nyc": "^15.1.0",
|
||||
"playwright": "^1.37.1",
|
||||
"source-map-loader": "^3.0.0",
|
||||
"source-map-support": "^0.5.20",
|
||||
"ts-loader": "^9.3.1",
|
||||
|
||||
@@ -57,10 +57,14 @@ export class EventEmitter<T, U = void> implements IEventEmitter<T, U> {
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.clearListeners();
|
||||
this._disposed = true;
|
||||
}
|
||||
|
||||
public clearListeners(): void {
|
||||
if (this._listeners) {
|
||||
this._listeners.length = 0;
|
||||
}
|
||||
this._disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { pollFor, openTerminal, launchBrowser } from './TestUtils';
|
||||
import { Browser, Page } from 'playwright';
|
||||
import { Browser, Page } from '@playwright/test';
|
||||
|
||||
const APP = 'http://127.0.0.1:3001/test';
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { pollFor, openTerminal, getBrowserType, launchBrowser, writeSync } from './TestUtils';
|
||||
import { Browser, Page } from 'playwright';
|
||||
import { Browser, Page } from '@playwright/test';
|
||||
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
|
||||
const APP = 'http://127.0.0.1:3001/test';
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { pollFor, writeSync, openTerminal, getBrowserType, launchBrowser } from './TestUtils';
|
||||
import { Browser, Page } from 'playwright';
|
||||
import { Browser, Page } from '@playwright/test';
|
||||
|
||||
const APP = 'http://127.0.0.1:3001/test';
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { writeSync, openTerminal, launchBrowser } from './TestUtils';
|
||||
import { Browser, Page } from 'playwright';
|
||||
import { Browser, Page } from '@playwright/test';
|
||||
|
||||
const APP = 'http://127.0.0.1:3001/test';
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { pollFor, timeout, writeSync, openTerminal, launchBrowser } from './TestUtils';
|
||||
import { Browser, Page } from 'playwright';
|
||||
import { Browser, Page } from '@playwright/test';
|
||||
import { fail } from 'assert';
|
||||
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import * as playwright from 'playwright';
|
||||
import * as playwright from '@playwright/test';
|
||||
import deepEqual = require('deep-equal');
|
||||
import { ITerminalInitOnlyOptions, ITerminalOptions } from 'xterm';
|
||||
import { deepStrictEqual, fail } from 'assert';
|
||||
import { deepStrictEqual } from 'assert';
|
||||
|
||||
export async function pollFor<T>(page: playwright.Page, evalOrFn: string | (() => Promise<T>), val: T, preFn?: () => Promise<void>, maxDuration?: number): Promise<void> {
|
||||
if (preFn) {
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { test } from '@playwright/test';
|
||||
import { createTestContext, ITestContext, openTerminal, pollFor } from './TestUtils';
|
||||
|
||||
let ctx: ITestContext;
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
ctx = await createTestContext(browser);
|
||||
await openTerminal(ctx);
|
||||
});
|
||||
test.afterAll(async () => await ctx.page.close());
|
||||
|
||||
test.beforeEach(async () => await ctx.proxy.reset());
|
||||
|
||||
test.describe('CharWidth Integration Tests', function(): void {
|
||||
test.describe('getStringCellWidth', () => {
|
||||
test('ASCII chars', async () => {
|
||||
await ctx.proxy.write('This is just ASCII text.#');
|
||||
await pollFor(ctx.page, () => sumWidths(0, 1, '#'), 25);
|
||||
});
|
||||
|
||||
test('combining chars', async () => {
|
||||
await ctx.proxy.write('e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301#');
|
||||
await pollFor(ctx.page, () => sumWidths(0, 1, '#'), 10);
|
||||
});
|
||||
|
||||
test('surrogate chars', async () => {
|
||||
await ctx.proxy.write('𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞#');
|
||||
await pollFor(ctx.page, () => sumWidths(0, 1, '#'), 28);
|
||||
});
|
||||
|
||||
test('surrogate combining chars', async () => {
|
||||
await ctx.proxy.write('𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301#');
|
||||
await pollFor(ctx.page, () => sumWidths(0, 1, '#'), 12);
|
||||
});
|
||||
|
||||
test('fullwidth chars', async () => {
|
||||
await ctx.proxy.write('1234567890#');
|
||||
await pollFor(ctx.page, () => sumWidths(0, 1, '#'), 21);
|
||||
});
|
||||
|
||||
test('fullwidth chars offset 1', async () => {
|
||||
await ctx.proxy.write('a1234567890#');
|
||||
await pollFor(ctx.page, () => sumWidths(0, 1, '#'), 22);
|
||||
});
|
||||
|
||||
// TODO: multiline tests once #1685 is resolved
|
||||
});
|
||||
});
|
||||
|
||||
async function sumWidths(start: number, end: number, sentinel: string): Promise<number> {
|
||||
await ctx.page.evaluate(`
|
||||
(function() {
|
||||
window.result = 0;
|
||||
const buffer = window.term.buffer.active;
|
||||
for (let i = ${start}; i < ${end}; i++) {
|
||||
const line = buffer.getLine(i);
|
||||
let j = 0;
|
||||
while (true) {
|
||||
const cell = line.getCell(j++);
|
||||
if (!cell) {
|
||||
break;
|
||||
}
|
||||
window.result += cell.getWidth();
|
||||
if (cell.getChars() === '${sentinel}') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
`);
|
||||
return await ctx.page.evaluate(`window.result`);
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Browser, JSHandle, Page } from '@playwright/test';
|
||||
import { deepStrictEqual } from 'assert';
|
||||
import type { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
import type { IRenderService } from 'browser/services/Services';
|
||||
import type { ICoreTerminal, IMarker } from 'common/Types';
|
||||
import * as playwright from '@playwright/test';
|
||||
import { PageFunction } from 'playwright-core/types/structs';
|
||||
import { IBuffer, IBufferCell, IBufferLine, IBufferRange, IModes, ITerminalOptions, Terminal } from 'xterm';
|
||||
import { EventEmitter } from '../../out/common/EventEmitter';
|
||||
// TODO: We could avoid needing this
|
||||
import deepEqual = require('deep-equal');
|
||||
|
||||
export interface ITestContext {
|
||||
page: Page;
|
||||
termHandle: JSHandle<Terminal>;
|
||||
proxy: TerminalProxy;
|
||||
}
|
||||
|
||||
export async function createTestContext(browser: Browser): Promise<ITestContext> {
|
||||
const page = await browser.newPage();
|
||||
page.on('console', e => console.log(`[${browser.browserType().name()}:${e.type()}]`, e));
|
||||
page.on('pageerror', e => console.error(`[${browser.browserType().name()}]`, e));
|
||||
await page.goto('/test');
|
||||
const proxy = new TerminalProxy(page);
|
||||
proxy.initPage();
|
||||
return {
|
||||
page,
|
||||
termHandle: await page.evaluateHandle('window.term'),
|
||||
proxy
|
||||
};
|
||||
}
|
||||
|
||||
type EnsureAsync<T> = T extends PromiseLike<any> ? T : Promise<T>;
|
||||
type EnsureAsyncProperties<T> = {
|
||||
[Key in keyof T]: EnsureAsync<T[Key]>
|
||||
};
|
||||
|
||||
interface ITerminalProxy extends
|
||||
// Interfaces that the proxy implements with async values
|
||||
EnsureAsyncProperties<Pick<Terminal, 'cols' | 'rows'>> {
|
||||
|
||||
// Custom proxy methods
|
||||
evaluate<T>(pageFunction: PageFunction<JSHandle<Terminal>[], T>): Promise<T>;
|
||||
write(data: string | Uint8Array): Promise<void>;
|
||||
}
|
||||
|
||||
export class TerminalProxy implements ITerminalProxy {
|
||||
constructor(private readonly _page: Page) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the proxy for a new playwright page.
|
||||
*/
|
||||
public async initPage(): Promise<void> {
|
||||
await this._page.exposeFunction('onBell', () => this._onBell.fire());
|
||||
await this._page.exposeFunction('onBinary', (e: string) => this._onBinary.fire(e));
|
||||
await this._page.exposeFunction('onCursorMove', () => this._onCursorMove.fire());
|
||||
await this._page.exposeFunction('onData', (e: string) => this._onData.fire(e));
|
||||
await this._page.exposeFunction('onKey', (e: { key: string, domEvent: KeyboardEvent }) => this._onKey.fire(e));
|
||||
await this._page.exposeFunction('onLineFeed', () => this._onLineFeed.fire());
|
||||
await this._page.exposeFunction('onRender', (e: { start: number, end: number }) => this._onRender.fire(e));
|
||||
await this._page.exposeFunction('onResize', (e: { cols: number, rows: number }) => this._onResize.fire(e));
|
||||
await this._page.exposeFunction('onScroll', (e: number) => this._onScroll.fire(e));
|
||||
await this._page.exposeFunction('onSelectionChange', () => this._onSelectionChange.fire());
|
||||
await this._page.exposeFunction('onTitleChange', (e: string) => this._onTitleChange.fire(e));
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the proxy for a new terminal object.
|
||||
*/
|
||||
public async initTerm(): Promise<void> {
|
||||
for (const emitter of [
|
||||
this._onBell,
|
||||
this._onBinary,
|
||||
this._onCursorMove,
|
||||
this._onData,
|
||||
this._onKey,
|
||||
this._onLineFeed,
|
||||
this._onRender,
|
||||
this._onResize,
|
||||
this._onScroll,
|
||||
this._onSelectionChange,
|
||||
this._onTitleChange
|
||||
]) {
|
||||
emitter.clearListeners();
|
||||
}
|
||||
await this.evaluate(([term]) => term.onBell((window as any).onBell));
|
||||
await this.evaluate(([term]) => term.onBinary((window as any).onBinary));
|
||||
await this.evaluate(([term]) => term.onCursorMove((window as any).onCursorMove));
|
||||
await this.evaluate(([term]) => term.onData((window as any).onData));
|
||||
await this.evaluate(([term]) => term.onKey((window as any).onKey));
|
||||
await this.evaluate(([term]) => term.onLineFeed((window as any).onLineFeed));
|
||||
await this.evaluate(([term]) => term.onRender((window as any).onRender));
|
||||
await this.evaluate(([term]) => term.onResize((window as any).onResize));
|
||||
await this.evaluate(([term]) => term.onScroll((window as any).onScroll));
|
||||
await this.evaluate(([term]) => term.onSelectionChange((window as any).onSelectionChange));
|
||||
await this.evaluate(([term]) => term.onTitleChange((window as any).onTitleChange));
|
||||
}
|
||||
|
||||
// #region Events
|
||||
private _onBell = new EventEmitter<void>();
|
||||
public readonly onBell = this._onBell.event;
|
||||
private _onBinary = new EventEmitter<string>();
|
||||
public readonly onBinary = this._onBinary.event;
|
||||
private _onCursorMove = new EventEmitter<void>();
|
||||
public readonly onCursorMove = this._onCursorMove.event;
|
||||
private _onData = new EventEmitter<string>();
|
||||
public readonly onData = this._onData.event;
|
||||
private _onKey = new EventEmitter<{ key: string, domEvent: KeyboardEvent }>();
|
||||
public readonly onKey = this._onKey.event;
|
||||
private _onLineFeed = new EventEmitter<void>();
|
||||
public readonly onLineFeed = this._onLineFeed.event;
|
||||
private _onRender = new EventEmitter<{ start: number, end: number }>();
|
||||
public readonly onRender = this._onRender.event;
|
||||
private _onResize = new EventEmitter<{ cols: number, rows: number }>();
|
||||
public readonly onResize = this._onResize.event;
|
||||
private _onScroll = new EventEmitter<number>();
|
||||
public readonly onScroll = this._onScroll.event;
|
||||
private _onSelectionChange = new EventEmitter<void>();
|
||||
public readonly onSelectionChange = this._onSelectionChange.event;
|
||||
private _onTitleChange = new EventEmitter<string>();
|
||||
public readonly onTitleChange = this._onTitleChange.event;
|
||||
// #endregion
|
||||
|
||||
// #region Simple properties
|
||||
public get cols(): Promise<number> { return this.evaluate(([term]) => term.cols); }
|
||||
public get rows(): Promise<number> { return this.evaluate(([term]) => term.rows); }
|
||||
public get modes(): Promise<IModes> { return this.evaluate(([term]) => term.modes); }
|
||||
// #endregion
|
||||
|
||||
// #region Complex properties
|
||||
public get buffer(): TerminalBufferNamespaceProxy { return new TerminalBufferNamespaceProxy(this._page, this); }
|
||||
/**
|
||||
* Exposes somewhat unsafe access to internals for testing things difficult to do with the regular
|
||||
* API
|
||||
*/
|
||||
public get core(): TerminalCoreProxy { return new TerminalCoreProxy(this._page, this); }
|
||||
// #endregion
|
||||
|
||||
// #region Proxied methods
|
||||
public async dispose(): Promise<void> { return this.evaluate(([term]) => term.dispose()); }
|
||||
public async reset(): Promise<void> { return this.evaluate(([term]) => term.reset()); }
|
||||
public async clear(): Promise<void> { return this.evaluate(([term]) => term.clear()); }
|
||||
public async focus(): Promise<void> { return this.evaluate(([term]) => term.focus()); }
|
||||
public async blur(): Promise<void> { return this.evaluate(([term]) => term.blur()); }
|
||||
public async hasSelection(): Promise<boolean> { return this.evaluate(([term]) => term.hasSelection()); }
|
||||
public async getSelection(): Promise<string> { return this.evaluate(([term]) => term.getSelection()); }
|
||||
public async getSelectionPosition(): Promise<IBufferRange | undefined> { return this.evaluate(([term]) => term.getSelectionPosition()); }
|
||||
public async selectAll(): Promise<void> { return this.evaluate(([term]) => term.selectAll()); }
|
||||
public async clearSelection(): Promise<void> { return this.evaluate(([term]) => term.clearSelection()); }
|
||||
public async select(column: number, row: number, length: number): Promise<void> { return this._page.evaluate(([term, column, row, length]) => term.select(column, row, length), [await this.getHandle(), column, row, length] as const); }
|
||||
public async paste(data: string): Promise<void> { return this._page.evaluate(([term, data]) => term.paste(data), [await this.getHandle(), data] as const); }
|
||||
public async getOption<T extends keyof ITerminalOptions>(key: T): Promise<ITerminalOptions[T]> { return this._page.evaluate(([term, key]) => term.options[key as T], [await this.getHandle(), key] as const); }
|
||||
public async setOption<T extends keyof ITerminalOptions>(key: T, value: ITerminalOptions[T]): Promise<any> { return this._page.evaluate(([term, key, value]) => term.options[key as T] = (value as ITerminalOptions[T]), [await this.getHandle(), key, value] as const); }
|
||||
public async setOptions(value: Partial<ITerminalOptions>): Promise<any> {
|
||||
return this._page.evaluate(([term, value]) => {
|
||||
term.options = value;
|
||||
}, [await this.getHandle(), value] as const);
|
||||
}
|
||||
public async scrollToTop(): Promise<void> { return this.evaluate(([term]) => term.scrollToTop()); }
|
||||
public async scrollToBottom(): Promise<void> { return this.evaluate(([term]) => term.scrollToBottom()); }
|
||||
public async scrollPages(pageCount: number): Promise<void> { return this._page.evaluate(([term, pageCount]) => term.scrollPages(pageCount), [await this.getHandle(), pageCount] as const); }
|
||||
public async scrollToLine(line: number): Promise<void> { return this._page.evaluate(([term, line]) => term.scrollToLine(line), [await this.getHandle(), line] as const); }
|
||||
public async scrollLines(amount: number): Promise<void> { return this._page.evaluate(([term, amount]) => term.scrollLines(amount), [await this.getHandle(), amount] as const); }
|
||||
public async write(data: string | Uint8Array): Promise<void> {
|
||||
return this._page.evaluate(([term, data]) => {
|
||||
return new Promise(r => term.write(typeof data === 'string' ? data : new Uint8Array(data), r));
|
||||
}, [await this.getHandle(), typeof data === 'string' ? data : Array.from(data)] as const);
|
||||
}
|
||||
public async writeln(data: string | Uint8Array): Promise<void> {
|
||||
return this._page.evaluate(([term, data]) => {
|
||||
return new Promise(r => term.writeln(typeof data === 'string' ? data : new Uint8Array(data), r));
|
||||
}, [await this.getHandle(), typeof data === 'string' ? data : Array.from(data)] as const);
|
||||
}
|
||||
public async resize(cols: number, rows: number): Promise<void> { return this._page.evaluate(([term, cols, rows]) => term.resize(cols, rows), [await this.getHandle(), cols, rows] as const); }
|
||||
public async registerMarker(y: number): Promise<IMarker | undefined> { return this._page.evaluate(([term, y]) => term.registerMarker(y), [await this.getHandle(), y] as const); }
|
||||
// #endregion
|
||||
|
||||
public async evaluate<T>(pageFunction: PageFunction<JSHandle<Terminal>[], T>): Promise<T> {
|
||||
return this._page.evaluate(pageFunction, [await this.getHandle()]);
|
||||
}
|
||||
|
||||
public async evaluateHandle<T>(pageFunction: PageFunction<JSHandle<Terminal>[], T>): Promise<JSHandle<T>> {
|
||||
return this._page.evaluateHandle(pageFunction, [await this.getHandle()]);
|
||||
}
|
||||
|
||||
public async getHandle(): Promise<JSHandle<Terminal>> {
|
||||
// This is async because it must be evaluated each time it is called since term may have changed
|
||||
return this._page.evaluateHandle('window.term');
|
||||
}
|
||||
}
|
||||
|
||||
class TerminalBufferNamespaceProxy {
|
||||
constructor(
|
||||
private readonly _page: Page,
|
||||
private readonly _proxy: TerminalProxy
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
public get active(): TerminalBufferProxy { return new TerminalBufferProxy(this._page, this._proxy, this._proxy.evaluateHandle(([term]) => term.buffer.active)); }
|
||||
public get normal(): TerminalBufferProxy { return new TerminalBufferProxy(this._page, this._proxy, this._proxy.evaluateHandle(([term]) => term.buffer.normal)); }
|
||||
public get alternate(): TerminalBufferProxy { return new TerminalBufferProxy(this._page, this._proxy, this._proxy.evaluateHandle(([term]) => term.buffer.alternate)); }
|
||||
}
|
||||
|
||||
class TerminalBufferProxy {
|
||||
constructor(
|
||||
private readonly _page: Page,
|
||||
private readonly _proxy: TerminalProxy,
|
||||
private readonly _handle: Promise<JSHandle<IBuffer>>
|
||||
) {
|
||||
}
|
||||
|
||||
public get type(): Promise<'normal' | 'alternate'> { return this.evaluate(([buffer]) => buffer.type); }
|
||||
public get cursorY(): Promise<number> { return this.evaluate(([buffer]) => buffer.cursorY); }
|
||||
public get cursorX(): Promise<number> { return this.evaluate(([buffer]) => buffer.cursorX); }
|
||||
public get viewportY(): Promise<number> { return this.evaluate(([buffer]) => buffer.viewportY); }
|
||||
public get baseY(): Promise<number> { return this.evaluate(([buffer]) => buffer.baseY); }
|
||||
public get length(): Promise<number> { return this.evaluate(([buffer]) => buffer.length); }
|
||||
public async getLine(y: number): Promise<TerminalBufferLine | undefined> {
|
||||
const lineHandle = await this._page.evaluateHandle(([buffer, y]) => buffer.getLine(y), [await this._handle, y] as const);
|
||||
const value = await lineHandle.jsonValue();
|
||||
if (value) {
|
||||
return new TerminalBufferLine(this._page, lineHandle as JSHandle<IBufferLine>);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public async evaluate<T>(pageFunction: PageFunction<JSHandle<IBuffer>[], T>): Promise<T> {
|
||||
return this._page.evaluate(pageFunction, [await this._handle]);
|
||||
}
|
||||
}
|
||||
|
||||
class TerminalBufferLine {
|
||||
constructor(
|
||||
private readonly _page: Page,
|
||||
private readonly _handle: JSHandle<IBufferLine>
|
||||
) {
|
||||
}
|
||||
|
||||
public get length(): Promise<number> { return this.evaluate(([bufferLine]) => bufferLine.length); }
|
||||
public get isWrapped(): Promise<boolean> { return this.evaluate(([bufferLine]) => bufferLine.isWrapped); }
|
||||
|
||||
public translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): Promise<string> {
|
||||
return this._page.evaluate(([bufferLine, trimRight, startColumn, endColumn]) => {
|
||||
return bufferLine.translateToString(trimRight, startColumn, endColumn);
|
||||
}, [this._handle, trimRight, startColumn, endColumn] as const);
|
||||
}
|
||||
|
||||
public async getCell(x: number): Promise<TerminalBufferCell | undefined> {
|
||||
const cellHandle = await this._page.evaluateHandle(([bufferLine, x]) => bufferLine.getCell(x), [this._handle, x] as const);
|
||||
const value = await cellHandle.jsonValue();
|
||||
if (value) {
|
||||
return new TerminalBufferCell(this._page, cellHandle as JSHandle<IBufferCell>);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public async evaluate<T>(pageFunction: PageFunction<JSHandle<IBufferLine>[], T>): Promise<T> {
|
||||
return this._page.evaluate(pageFunction, [this._handle]);
|
||||
}
|
||||
}
|
||||
|
||||
class TerminalBufferCell {
|
||||
constructor(
|
||||
private readonly _page: Page,
|
||||
private readonly _handle: JSHandle<IBufferCell>
|
||||
) {
|
||||
}
|
||||
|
||||
public getWidth(): Promise<number> { return this.evaluate(([line]) => line.getWidth()); }
|
||||
public getChars(): Promise<string> { return this.evaluate(([line]) => line.getChars()); }
|
||||
|
||||
public async evaluate<T>(pageFunction: PageFunction<JSHandle<IBufferCell>[], T>): Promise<T> {
|
||||
return this._page.evaluate(pageFunction, [this._handle]);
|
||||
}
|
||||
}
|
||||
|
||||
class TerminalCoreProxy {
|
||||
constructor(
|
||||
private readonly _page: Page,
|
||||
private readonly _proxy: TerminalProxy
|
||||
) {
|
||||
}
|
||||
|
||||
public get isDisposed(): Promise<boolean> { return this.evaluate(([core]) => (core as any)._isDisposed); }
|
||||
public get renderDimensions(): Promise<IRenderDimensions> { return this.evaluate(([core]) => ((core as any)._renderService as IRenderService).dimensions); }
|
||||
|
||||
public async triggerBinaryEvent(data: string): Promise<void> {
|
||||
return this._page.evaluate(([core, data]) => core.coreService.triggerBinaryEvent(data), [await this._getCoreHandle(), data] as const);
|
||||
}
|
||||
|
||||
|
||||
private async _getCoreHandle(): Promise<JSHandle<ICoreTerminal>> {
|
||||
return this._proxy.evaluateHandle(([term]) => (term as any)._core as ICoreTerminal);
|
||||
}
|
||||
|
||||
public async evaluate<T>(pageFunction: PageFunction<JSHandle<ICoreTerminal>[], T>): Promise<T> {
|
||||
return this._page.evaluate(pageFunction, [await this._getCoreHandle()]);
|
||||
}
|
||||
}
|
||||
|
||||
export async function openTerminal(ctx: ITestContext, options: ITerminalOptions = {}): Promise<void> {
|
||||
await ctx.page.evaluate(`
|
||||
if ('term' in window) {
|
||||
window.term.dispose();
|
||||
}
|
||||
window.term = new Terminal(${JSON.stringify(options)});
|
||||
window.term.open(document.querySelector('#terminal-container'));
|
||||
`);
|
||||
await ctx.page.waitForSelector('.xterm-rows');
|
||||
ctx.termHandle = await ctx.page.evaluateHandle('window.term');
|
||||
await ctx.proxy.initTerm();
|
||||
}
|
||||
|
||||
|
||||
|
||||
export async function pollFor<T>(page: playwright.Page, evalOrFn: string | (() => Promise<T>), val: T, preFn?: () => Promise<void>, maxDuration?: number): Promise<void> {
|
||||
if (preFn) {
|
||||
await preFn();
|
||||
}
|
||||
const result = typeof evalOrFn === 'string' ? await page.evaluate(evalOrFn) : await evalOrFn();
|
||||
|
||||
if (process.env.DEBUG) {
|
||||
console.log('pollFor result: ', result);
|
||||
}
|
||||
|
||||
if (!deepEqual(result, val)) {
|
||||
if (maxDuration === undefined) {
|
||||
maxDuration = 2000;
|
||||
}
|
||||
if (maxDuration <= 0) {
|
||||
deepStrictEqual(result, val, 'pollFor max duration exceeded');
|
||||
}
|
||||
return new Promise<void>(r => {
|
||||
setTimeout(() => r(pollFor(page, evalOrFn, val, preFn, maxDuration! - 10)), 10);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeSync(page: playwright.Page, data: string): Promise<void> {
|
||||
await page.evaluate(`
|
||||
window.ready = false;
|
||||
window.term.write('${data}', () => window.ready = true);
|
||||
`);
|
||||
await pollFor(page, 'window.ready', true);
|
||||
}
|
||||
|
||||
export async function timeout(ms: number): Promise<void> {
|
||||
return new Promise<void>(r => setTimeout(r, ms));
|
||||
}
|
||||
|
||||
export function getBrowserType(): playwright.BrowserType<playwright.WebKitBrowser> | playwright.BrowserType<playwright.ChromiumBrowser> | playwright.BrowserType<playwright.FirefoxBrowser> {
|
||||
// Default to chromium
|
||||
let browserType: playwright.BrowserType<playwright.WebKitBrowser> | playwright.BrowserType<playwright.ChromiumBrowser> | playwright.BrowserType<playwright.FirefoxBrowser> = playwright['chromium'];
|
||||
|
||||
const index = process.argv.indexOf('--browser');
|
||||
if (index !== -1 && process.argv.length > index + 1 && typeof process.argv[index + 1] === 'string') {
|
||||
const string = process.argv[index + 1];
|
||||
if (string === 'firefox' || string === 'webkit') {
|
||||
browserType = playwright[string];
|
||||
}
|
||||
}
|
||||
|
||||
return browserType;
|
||||
}
|
||||
|
||||
export function launchBrowser(): Promise<playwright.Browser> {
|
||||
const browserType = getBrowserType();
|
||||
const options: Record<string, unknown> = {
|
||||
headless: process.argv.includes('--headless')
|
||||
};
|
||||
|
||||
const index = process.argv.indexOf('--executablePath');
|
||||
if (index > 0 && process.argv.length > index + 1 && typeof process.argv[index + 1] === 'string') {
|
||||
options.executablePath = process.argv[index + 1];
|
||||
}
|
||||
|
||||
return browserType.launch(options);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user