From 9e768a4c7ccf099fc502330d8ebaf3ea508f00f1 Mon Sep 17 00:00:00 2001 From: tisilent Date: Fri, 25 Aug 2023 18:37:48 +0800 Subject: [PATCH 01/40] Update circle line custom powerline glyph. --- src/browser/renderer/shared/CustomGlyphs.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/browser/renderer/shared/CustomGlyphs.ts b/src/browser/renderer/shared/CustomGlyphs.ts index cface515..d0d9a528 100644 --- a/src/browser/renderer/shared/CustomGlyphs.ts +++ b/src/browser/renderer/shared/CustomGlyphs.ts @@ -366,11 +366,11 @@ export const powerlineDefinitions: { [index: string]: IVectorShape } = { // Right semi-circle solid '\u{E0B4}': { d: 'M0,0 L0,1 C0.552,1,1,0.776,1,.5 C1,0.224,0.552,0,0,0', type: VectorType.FILL, rightPadding: 1 }, // Right semi-circle line - '\u{E0B5}': { d: 'M0,1 C0.552,1,1,0.776,1,.5 C1,0.224,0.552,0,0,0', type: VectorType.STROKE, rightPadding: 1 }, + '\u{E0B5}': { d: 'M.4,1 C.622,1,1,.676,.88,.5 C1,.324,0.622,0,.4,0', type: VectorType.STROKE, rightPadding: 1 }, // Left semi-circle solid '\u{E0B6}': { d: 'M1,0 L1,1 C0.448,1,0,0.776,0,.5 C0,0.224,0.448,0,1,0', type: VectorType.FILL, leftPadding: 1 }, // Left semi-circle line - '\u{E0B7}': { d: 'M1,1 C0.448,1,0,0.776,0,.5 C0,0.224,0.448,0,1,0', type: VectorType.STROKE, leftPadding: 1 }, + '\u{E0B7}': { d: 'M.6,1 C0.378,1,0,.676,.12,.5 C0,0.324,0.378,0,0.6,0', type: VectorType.STROKE, leftPadding: 1 }, // Lower left triangle '\u{E0B8}': { d: 'M-.5,-.5 L1.5,1.5 L-.5,1.5', type: VectorType.FILL }, // Backslash separator From 2b7b4d6feaaab69874b5245b4dd3c889c0718f31 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 27 Aug 2023 07:31:20 -0700 Subject: [PATCH 02/40] Upgrade playwright --- package.json | 2 +- yarn.lock | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 70214933..7ad87d81 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "mustache": "^4.2.0", "node-pty": "^0.10.1", "nyc": "^15.1.0", - "playwright": "^1.22.1", + "playwright": "^1.37.1", "source-map-loader": "^3.0.0", "source-map-support": "^0.5.20", "ts-loader": "^9.3.1", diff --git a/yarn.lock b/yarn.lock index 4b3a34ab..206445f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3061,17 +3061,17 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -playwright-core@1.36.2: - version "1.36.2" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.36.2.tgz#32382f2d96764c24c65a86ea336cf79721c2e50e" - integrity sha512-sQYZt31dwkqxOrP7xy2ggDfEzUxM1lodjhsQ3NMMv5uGTRDsLxU0e4xf4wwMkF2gplIxf17QMBCodSFgm6bFVQ== +playwright-core@1.37.1: + version "1.37.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.37.1.tgz#cb517d52e2e8cb4fa71957639f1cd105d1683126" + integrity sha512-17EuQxlSIYCmEMwzMqusJ2ztDgJePjrbttaefgdsiqeLWidjYz9BxXaTaZWxH1J95SHGk6tjE+dwgWILJoUZfA== -playwright@^1.22.1: - version "1.36.2" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.36.2.tgz#4f25272209a31ad1138d7b9868e0325159f003e3" - integrity sha512-4Fmlq3KWsl85Bl4InJw1NC21aeQV0iSZuFvTDcy1F8zVmXmgQRe89GxF8zMSRt/KIS+2tUolak7EXVl9aC+JdA== +playwright@^1.37.1: + version "1.37.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.37.1.tgz#6e488d82d7d98b9127c5db9c701f9c956ab47e76" + integrity sha512-bgUXRrQKhT48zHdxDYQTpf//0xDfDd5hLeEhjuSw8rXEGoT9YeElpfvs/izonTNY21IQZ7d3s22jLxYaAnubbQ== dependencies: - playwright-core "1.36.2" + playwright-core "1.37.1" prelude-ls@^1.2.1: version "1.2.1" From fbebabe2e62217b28a4b7f86c2f1afc7864c2f8d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 27 Aug 2023 08:08:42 -0700 Subject: [PATCH 03/40] Add @playwright/test support --- .eslintrc.json | 1 + .../test/AttachAddon.api.ts | 2 +- addons/xterm-addon-fit/test/FitAddon.api.ts | 2 +- .../xterm-addon-image/test/ImageAddon.api.ts | 2 +- .../test/SearchAddon.api.ts | 2 +- .../test/SerializeAddon.api.ts | 2 +- .../test/Unicode11Addon.api.ts | 2 +- .../test/WebLinksAddon.api.ts | 2 +- .../test/WebglRenderer.api.ts | 2 +- demo/client.ts | 17 +- package.json | 4 +- src/common/EventEmitter.ts | 6 +- test/api/CharWidth.api.ts | 2 +- test/api/InputHandler.api.ts | 2 +- test/api/MouseTracking.api.ts | 2 +- test/api/Parser.api.ts | 2 +- test/api/Terminal.api.ts | 2 +- test/api/TestUtils.ts | 4 +- test/playwright/CharWidth.test.ts | 76 ++++ test/playwright/TestUtils.ts | 385 ++++++++++++++++++ test/playwright/playwright.config.ts | 34 ++ test/playwright/tsconfig.json | 42 ++ tsconfig.all.json | 1 + yarn.lock | 19 +- 24 files changed, 585 insertions(+), 30 deletions(-) create mode 100644 test/playwright/CharWidth.test.ts create mode 100644 test/playwright/TestUtils.ts create mode 100644 test/playwright/playwright.config.ts create mode 100644 test/playwright/tsconfig.json diff --git a/.eslintrc.json b/.eslintrc.json index 9287da5c..0626a446 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -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", diff --git a/addons/xterm-addon-attach/test/AttachAddon.api.ts b/addons/xterm-addon-attach/test/AttachAddon.api.ts index a4827656..230b9e2e 100644 --- a/addons/xterm-addon-attach/test/AttachAddon.api.ts +++ b/addons/xterm-addon-attach/test/AttachAddon.api.ts @@ -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'; diff --git a/addons/xterm-addon-fit/test/FitAddon.api.ts b/addons/xterm-addon-fit/test/FitAddon.api.ts index 36b44211..5611972e 100644 --- a/addons/xterm-addon-fit/test/FitAddon.api.ts +++ b/addons/xterm-addon-fit/test/FitAddon.api.ts @@ -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'; diff --git a/addons/xterm-addon-image/test/ImageAddon.api.ts b/addons/xterm-addon-image/test/ImageAddon.api.ts index 5b43eb35..43334bbb 100644 --- a/addons/xterm-addon-image/test/ImageAddon.api.ts +++ b/addons/xterm-addon-image/test/ImageAddon.api.ts @@ -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'; diff --git a/addons/xterm-addon-search/test/SearchAddon.api.ts b/addons/xterm-addon-search/test/SearchAddon.api.ts index 0c20084a..92fd6b29 100644 --- a/addons/xterm-addon-search/test/SearchAddon.api.ts +++ b/addons/xterm-addon-search/test/SearchAddon.api.ts @@ -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'; diff --git a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts index 522d3010..c943e7b2 100644 --- a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts +++ b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts @@ -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'; diff --git a/addons/xterm-addon-unicode11/test/Unicode11Addon.api.ts b/addons/xterm-addon-unicode11/test/Unicode11Addon.api.ts index 4c695b00..c7f3588f 100644 --- a/addons/xterm-addon-unicode11/test/Unicode11Addon.api.ts +++ b/addons/xterm-addon-unicode11/test/Unicode11Addon.api.ts @@ -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'; diff --git a/addons/xterm-addon-web-links/test/WebLinksAddon.api.ts b/addons/xterm-addon-web-links/test/WebLinksAddon.api.ts index a086e1d3..fb5be20b 100644 --- a/addons/xterm-addon-web-links/test/WebLinksAddon.api.ts +++ b/addons/xterm-addon-web-links/test/WebLinksAddon.api.ts @@ -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'; diff --git a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts index 5d9cc0f0..7db2de26 100644 --- a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts @@ -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'; diff --git a/demo/client.ts b/demo/client.ts index d0b79c68..c643100e 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -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 '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 '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('#image-storagelimit'); limitStorageElement.valueAsNumber = addons.image.instance.storageLimit; addDomListener(limitStorageElement, 'change', () => { diff --git a/package.json b/package.json index 7ad87d81..89bd072e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/common/EventEmitter.ts b/src/common/EventEmitter.ts index 4684809f..fd959042 100644 --- a/src/common/EventEmitter.ts +++ b/src/common/EventEmitter.ts @@ -57,10 +57,14 @@ export class EventEmitter implements IEventEmitter { } public dispose(): void { + this.clearListeners(); + this._disposed = true; + } + + public clearListeners(): void { if (this._listeners) { this._listeners.length = 0; } - this._disposed = true; } } diff --git a/test/api/CharWidth.api.ts b/test/api/CharWidth.api.ts index 83067149..43d5ab20 100644 --- a/test/api/CharWidth.api.ts +++ b/test/api/CharWidth.api.ts @@ -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'; diff --git a/test/api/InputHandler.api.ts b/test/api/InputHandler.api.ts index c894a4d8..4059edae 100644 --- a/test/api/InputHandler.api.ts +++ b/test/api/InputHandler.api.ts @@ -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'; diff --git a/test/api/MouseTracking.api.ts b/test/api/MouseTracking.api.ts index 8a64a59a..84b4402f 100644 --- a/test/api/MouseTracking.api.ts +++ b/test/api/MouseTracking.api.ts @@ -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'; diff --git a/test/api/Parser.api.ts b/test/api/Parser.api.ts index ada28adf..94d4a54a 100644 --- a/test/api/Parser.api.ts +++ b/test/api/Parser.api.ts @@ -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'; diff --git a/test/api/Terminal.api.ts b/test/api/Terminal.api.ts index 01716223..d0ba1dd6 100644 --- a/test/api/Terminal.api.ts +++ b/test/api/Terminal.api.ts @@ -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'; diff --git a/test/api/TestUtils.ts b/test/api/TestUtils.ts index 3fd0648e..702fcb2d 100644 --- a/test/api/TestUtils.ts +++ b/test/api/TestUtils.ts @@ -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(page: playwright.Page, evalOrFn: string | (() => Promise), val: T, preFn?: () => Promise, maxDuration?: number): Promise { if (preFn) { diff --git a/test/playwright/CharWidth.test.ts b/test/playwright/CharWidth.test.ts new file mode 100644 index 00000000..fbc06697 --- /dev/null +++ b/test/playwright/CharWidth.test.ts @@ -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 { + 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`); +} diff --git a/test/playwright/TestUtils.ts b/test/playwright/TestUtils.ts new file mode 100644 index 00000000..1fb54178 --- /dev/null +++ b/test/playwright/TestUtils.ts @@ -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; + proxy: TerminalProxy; +} + +export async function createTestContext(browser: Browser): Promise { + 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 extends PromiseLike ? T : Promise; +type EnsureAsyncProperties = { + [Key in keyof T]: EnsureAsync +}; + +interface ITerminalProxy extends + // Interfaces that the proxy implements with async values + EnsureAsyncProperties> { + + // Custom proxy methods + evaluate(pageFunction: PageFunction[], T>): Promise; + write(data: string | Uint8Array): Promise; +} + +export class TerminalProxy implements ITerminalProxy { + constructor(private readonly _page: Page) { + } + + /** + * Initialize the proxy for a new playwright page. + */ + public async initPage(): Promise { + 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 { + 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(); + public readonly onBell = this._onBell.event; + private _onBinary = new EventEmitter(); + public readonly onBinary = this._onBinary.event; + private _onCursorMove = new EventEmitter(); + public readonly onCursorMove = this._onCursorMove.event; + private _onData = new EventEmitter(); + public readonly onData = this._onData.event; + private _onKey = new EventEmitter<{ key: string, domEvent: KeyboardEvent }>(); + public readonly onKey = this._onKey.event; + private _onLineFeed = new EventEmitter(); + 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(); + public readonly onScroll = this._onScroll.event; + private _onSelectionChange = new EventEmitter(); + public readonly onSelectionChange = this._onSelectionChange.event; + private _onTitleChange = new EventEmitter(); + public readonly onTitleChange = this._onTitleChange.event; + // #endregion + + // #region Simple properties + public get cols(): Promise { return this.evaluate(([term]) => term.cols); } + public get rows(): Promise { return this.evaluate(([term]) => term.rows); } + public get modes(): Promise { 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 { return this.evaluate(([term]) => term.dispose()); } + public async reset(): Promise { return this.evaluate(([term]) => term.reset()); } + public async clear(): Promise { return this.evaluate(([term]) => term.clear()); } + public async focus(): Promise { return this.evaluate(([term]) => term.focus()); } + public async blur(): Promise { return this.evaluate(([term]) => term.blur()); } + public async hasSelection(): Promise { return this.evaluate(([term]) => term.hasSelection()); } + public async getSelection(): Promise { return this.evaluate(([term]) => term.getSelection()); } + public async getSelectionPosition(): Promise { return this.evaluate(([term]) => term.getSelectionPosition()); } + public async selectAll(): Promise { return this.evaluate(([term]) => term.selectAll()); } + public async clearSelection(): Promise { return this.evaluate(([term]) => term.clearSelection()); } + public async select(column: number, row: number, length: number): Promise { 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 { return this._page.evaluate(([term, data]) => term.paste(data), [await this.getHandle(), data] as const); } + public async getOption(key: T): Promise { return this._page.evaluate(([term, key]) => term.options[key as T], [await this.getHandle(), key] as const); } + public async setOption(key: T, value: ITerminalOptions[T]): Promise { 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): Promise { + return this._page.evaluate(([term, value]) => { + term.options = value; + }, [await this.getHandle(), value] as const); + } + public async scrollToTop(): Promise { return this.evaluate(([term]) => term.scrollToTop()); } + public async scrollToBottom(): Promise { return this.evaluate(([term]) => term.scrollToBottom()); } + public async scrollPages(pageCount: number): Promise { return this._page.evaluate(([term, pageCount]) => term.scrollPages(pageCount), [await this.getHandle(), pageCount] as const); } + public async scrollToLine(line: number): Promise { return this._page.evaluate(([term, line]) => term.scrollToLine(line), [await this.getHandle(), line] as const); } + public async scrollLines(amount: number): Promise { return this._page.evaluate(([term, amount]) => term.scrollLines(amount), [await this.getHandle(), amount] as const); } + public async write(data: string | Uint8Array): Promise { + 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 { + 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 { return this._page.evaluate(([term, cols, rows]) => term.resize(cols, rows), [await this.getHandle(), cols, rows] as const); } + public async registerMarker(y: number): Promise { return this._page.evaluate(([term, y]) => term.registerMarker(y), [await this.getHandle(), y] as const); } + // #endregion + + public async evaluate(pageFunction: PageFunction[], T>): Promise { + return this._page.evaluate(pageFunction, [await this.getHandle()]); + } + + public async evaluateHandle(pageFunction: PageFunction[], T>): Promise> { + return this._page.evaluateHandle(pageFunction, [await this.getHandle()]); + } + + public async getHandle(): Promise> { + // 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> + ) { + } + + public get type(): Promise<'normal' | 'alternate'> { return this.evaluate(([buffer]) => buffer.type); } + public get cursorY(): Promise { return this.evaluate(([buffer]) => buffer.cursorY); } + public get cursorX(): Promise { return this.evaluate(([buffer]) => buffer.cursorX); } + public get viewportY(): Promise { return this.evaluate(([buffer]) => buffer.viewportY); } + public get baseY(): Promise { return this.evaluate(([buffer]) => buffer.baseY); } + public get length(): Promise { return this.evaluate(([buffer]) => buffer.length); } + public async getLine(y: number): Promise { + 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); + } + return undefined; + } + + public async evaluate(pageFunction: PageFunction[], T>): Promise { + return this._page.evaluate(pageFunction, [await this._handle]); + } +} + +class TerminalBufferLine { + constructor( + private readonly _page: Page, + private readonly _handle: JSHandle + ) { + } + + public get length(): Promise { return this.evaluate(([bufferLine]) => bufferLine.length); } + public get isWrapped(): Promise { return this.evaluate(([bufferLine]) => bufferLine.isWrapped); } + + public translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): Promise { + 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 { + 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); + } + return undefined; + } + + public async evaluate(pageFunction: PageFunction[], T>): Promise { + return this._page.evaluate(pageFunction, [this._handle]); + } +} + +class TerminalBufferCell { + constructor( + private readonly _page: Page, + private readonly _handle: JSHandle + ) { + } + + public getWidth(): Promise { return this.evaluate(([line]) => line.getWidth()); } + public getChars(): Promise { return this.evaluate(([line]) => line.getChars()); } + + public async evaluate(pageFunction: PageFunction[], T>): Promise { + return this._page.evaluate(pageFunction, [this._handle]); + } +} + +class TerminalCoreProxy { + constructor( + private readonly _page: Page, + private readonly _proxy: TerminalProxy + ) { + } + + public get isDisposed(): Promise { return this.evaluate(([core]) => (core as any)._isDisposed); } + public get renderDimensions(): Promise { return this.evaluate(([core]) => ((core as any)._renderService as IRenderService).dimensions); } + + public async triggerBinaryEvent(data: string): Promise { + return this._page.evaluate(([core, data]) => core.coreService.triggerBinaryEvent(data), [await this._getCoreHandle(), data] as const); + } + + + private async _getCoreHandle(): Promise> { + return this._proxy.evaluateHandle(([term]) => (term as any)._core as ICoreTerminal); + } + + public async evaluate(pageFunction: PageFunction[], T>): Promise { + return this._page.evaluate(pageFunction, [await this._getCoreHandle()]); + } +} + +export async function openTerminal(ctx: ITestContext, options: ITerminalOptions = {}): Promise { + 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(page: playwright.Page, evalOrFn: string | (() => Promise), val: T, preFn?: () => Promise, maxDuration?: number): Promise { + 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(r => { + setTimeout(() => r(pollFor(page, evalOrFn, val, preFn, maxDuration! - 10)), 10); + }); + } +} + +export async function writeSync(page: playwright.Page, data: string): Promise { + 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 { + return new Promise(r => setTimeout(r, ms)); +} + +export function getBrowserType(): playwright.BrowserType | playwright.BrowserType | playwright.BrowserType { + // Default to chromium + let browserType: playwright.BrowserType | playwright.BrowserType | playwright.BrowserType = 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 { + const browserType = getBrowserType(); + const options: Record = { + 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); +} diff --git a/test/playwright/playwright.config.ts b/test/playwright/playwright.config.ts new file mode 100644 index 00000000..57b2f1e2 --- /dev/null +++ b/test/playwright/playwright.config.ts @@ -0,0 +1,34 @@ +import { PlaywrightTestConfig } from '@playwright/test'; + +const config: PlaywrightTestConfig = { + testDir: '.', + timeout: 10000, + projects: [ + { + name: 'Chrome Stable', + use: { + browserName: 'chromium', + channel: 'chrome' + } + }, + { + name: 'Firefox Stable', + use: { + browserName: 'firefox' + } + }, + { + name: 'WebKit', + use: { + browserName: 'webkit' + } + } + ], + webServer: { + command: 'npm start', + port: 3000, + timeout: 120000, + reuseExistingServer: !process.env.CI + } +}; +export default config; diff --git a/test/playwright/tsconfig.json b/test/playwright/tsconfig.json new file mode 100644 index 00000000..2cea437a --- /dev/null +++ b/test/playwright/tsconfig.json @@ -0,0 +1,42 @@ +{ + "compilerOptions": { + "lib": [ + "dom", + "es6" + ], + "rootDir": ".", + "outDir": "../../out-test/playwright", + "types": [ + "../../node_modules/@types/mocha", + "../../node_modules/@types/node" + ], + "sourceMap": true, + "removeComments": true, + "pretty": true, + "strict": true, + "declaration": true, + "baseUrl": ".", + "paths": { + "browser/*": [ + "../../src/browser/*" + ], + "common/*": [ + "../../src/common/*" + ] + }, + "target": "es2016", + "module": "commonjs", + }, + "include": [ + "./**/*", + "../../typings/xterm.d.ts" + ], + "references": [ + { + "path": "../../src/browser" + }, + { + "path": "../../src/common" + } + ] +} diff --git a/tsconfig.all.json b/tsconfig.all.json index 7b577b07..79ced9cc 100644 --- a/tsconfig.all.json +++ b/tsconfig.all.json @@ -6,6 +6,7 @@ { "path": "./src/headless" }, { "path": "./test/api" }, { "path": "./test/benchmark" }, + { "path": "./test/playwright" }, { "path": "./addons/xterm-addon-attach" }, { "path": "./addons/xterm-addon-canvas" }, { "path": "./addons/xterm-addon-fit" }, diff --git a/yarn.lock b/yarn.lock index 206445f5..2945d97b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -347,6 +347,16 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@playwright/test@^1.37.1": + version "1.37.1" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.37.1.tgz#e7f44ae0faf1be52d6360c6bbf689fd0057d9b6f" + integrity sha512-bq9zTli3vWJo8S3LwB91U0qDNQDpEXnw7knhxLM0nwDvexQAwx9tO8iKDZSqqneVq+URd/WIoz+BALMqUTgdSg== + dependencies: + "@types/node" "*" + playwright-core "1.37.1" + optionalDependencies: + fsevents "2.3.2" + "@tootallnate/once@2": version "2.0.0" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" @@ -1920,7 +1930,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@~2.3.2: +fsevents@2.3.2, fsevents@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== @@ -3066,13 +3076,6 @@ playwright-core@1.37.1: resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.37.1.tgz#cb517d52e2e8cb4fa71957639f1cd105d1683126" integrity sha512-17EuQxlSIYCmEMwzMqusJ2ztDgJePjrbttaefgdsiqeLWidjYz9BxXaTaZWxH1J95SHGk6tjE+dwgWILJoUZfA== -playwright@^1.37.1: - version "1.37.1" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.37.1.tgz#6e488d82d7d98b9127c5db9c701f9c956ab47e76" - integrity sha512-bgUXRrQKhT48zHdxDYQTpf//0xDfDd5hLeEhjuSw8rXEGoT9YeElpfvs/izonTNY21IQZ7d3s22jLxYaAnubbQ== - dependencies: - playwright-core "1.37.1" - prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" From 9eadaf707970657763fafa46288acfb29249a619 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 27 Aug 2023 08:10:58 -0700 Subject: [PATCH 04/40] Feature detect WebAssembly instead --- demo/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/client.ts b/demo/client.ts index c643100e..8297f492 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -23,7 +23,7 @@ import { LigaturesAddon } from '../addons/xterm-addon-ligatures/out/LigaturesAdd // 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')) { +if ('WebAssembly' in window) { const imageAddon = require('../addons/xterm-addon-image/out/ImageAddon'); ImageAddon = imageAddon.ImageAddon; } From 6f04e00ea17baa2b67e20ff14269c04ee6ae940c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 27 Aug 2023 08:16:53 -0700 Subject: [PATCH 05/40] Add test-playwright-parallel job --- .github/workflows/ci.yml | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75337542..e417a5a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -182,6 +182,45 @@ jobs: - name: Integration tests (${{ matrix.browser }}) run: yarn test-api-${{ matrix.browser }} --headless --forbid-only + test-playwright-parallel: + timeout-minutes: 20 + strategy: + matrix: + node-version: [18] # just one as integration tests are about testing in browser + runs-on: [ubuntu] # macos is flaky + runs-on: ${{ matrix.runs-on }}-latest + steps: + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }}.x + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }}.x + cache: 'yarn' + - name: Install dependencies + run: | + yarn --frozen-lockfile + yarn install-addons + - name: Install playwright + run: npx playwright install + - name: Wait for build job + uses: NathanFirmo/wait-for-other-job@v1.1.1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + job: build + - uses: actions/download-artifact@v3 + with: + name: build-artifacts + - name: Unzip artifacts (Linux, macOS) + if: runner.os != 'Windows' + run: unzip -o compressed-build.zip + - name: Unzip artifacts (Windows) + if: runner.os == 'Windows' + run: 7z x compressed-build.zip -aoa -o${{ github.workspace }} + - name: Print directory structure + run: ls -R + - name: Integration tests + run: yarn test-playwright --forbid-only + test-api: needs: build timeout-minutes: 20 From 86b87448386e204690fe958ed1f170baa3760b8e Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 27 Aug 2023 08:22:54 -0700 Subject: [PATCH 06/40] Install playwright deps --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e417a5a7..4bbfd19c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -201,7 +201,9 @@ jobs: yarn --frozen-lockfile yarn install-addons - name: Install playwright - run: npx playwright install + run: | + npx playwright install-deps + npx playwright install - name: Wait for build job uses: NathanFirmo/wait-for-other-job@v1.1.1 with: From b26c53d7e23f142b29dde3cf26a3cd8d4c0c40d7 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 27 Aug 2023 08:28:23 -0700 Subject: [PATCH 07/40] Make client dist a build artifact --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4bbfd19c..4e03fc1d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,7 @@ jobs: - name: Zip artifacts run: | zip -r compressed-build \ + ./client/dist/* \ ./out/* \ ./out-test/* \ ./addons/xterm-addon-attach/out/* \ From a4683ab874eb6c9b16c69199f8ddbb030940b0d5 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 27 Aug 2023 08:44:24 -0700 Subject: [PATCH 08/40] dist -> /dist --- demo/test.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/test.html b/demo/test.html index c14ca4bc..dea1350a 100644 --- a/demo/test.html +++ b/demo/test.html @@ -7,6 +7,6 @@
- + From 486d61736b7abea83b0a87160ca628d3b59f8357 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 27 Aug 2023 08:50:24 -0700 Subject: [PATCH 09/40] Start demo --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e03fc1d..99b2acfb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,6 @@ jobs: - name: Zip artifacts run: | zip -r compressed-build \ - ./client/dist/* \ ./out/* \ ./out-test/* \ ./addons/xterm-addon-attach/out/* \ @@ -221,6 +220,8 @@ jobs: run: 7z x compressed-build.zip -aoa -o${{ github.workspace }} - name: Print directory structure run: ls -R + - name: Build demo + run: yarn start & - name: Integration tests run: yarn test-playwright --forbid-only From 7000447915a99bc017dbf362c2bde090881ed0d0 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 27 Aug 2023 08:54:04 -0700 Subject: [PATCH 10/40] Build demo step --- .github/workflows/ci.yml | 2 +- demo/build.js | 77 ++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 demo/build.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99b2acfb..9629e2c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -221,7 +221,7 @@ jobs: - name: Print directory structure run: ls -R - name: Build demo - run: yarn start & + run: yarn build-demo - name: Integration tests run: yarn test-playwright --forbid-only diff --git a/demo/build.js b/demo/build.js new file mode 100644 index 00000000..6685ba17 --- /dev/null +++ b/demo/build.js @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +// @ts-check + +const path = require('path'); +const webpack = require('webpack'); + +/** + * This webpack config builds and watches the demo project. It works by taking the output from tsc + * (via `yarn watch`) which is put into `out/` and then webpacks it into `demo/dist/`. The aliases + * are used fix up the absolute paths output by tsc (because of `baseUrl` and `paths` in + * `tsconfig.json`. + * + * For production builds see `webpack.config.js` in the root directory. If that is built the demo + * can use that by switching out which `Terminal` is imported in `client.ts`, this is useful for + * validating that the packaged version works correctly. + * + * @type {import('webpack').Configuration} + */ +const clientConfig = { + entry: path.resolve(__dirname, 'client.ts'), + devtool: 'inline-source-map', + module: { + rules: [ + { + test: /\.tsx?$/, + use: 'ts-loader', + exclude: /node_modules/ + }, + { + test: /\.js$/, + use: ["source-map-loader"], + enforce: "pre", + exclude: /node_modules/ + } + ] + }, + resolve: { + modules: [ + 'node_modules', + path.resolve(__dirname, '..'), + path.resolve(__dirname, '../addons') + ], + extensions: [ '.tsx', '.ts', '.js' ], + alias: { + common: path.resolve('./out/common'), + browser: path.resolve('./out/browser') + }, + fallback: { + // The ligature modules contains fallbacks for node environments, we never want to browserify them + stream: false, + util: false, + os: false, + path: false, + fs: false + } + }, + output: { + filename: 'client-bundle.js', + path: path.resolve(__dirname, 'dist') + }, + mode: 'development', + watch: true +}; +const compiler = webpack(clientConfig); + +compiler.compile(() => (err, stats) => { + if (err) { + console.error(err); + } + console.log(stats?.toString({ + colors: true + })); +}); diff --git a/package.json b/package.json index 89bd072e..59cdf9e3 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "package-headless": "webpack --config ./webpack.config.headless.js", "postpackage-headless": "node ./bin/package_headless.js", "start": "node demo/start", + "build-demo": "node demo/build", "start-debug": "node --inspect-brk demo/start", "lint": "eslint -c .eslintrc.json --max-warnings 0 --ext .ts src/ addons/", "lint-api": "eslint --no-eslintrc -c .eslintrc.json.typings --max-warnings 0 --no-ignore --ext .d.ts typings/", From ec984d25db9b6b4e03feef040fc4a9bb0803a2a9 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 27 Aug 2023 08:56:40 -0700 Subject: [PATCH 11/40] Install --with-deps --- .github/workflows/ci.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9629e2c7..215aa988 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -201,9 +201,7 @@ jobs: yarn --frozen-lockfile yarn install-addons - name: Install playwright - run: | - npx playwright install-deps - npx playwright install + run: npx playwright install --with-deps - name: Wait for build job uses: NathanFirmo/wait-for-other-job@v1.1.1 with: From 2b21f27ac2d3b3124dfeaecefbb2962982c75ef5 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 27 Aug 2023 09:00:48 -0700 Subject: [PATCH 12/40] Fix demo build script --- demo/start.js | 59 +--------------------------- demo/{build.js => webpack.config.js} | 31 ++++----------- package.json | 2 +- 3 files changed, 10 insertions(+), 82 deletions(-) rename demo/{build.js => webpack.config.js} (54%) diff --git a/demo/start.js b/demo/start.js index 7349f36d..c0037545 100644 --- a/demo/start.js +++ b/demo/start.js @@ -5,69 +5,12 @@ // @ts-check -const path = require('path'); +const clientConfig = require('./webpack.config'); const webpack = require('webpack'); const startServer = require('./server.js'); startServer(); -/** - * This webpack config builds and watches the demo project. It works by taking the output from tsc - * (via `yarn watch`) which is put into `out/` and then webpacks it into `demo/dist/`. The aliases - * are used fix up the absolute paths output by tsc (because of `baseUrl` and `paths` in - * `tsconfig.json`. - * - * For production builds see `webpack.config.js` in the root directory. If that is built the demo - * can use that by switching out which `Terminal` is imported in `client.ts`, this is useful for - * validating that the packaged version works correctly. - * - * @type {import('webpack').Configuration} - */ -const clientConfig = { - entry: path.resolve(__dirname, 'client.ts'), - devtool: 'inline-source-map', - module: { - rules: [ - { - test: /\.tsx?$/, - use: 'ts-loader', - exclude: /node_modules/ - }, - { - test: /\.js$/, - use: ["source-map-loader"], - enforce: "pre", - exclude: /node_modules/ - } - ] - }, - resolve: { - modules: [ - 'node_modules', - path.resolve(__dirname, '..'), - path.resolve(__dirname, '../addons') - ], - extensions: [ '.tsx', '.ts', '.js' ], - alias: { - common: path.resolve('./out/common'), - browser: path.resolve('./out/browser') - }, - fallback: { - // The ligature modules contains fallbacks for node environments, we never want to browserify them - stream: false, - util: false, - os: false, - path: false, - fs: false - } - }, - output: { - filename: 'client-bundle.js', - path: path.resolve(__dirname, 'dist') - }, - mode: 'development', - watch: true -}; const compiler = webpack(clientConfig); compiler.watch({ diff --git a/demo/build.js b/demo/webpack.config.js similarity index 54% rename from demo/build.js rename to demo/webpack.config.js index 6685ba17..46a85394 100644 --- a/demo/build.js +++ b/demo/webpack.config.js @@ -1,26 +1,21 @@ /** - * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * Copyright (c) 2019 The xterm.js authors. All rights reserved. * @license MIT */ // @ts-check const path = require('path'); -const webpack = require('webpack'); /** - * This webpack config builds and watches the demo project. It works by taking the output from tsc - * (via `yarn watch`) which is put into `out/` and then webpacks it into `demo/dist/`. The aliases - * are used fix up the absolute paths output by tsc (because of `baseUrl` and `paths` in - * `tsconfig.json`. - * - * For production builds see `webpack.config.js` in the root directory. If that is built the demo - * can use that by switching out which `Terminal` is imported in `client.ts`, this is useful for - * validating that the packaged version works correctly. + * This webpack config does a production build for xterm.js. It works by taking the output from tsc + * (via `yarn watch` or `yarn prebuild`) which are put into `out/` and webpacks them into a + * production mode umd library module in `lib/`. The aliases are used fix up the absolute paths + * output by tsc (because of `baseUrl` and `paths` in `tsconfig.json`. * * @type {import('webpack').Configuration} */ -const clientConfig = { +const config = { entry: path.resolve(__dirname, 'client.ts'), devtool: 'inline-source-map', module: { @@ -62,16 +57,6 @@ const clientConfig = { filename: 'client-bundle.js', path: path.resolve(__dirname, 'dist') }, - mode: 'development', - watch: true + mode: 'development' }; -const compiler = webpack(clientConfig); - -compiler.compile(() => (err, stats) => { - if (err) { - console.error(err); - } - console.log(stats?.toString({ - colors: true - })); -}); +module.exports = config; diff --git a/package.json b/package.json index 59cdf9e3..853ba45a 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "package-headless": "webpack --config ./webpack.config.headless.js", "postpackage-headless": "node ./bin/package_headless.js", "start": "node demo/start", - "build-demo": "node demo/build", + "build-demo": "webpack --config ./demo/webpack.config.js", "start-debug": "node --inspect-brk demo/start", "lint": "eslint -c .eslintrc.json --max-warnings 0 --ext .ts src/ addons/", "lint-api": "eslint --no-eslintrc -c .eslintrc.json.typings --max-warnings 0 --no-ignore --ext .d.ts typings/", From cb977ab6b65a8c81dcbaa8e15745e0e83a641c71 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 27 Aug 2023 09:03:55 -0700 Subject: [PATCH 13/40] Remove CharWidth.test.ts from api (in playwright) --- test/api/CharWidth.api.ts | 96 --------------------------------------- 1 file changed, 96 deletions(-) delete mode 100644 test/api/CharWidth.api.ts diff --git a/test/api/CharWidth.api.ts b/test/api/CharWidth.api.ts deleted file mode 100644 index 43d5ab20..00000000 --- a/test/api/CharWidth.api.ts +++ /dev/null @@ -1,96 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { pollFor, openTerminal, launchBrowser } from './TestUtils'; -import { Browser, Page } from '@playwright/test'; - -const APP = 'http://127.0.0.1:3001/test'; - -let browser: Browser; -let page: Page; -const width = 800; -const height = 600; - -describe('CharWidth Integration Tests', function(): void { - before(async function(): Promise { - browser = await launchBrowser(); - page = await (await browser.newContext()).newPage(); - await page.setViewportSize({ width, height }); - await page.goto(APP); - await openTerminal(page, { rows: 5, cols: 30 }); - }); - - after(() => { - browser.close(); - }); - - describe('getStringCellWidth', () => { - beforeEach(async () => { - await page.evaluate(`window.term.reset()`); - }); - - it('ASCII chars', async function(): Promise { - const input = 'This is just ASCII text.#'; - await page.evaluate(`window.term.write('${input}')`); - await pollFor(page, () => sumWidths(0, 1, '#'), 25); - }); - - it('combining chars', async function(): Promise { - const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301#'; - await page.evaluate(`window.term.write('${input}')`); - await pollFor(page, () => sumWidths(0, 1, '#'), 10); - }); - - it('surrogate chars', async function(): Promise { - const input = '𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞#'; - await page.evaluate(`window.term.write('${input}')`); - await pollFor(page, () => sumWidths(0, 1, '#'), 28); - }); - - it('surrogate combining chars', async function(): Promise { - const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301#'; - await page.evaluate(`window.term.write('${input}')`); - await pollFor(page, () => sumWidths(0, 1, '#'), 12); - }); - - it('fullwidth chars', async function(): Promise { - const input = '1234567890#'; - await page.evaluate(`window.term.write('${input}')`); - await pollFor(page, () => sumWidths(0, 1, '#'), 21); - }); - - it('fullwidth chars offset 1', async function(): Promise { - const input = 'a1234567890#'; - await page.evaluate(`window.term.write('${input}')`); - await pollFor(page, () => sumWidths(0, 1, '#'), 22); - }); - - // TODO: multiline tests once #1685 is resolved - }); -}); - -async function sumWidths(start: number, end: number, sentinel: string): Promise { - await 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 page.evaluate(`window.result`); -} From 5b25c65521eb063989f00a375c4a2a40af04e81c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 27 Aug 2023 09:11:47 -0700 Subject: [PATCH 14/40] Migrate Parser tests to playwright/test --- test/api/Parser.api.ts | 213 ---------------------------- test/playwright/Parser.test.ts | 244 +++++++++++++++++++++++++++++++++ 2 files changed, 244 insertions(+), 213 deletions(-) delete mode 100644 test/api/Parser.api.ts create mode 100644 test/playwright/Parser.test.ts diff --git a/test/api/Parser.api.ts b/test/api/Parser.api.ts deleted file mode 100644 index 94d4a54a..00000000 --- a/test/api/Parser.api.ts +++ /dev/null @@ -1,213 +0,0 @@ -/** - * Copyright (c) 2019 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { assert } from 'chai'; -import { writeSync, openTerminal, launchBrowser } from './TestUtils'; -import { Browser, Page } from '@playwright/test'; - -const APP = 'http://127.0.0.1:3001/test'; - -let browser: Browser; -let page: Page; -const width = 800; -const height = 600; - -describe('Parser Integration Tests', function (): void { - before(async function (): Promise { - browser = await launchBrowser(); - page = await (await browser.newContext()).newPage(); - await page.setViewportSize({ width, height }); - await page.goto(APP); - await openTerminal(page); - }); - - after(async () => browser.close()); - - describe('registerCsiHandler', () => { - it('should call custom CSI handler with js array params', async () => { - await page.evaluate(` - window.term.reset(); - window._customCsiHandlerParams = []; - const _customCsiHandler = window.term.parser.registerCsiHandler({final: 'm'}, (params, collect) => { - window._customCsiHandlerParams.push(params); - return false; - }, ''); - `); - await writeSync(page, '\x1b[38;5;123mparams\x1b[38:2::50:100:150msubparams'); - assert.deepEqual(await page.evaluate(`window._customCsiHandlerParams`), [ - [38, 5, 123], - [38, [2, -1, 50, 100, 150]] - ]); - }); - it('async', async () => { - await page.evaluate(` - window.term.reset(); - window._customCsiHandlerCallStack = []; - const _customCsiHandlerA = window.term.parser.registerCsiHandler({intermediates:'+', final: 'Z'}, params => { - window._customCsiHandlerCallStack.push(['A', params]); - return false; - }); - const _customCsiHandlerB = window.term.parser.registerCsiHandler({intermediates:'+', final: 'Z'}, async params => { - await new Promise(res => setTimeout(res, 50)); - window._customCsiHandlerCallStack.push(['B', params]); - return false; - }); - const _customCsiHandlerC = window.term.parser.registerCsiHandler({intermediates:'+', final: 'Z'}, params => { - window._customCsiHandlerCallStack.push(['C', params]); - return false; - }); - `); - await writeSync(page, '\x1b[1;2+Z'); - assert.deepEqual(await page.evaluate(`window._customCsiHandlerCallStack`), [ - ['C', [1, 2]], - ['B', [1, 2]], - ['A', [1, 2]] - ]); - }); - }); - describe('registerDcsHandler', () => { - it('should respects return value', async () => { - await page.evaluate(` - window.term.reset(); - window._customDcsHandlerCallStack = []; - const _customDcsHandlerA = window.term.parser.registerDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { - window._customDcsHandlerCallStack.push(['A', params, data]); - return false; - }); - const _customDcsHandlerB = window.term.parser.registerDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { - window._customDcsHandlerCallStack.push(['B', params, data]); - return true; - }); - const _customDcsHandlerC = window.term.parser.registerDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { - window._customDcsHandlerCallStack.push(['C', params, data]); - return false; - }); - `); - await writeSync(page, '\x1bP1;2+psome data\x1b\\\\'); - assert.deepEqual(await page.evaluate(`window._customDcsHandlerCallStack`), [ - ['C', [1, 2], 'some data'], - ['B', [1, 2], 'some data'] - ]); - }); - it('async', async () => { - await page.evaluate(` - window.term.reset(); - window._customDcsHandlerCallStack = []; - const _customDcsHandlerA = window.term.parser.registerDcsHandler({intermediates:'+', final: 'q'}, (data, params) => { - window._customDcsHandlerCallStack.push(['A', params, data]); - return false; - }); - const _customDcsHandlerB = window.term.parser.registerDcsHandler({intermediates:'+', final: 'q'}, async (data, params) => { - await new Promise(res => setTimeout(res, 50)); - window._customDcsHandlerCallStack.push(['B', params, data]); - return false; - }); - const _customDcsHandlerC = window.term.parser.registerDcsHandler({intermediates:'+', final: 'q'}, (data, params) => { - window._customDcsHandlerCallStack.push(['C', params, data]); - return false; - }); - `); - await writeSync(page, '\x1bP1;2+qsome data\x1b\\\\'); - assert.deepEqual(await page.evaluate(`window._customDcsHandlerCallStack`), [ - ['C', [1, 2], 'some data'], - ['B', [1, 2], 'some data'], - ['A', [1, 2], 'some data'] - ]); - }); - }); - describe('registerEscHandler', () => { - it('should respects return value', async () => { - await page.evaluate(` - window.term.reset(); - window._customEscHandlerCallStack = []; - const _customEscHandlerA = window.term.parser.registerEscHandler({intermediates:'(', final: 'B'}, () => { - window._customEscHandlerCallStack.push('A'); - return false; - }); - const _customEscHandlerB = window.term.parser.registerEscHandler({intermediates:'(', final: 'B'}, () => { - window._customEscHandlerCallStack.push('B'); - return true; - }); - const _customEscHandlerC = window.term.parser.registerEscHandler({intermediates:'(', final: 'B'}, () => { - window._customEscHandlerCallStack.push('C'); - return false; - }); - `); - await writeSync(page, '\x1b(B'); - assert.deepEqual(await page.evaluate(`window._customEscHandlerCallStack`), ['C', 'B']); - }); - it('async', async () => { - await page.evaluate(` - window.term.reset(); - window._customEscHandlerCallStack = []; - const _customEscHandlerA = window.term.parser.registerEscHandler({intermediates:'(', final: 'Z'}, () => { - window._customEscHandlerCallStack.push('A'); - return false; - }); - const _customEscHandlerB = window.term.parser.registerEscHandler({intermediates:'(', final: 'Z'}, async () => { - await new Promise(res => setTimeout(res, 50)); - window._customEscHandlerCallStack.push('B'); - return false; - }); - const _customEscHandlerC = window.term.parser.registerEscHandler({intermediates:'(', final: 'Z'}, () => { - window._customEscHandlerCallStack.push('C'); - return false; - }); - `); - await writeSync(page, '\x1b(Z'); - assert.deepEqual(await page.evaluate(`window._customEscHandlerCallStack`), ['C', 'B', 'A']); - }); - }); - describe('registerOscHandler', () => { - it('should respects return value', async () => { - await page.evaluate(` - window.term.reset(); - window._customOscHandlerCallStack = []; - const _customOscHandlerA = window.term.parser.registerOscHandler(1234, data => { - window._customOscHandlerCallStack.push(['A', data]); - return false; - }); - const _customOscHandlerB = window.term.parser.registerOscHandler(1234, data => { - window._customOscHandlerCallStack.push(['B', data]); - return true; - }); - const _customOscHandlerC = window.term.parser.registerOscHandler(1234, data => { - window._customOscHandlerCallStack.push(['C', data]); - return false; - }); - `); - await writeSync(page, '\x1b]1234;some data\x07'); - assert.deepEqual(await page.evaluate(`window._customOscHandlerCallStack`), [ - ['C', 'some data'], - ['B', 'some data'] - ]); - }); - it('async', async () => { - await page.evaluate(` - window.term.reset(); - window._customOscHandlerCallStack = []; - const _customOscHandlerA = window.term.parser.registerOscHandler(666, data => { - window._customOscHandlerCallStack.push(['A', data]); - return false; - }); - const _customOscHandlerB = window.term.parser.registerOscHandler(666, async data => { - await new Promise(res => setTimeout(res, 50)); - window._customOscHandlerCallStack.push(['B', data]); - return false; - }); - const _customOscHandlerC = window.term.parser.registerOscHandler(666, data => { - window._customOscHandlerCallStack.push(['C', data]); - return false; - }); - `); - await writeSync(page, '\x1b]666;some data\x07'); - assert.deepEqual(await page.evaluate(`window._customOscHandlerCallStack`), [ - ['C', 'some data'], - ['B', 'some data'], - ['A', 'some data'] - ]); - }); - }); -}); diff --git a/test/playwright/Parser.test.ts b/test/playwright/Parser.test.ts new file mode 100644 index 00000000..02e0c4b4 --- /dev/null +++ b/test/playwright/Parser.test.ts @@ -0,0 +1,244 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ +import { test } from '@playwright/test'; +import { deepStrictEqual, ok, strictEqual } from 'assert'; +import type { IDisposable } from 'xterm'; +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()); + +declare global { + interface Window { // eslint-disable-line @typescript-eslint/naming-convention + customCsiHandlerParams?: (number | number[])[][]; + customCsiHandlerCallStack?: string[]; + customDcsHandlerCallStack?: [string, (number | number[])[], string][]; + customEscHandlerCallStack?: string[]; + customOscHandlerCallStack?: string[][]; + disposable?: IDisposable; + disposables?: IDisposable[]; + } +} + +test.describe('Parser Integration Tests', function (): void { + test.beforeEach(async () => await ctx.proxy.reset()); + test.afterEach(async () => { + await ctx.page.evaluate(() => { + window.disposable?.dispose(); + window.disposable = undefined; + window.disposables?.forEach(e => e.dispose()); + window.disposables = undefined; + }); + }); + + test.describe('registerCsiHandler', () => { + test('should call custom CSI handler with js array params', async () => { + await ctx.proxy.evaluate(([term]) => { + window.customCsiHandlerParams = []; + window.disposable = term.parser.registerCsiHandler({final: 'm'}, (params) => { + window.customCsiHandlerParams!.push(params); + return false; + }); + }); + await ctx.proxy.write('\x1b[38;5;123mparams\x1b[38:2::50:100:150msubparams'); + deepStrictEqual(await ctx.page.evaluate(() => window.customCsiHandlerParams), [ + [38, 5, 123], + [38, [2, -1, 50, 100, 150]] + ]); + }); + test('async', async () => { + await ctx.proxy.evaluate(([term]) => { + window.customCsiHandlerCallStack = []; + window.customCsiHandlerParams = []; + window.disposables = [ + term.parser.registerCsiHandler({intermediates:'+', final: 'Z'}, params => { + window.customCsiHandlerCallStack!.push('A'); + window.customCsiHandlerParams!.push(params); + return false; + }), + term.parser.registerCsiHandler({intermediates:'+', final: 'Z'}, params => { + return new Promise(res => setTimeout(res, 50)).then(() => { + window.customCsiHandlerCallStack!.push('B'); + window.customCsiHandlerParams!.push(params); + return false; + }); + }), + term.parser.registerCsiHandler({intermediates:'+', final: 'Z'}, params => { + window.customCsiHandlerCallStack!.push('C'); + window.customCsiHandlerParams!.push(params); + return false; + }) + ]; + }); + await ctx.proxy.write('\x1b[1;2+Z'); + deepStrictEqual(await ctx.page.evaluate(() => window.customCsiHandlerCallStack), [ + 'C', + 'B', + 'A' + ]); + deepStrictEqual(await ctx.page.evaluate(() => window.customCsiHandlerParams), [ + [1, 2], + [1, 2], + [1, 2] + ]); + }); + }); + test.describe('registerDcsHandler', () => { + test('should respects return value', async () => { + await ctx.proxy.evaluate(([term]) => { + window.customDcsHandlerCallStack = []; + window.disposables = [ + term.parser.registerDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { + window.customDcsHandlerCallStack!.push(['A', params, data]); + return false; + }), + term.parser.registerDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { + window.customDcsHandlerCallStack!.push(['B', params, data]); + return true; + }), + term.parser.registerDcsHandler({intermediates:'+', final: 'p'}, (data, params) => { + window.customDcsHandlerCallStack!.push(['C', params, data]); + return false; + }) + ]; + }); + await ctx.proxy.write('\x1bP1;2+psome data\x1b\\'); + deepStrictEqual(await ctx.page.evaluate(() => window.customDcsHandlerCallStack), [ + ['C', [1, 2], 'some data'], + ['B', [1, 2], 'some data'] + ]); + }); + test('async', async () => { + await ctx.proxy.evaluate(([term]) => { + window.customDcsHandlerCallStack = []; + window.disposables = [ + term.parser.registerDcsHandler({intermediates:'+', final: 'q'}, (data, params) => { + window.customDcsHandlerCallStack!.push(['A', params, data]); + return false; + }), + term.parser.registerDcsHandler({intermediates:'+', final: 'q'}, (data, params) => { + return new Promise(res => setTimeout(res, 50)).then(() => { + window.customDcsHandlerCallStack!.push(['B', params, data]); + return false; + }); + }), + term.parser.registerDcsHandler({intermediates:'+', final: 'q'}, (data, params) => { + window.customDcsHandlerCallStack!.push(['C', params, data]); + return false; + }) + ]; + }); + await ctx.proxy.write('\x1bP1;2+qsome data\x1b\\'); + deepStrictEqual(await ctx.page.evaluate(() => window.customDcsHandlerCallStack), [ + ['C', [1, 2], 'some data'], + ['B', [1, 2], 'some data'], + ['A', [1, 2], 'some data'] + ]); + }); + }); + test.describe('registerEscHandler', () => { + test('should respects return value', async () => { + await ctx.proxy.evaluate(([term]) => { + window.customEscHandlerCallStack = []; + window.disposables = [ + term.parser.registerEscHandler({intermediates:'(', final: 'B'}, () => { + window.customEscHandlerCallStack!.push('A'); + return false; + }), + term.parser.registerEscHandler({intermediates:'(', final: 'B'}, () => { + window.customEscHandlerCallStack!.push('B'); + return true; + }), + term.parser.registerEscHandler({intermediates:'(', final: 'B'}, () => { + window.customEscHandlerCallStack!.push('C'); + return false; + }) + ]; + }); + await ctx.proxy.write('\x1b(B'); + deepStrictEqual(await ctx.page.evaluate(() => window.customEscHandlerCallStack), ['C', 'B']); + }); + test('async', async () => { + await ctx.proxy.evaluate(([term]) => { + window.customEscHandlerCallStack = []; + window.disposables = [ + term.parser.registerEscHandler({intermediates:'(', final: 'Z'}, () => { + window.customEscHandlerCallStack!.push('A'); + return false; + }), + term.parser.registerEscHandler({intermediates:'(', final: 'Z'}, () => { + return new Promise(res => setTimeout(res, 50)).then(() => { + window.customEscHandlerCallStack!.push('B'); + return false; + }); + }), + term.parser.registerEscHandler({intermediates:'(', final: 'Z'}, () => { + window.customEscHandlerCallStack!.push('C'); + return false; + }) + ]; + }); + await ctx.proxy.write('\x1b(Z'); + deepStrictEqual(await ctx.page.evaluate(() => window.customEscHandlerCallStack), ['C', 'B', 'A']); + }); + }); + test.describe('registerOscHandler', () => { + test('should respects return value', async () => { + await ctx.proxy.evaluate(([term]) => { + window.customOscHandlerCallStack = []; + window.disposables = [ + term.parser.registerOscHandler(1234, data => { + window.customOscHandlerCallStack!.push(['A', data]); + return false; + }), + term.parser.registerOscHandler(1234, data => { + window.customOscHandlerCallStack!.push(['B', data]); + return true; + }), + term.parser.registerOscHandler(1234, data => { + window.customOscHandlerCallStack!.push(['C', data]); + return false; + }) + ]; + }); + await ctx.proxy.write('\x1b]1234;some data\x07'); + deepStrictEqual(await ctx.page.evaluate(() => window.customOscHandlerCallStack), [ + ['C', 'some data'], + ['B', 'some data'] + ]); + }); + test('async', async () => { + await ctx.proxy.evaluate(([term]) => { + window.customOscHandlerCallStack = []; + window.disposables = [ + term.parser.registerOscHandler(666, data => { + window.customOscHandlerCallStack!.push(['A', data]); + return false; + }), + term.parser.registerOscHandler(666, data => { + return new Promise(res => setTimeout(res, 50)).then(() => { + window.customOscHandlerCallStack!.push(['B', data]); + return false; + }); + }), + term.parser.registerOscHandler(666, data => { + window.customOscHandlerCallStack!.push(['C', data]); + return false; + }) + ]; + }); + await ctx.proxy.write('\x1b]666;some data\x07'); + deepStrictEqual(await ctx.page.evaluate(() => window.customOscHandlerCallStack), [ + ['C', 'some data'], + ['B', 'some data'], + ['A', 'some data'] + ]); + }); + }); +}); From 7fecec65992a96a629467a6007e2f14260f7a6a2 Mon Sep 17 00:00:00 2001 From: tisilent Date: Mon, 28 Aug 2023 16:54:27 +0800 Subject: [PATCH 15/40] Offset outward --- src/browser/renderer/shared/CustomGlyphs.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/browser/renderer/shared/CustomGlyphs.ts b/src/browser/renderer/shared/CustomGlyphs.ts index d0d9a528..cfb7bff8 100644 --- a/src/browser/renderer/shared/CustomGlyphs.ts +++ b/src/browser/renderer/shared/CustomGlyphs.ts @@ -366,11 +366,11 @@ export const powerlineDefinitions: { [index: string]: IVectorShape } = { // Right semi-circle solid '\u{E0B4}': { d: 'M0,0 L0,1 C0.552,1,1,0.776,1,.5 C1,0.224,0.552,0,0,0', type: VectorType.FILL, rightPadding: 1 }, // Right semi-circle line - '\u{E0B5}': { d: 'M.4,1 C.622,1,1,.676,.88,.5 C1,.324,0.622,0,.4,0', type: VectorType.STROKE, rightPadding: 1 }, + '\u{E0B5}': { d: 'M.4,1 C.622,1,1,.826,.98,.5 C1,.174,0.622,0,.4,0', type: VectorType.STROKE, rightPadding: 1 }, // Left semi-circle solid '\u{E0B6}': { d: 'M1,0 L1,1 C0.448,1,0,0.776,0,.5 C0,0.224,0.448,0,1,0', type: VectorType.FILL, leftPadding: 1 }, // Left semi-circle line - '\u{E0B7}': { d: 'M.6,1 C0.378,1,0,.676,.12,.5 C0,0.324,0.378,0,0.6,0', type: VectorType.STROKE, leftPadding: 1 }, + '\u{E0B7}': { d: 'M.6,1 C0.378,1,0,.826,.02,.5 C0,0.174,0.378,0,0.6,0', type: VectorType.STROKE, leftPadding: 1 }, // Lower left triangle '\u{E0B8}': { d: 'M-.5,-.5 L1.5,1.5 L-.5,1.5', type: VectorType.FILL }, // Backslash separator From f7ac0d878b42e8ec57cd67c447743ae74479b187 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 28 Aug 2023 02:20:31 -0700 Subject: [PATCH 16/40] Center new semi-circle line glyphs --- src/browser/renderer/shared/CustomGlyphs.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/browser/renderer/shared/CustomGlyphs.ts b/src/browser/renderer/shared/CustomGlyphs.ts index cfb7bff8..c08bc4b1 100644 --- a/src/browser/renderer/shared/CustomGlyphs.ts +++ b/src/browser/renderer/shared/CustomGlyphs.ts @@ -366,11 +366,11 @@ export const powerlineDefinitions: { [index: string]: IVectorShape } = { // Right semi-circle solid '\u{E0B4}': { d: 'M0,0 L0,1 C0.552,1,1,0.776,1,.5 C1,0.224,0.552,0,0,0', type: VectorType.FILL, rightPadding: 1 }, // Right semi-circle line - '\u{E0B5}': { d: 'M.4,1 C.622,1,1,.826,.98,.5 C1,.174,0.622,0,.4,0', type: VectorType.STROKE, rightPadding: 1 }, + '\u{E0B5}': { d: 'M.2,1 C.422,1,.8,.826,.78,.5 C.8,.174,0.422,0,.2,0', type: VectorType.STROKE, rightPadding: 1 }, // Left semi-circle solid '\u{E0B6}': { d: 'M1,0 L1,1 C0.448,1,0,0.776,0,.5 C0,0.224,0.448,0,1,0', type: VectorType.FILL, leftPadding: 1 }, // Left semi-circle line - '\u{E0B7}': { d: 'M.6,1 C0.378,1,0,.826,.02,.5 C0,0.174,0.378,0,0.6,0', type: VectorType.STROKE, leftPadding: 1 }, + '\u{E0B7}': { d: 'M.8,1 C0.578,1,0.2,.826,.22,.5 C0.2,0.174,0.578,0,0.8,0', type: VectorType.STROKE, leftPadding: 1 }, // Lower left triangle '\u{E0B8}': { d: 'M-.5,-.5 L1.5,1.5 L-.5,1.5', type: VectorType.FILL }, // Backslash separator From 7bd9f8ee38deb79272610ea63007b79623a0053c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Mon, 28 Aug 2023 02:52:03 -0700 Subject: [PATCH 17/40] Revert image addon options type name --- demo/client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index 8297f492..f9591ce4 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -21,7 +21,7 @@ import { Unicode11Addon } from '../addons/xterm-addon-unicode11/out/Unicode11Add 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'; +import type { ImageAddonType, IImageAddonOptions } from '../addons/xterm-addon-image/out/ImageAddon'; let ImageAddon: ImageAddonType | undefined; // eslint-disable-line @typescript-eslint/naming-convention if ('WebAssembly' in window) { const imageAddon = require('../addons/xterm-addon-image/out/ImageAddon'); @@ -1263,7 +1263,7 @@ Test BG-colored Erase (BCE): function initImageAddonExposed(): void { - const DEFAULT_OPTIONS: IImageAddonOptionsType = (addons.image.instance as any)._defaultOpts; + const DEFAULT_OPTIONS: IImageAddonOptions = (addons.image.instance as any)._defaultOpts; const limitStorageElement = document.querySelector('#image-storagelimit'); limitStorageElement.valueAsNumber = addons.image.instance.storageLimit; addDomListener(limitStorageElement, 'change', () => { From fac5e5f02d7e496378b61713f7ed5ecf3ff34832 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 1 Sep 2023 08:46:14 -0700 Subject: [PATCH 18/40] Improve types of proxy object to extend base interfaces --- test/playwright/TestUtils.ts | 39 +++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/test/playwright/TestUtils.ts b/test/playwright/TestUtils.ts index 1fb54178..357a5dd3 100644 --- a/test/playwright/TestUtils.ts +++ b/test/playwright/TestUtils.ts @@ -10,7 +10,7 @@ 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 { IBuffer, IBufferCell, IBufferLine, IBufferNamespace, IBufferRange, IModes, ITerminalOptions, Terminal } from 'xterm'; import { EventEmitter } from '../../out/common/EventEmitter'; // TODO: We could avoid needing this import deepEqual = require('deep-equal'); @@ -40,16 +40,34 @@ type EnsureAsyncProperties = { [Key in keyof T]: EnsureAsync }; -interface ITerminalProxy extends - // Interfaces that the proxy implements with async values - EnsureAsyncProperties> { +type PromisifyMethods = { + [K in keyof T]: T[K] extends (...args: infer Args) => infer Return + ? (...args: Args) => Promise + : T[K]; +}; - // Custom proxy methods +/** + * A type that proxy objects implement that enables overriding a base interface with a set of types + * to override as async (ie. properties that are not synced to the node side), and a set of types to + * omit from the interface (ie. properties that map to completely different implementations). + */ +type PlaywrightApiProxy = ( + // Interfaces that the proxy implements as async + EnsureAsyncProperties> & + PromisifyMethods> & + // Interfaces that the proxy implements as is (exclude async/custom) + Omit +); + +interface ITerminalProxyCustomMethods { evaluate(pageFunction: PageFunction[], T>): Promise; write(data: string | Uint8Array): Promise; } -export class TerminalProxy implements ITerminalProxy { +type TerminalProxyAsyncPropOverrides = 'cols' | 'rows' | 'modes'; +type TerminalProxyAsyncMethodOverrides = 'hasSelection' | 'getSelection' | 'getSelectionPosition' | 'registerMarker'; +type TerminalProxyCustomOverrides = 'buffer'; +export class TerminalProxy implements ITerminalProxyCustomMethods, PlaywrightApiProxy { constructor(private readonly _page: Page) { } @@ -178,7 +196,7 @@ export class TerminalProxy implements ITerminalProxy { }, [await this.getHandle(), typeof data === 'string' ? data : Array.from(data)] as const); } public async resize(cols: number, rows: number): Promise { return this._page.evaluate(([term, cols, rows]) => term.resize(cols, rows), [await this.getHandle(), cols, rows] as const); } - public async registerMarker(y: number): Promise { return this._page.evaluate(([term, y]) => term.registerMarker(y), [await this.getHandle(), y] as const); } + public async registerMarker(y?: number | undefined): Promise { return this._page.evaluate(([term, y]) => term.registerMarker(y), [await this.getHandle(), y] as const); } // #endregion public async evaluate(pageFunction: PageFunction[], T>): Promise { @@ -195,7 +213,10 @@ export class TerminalProxy implements ITerminalProxy { } } -class TerminalBufferNamespaceProxy { +class TerminalBufferNamespaceProxy implements PlaywrightApiProxy { + private _onBufferChange = new EventEmitter(); + public readonly onBufferChange = this._onBufferChange.event; + constructor( private readonly _page: Page, private readonly _proxy: TerminalProxy @@ -208,7 +229,7 @@ class TerminalBufferNamespaceProxy { public get alternate(): TerminalBufferProxy { return new TerminalBufferProxy(this._page, this._proxy, this._proxy.evaluateHandle(([term]) => term.buffer.alternate)); } } -class TerminalBufferProxy { +class TerminalBufferProxy /* implements EnsureAsyncProperties*/ { constructor( private readonly _page: Page, private readonly _proxy: TerminalProxy, From 92d09f2ea153bad4ddd97d7514076ff0046c3601 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 1 Sep 2023 08:50:03 -0700 Subject: [PATCH 19/40] Clean up, fix errors, add todo --- test/playwright/TestUtils.ts | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/test/playwright/TestUtils.ts b/test/playwright/TestUtils.ts index 357a5dd3..c18426c9 100644 --- a/test/playwright/TestUtils.ts +++ b/test/playwright/TestUtils.ts @@ -39,8 +39,7 @@ type EnsureAsync = T extends PromiseLike ? T : Promise; type EnsureAsyncProperties = { [Key in keyof T]: EnsureAsync }; - -type PromisifyMethods = { +type EnsureAsyncMethods = { [K in keyof T]: T[K] extends (...args: infer Args) => infer Return ? (...args: Args) => Promise : T[K]; @@ -54,7 +53,7 @@ type PromisifyMethods = { type PlaywrightApiProxy = ( // Interfaces that the proxy implements as async EnsureAsyncProperties> & - PromisifyMethods> & + EnsureAsyncMethods> & // Interfaces that the proxy implements as is (exclude async/custom) Omit ); @@ -66,7 +65,26 @@ interface ITerminalProxyCustomMethods { type TerminalProxyAsyncPropOverrides = 'cols' | 'rows' | 'modes'; type TerminalProxyAsyncMethodOverrides = 'hasSelection' | 'getSelection' | 'getSelectionPosition' | 'registerMarker'; -type TerminalProxyCustomOverrides = 'buffer'; +type TerminalProxyCustomOverrides = 'buffer' | ( + // TODO: Review and implement below if needed + 'element' | + 'textarea' | + 'markers' | + 'unicode' | + 'parser' | + 'options' | + 'onWriteParsed' | + 'open' | + 'attachCustomKeyEventHandler' | + 'registerLinkProvider' | + 'registerCharacterJoiner' | + 'deregisterCharacterJoiner' | + 'registerDecoration' | + 'selectLines' | + 'refresh' | + 'clearTextureAtlas' | + 'loadAddon' +); export class TerminalProxy implements ITerminalProxyCustomMethods, PlaywrightApiProxy { constructor(private readonly _page: Page) { } @@ -213,7 +231,7 @@ export class TerminalProxy implements ITerminalProxyCustomMethods, PlaywrightApi } } -class TerminalBufferNamespaceProxy implements PlaywrightApiProxy { +class TerminalBufferNamespaceProxy implements PlaywrightApiProxy { private _onBufferChange = new EventEmitter(); public readonly onBufferChange = this._onBufferChange.event; From 66454d378b411bf0c0db49a9746f58bc6179a87d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 1 Sep 2023 08:50:39 -0700 Subject: [PATCH 20/40] Add todos --- test/playwright/TestUtils.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/playwright/TestUtils.ts b/test/playwright/TestUtils.ts index c18426c9..5657b79d 100644 --- a/test/playwright/TestUtils.ts +++ b/test/playwright/TestUtils.ts @@ -247,6 +247,7 @@ class TerminalBufferNamespaceProxy implements PlaywrightApiProxy term.buffer.alternate)); } } +// TODO: Adopt PlaywrightApiProxy class TerminalBufferProxy /* implements EnsureAsyncProperties*/ { constructor( private readonly _page: Page, @@ -275,6 +276,7 @@ class TerminalBufferProxy /* implements EnsureAsyncProperties*/ { } } +// TODO: Adopt PlaywrightApiProxy class TerminalBufferLine { constructor( private readonly _page: Page, @@ -305,6 +307,7 @@ class TerminalBufferLine { } } +// TODO: Adopt PlaywrightApiProxy class TerminalBufferCell { constructor( private readonly _page: Page, From 13af55f9fca3e3d8bddb9fc6276af8dcaa51f336 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 2 Sep 2023 06:00:33 -0700 Subject: [PATCH 21/40] Add onWriteParsed event to proxy --- test/playwright/TestUtils.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/playwright/TestUtils.ts b/test/playwright/TestUtils.ts index 5657b79d..b1a98eb8 100644 --- a/test/playwright/TestUtils.ts +++ b/test/playwright/TestUtils.ts @@ -73,7 +73,6 @@ type TerminalProxyCustomOverrides = 'buffer' | ( 'unicode' | 'parser' | 'options' | - 'onWriteParsed' | 'open' | 'attachCustomKeyEventHandler' | 'registerLinkProvider' | @@ -104,6 +103,7 @@ export class TerminalProxy implements ITerminalProxyCustomMethods, PlaywrightApi 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)); + await this._page.exposeFunction('onWriteParsed', () => this._onWriteParsed.fire()); } /** @@ -121,7 +121,8 @@ export class TerminalProxy implements ITerminalProxyCustomMethods, PlaywrightApi this._onResize, this._onScroll, this._onSelectionChange, - this._onTitleChange + this._onTitleChange, + this._onWriteParsed ]) { emitter.clearListeners(); } @@ -136,6 +137,7 @@ export class TerminalProxy implements ITerminalProxyCustomMethods, PlaywrightApi 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)); + await this.evaluate(([term]) => term.onWriteParsed((window as any).onWriteParsed)); } // #region Events @@ -161,6 +163,8 @@ export class TerminalProxy implements ITerminalProxyCustomMethods, PlaywrightApi public readonly onSelectionChange = this._onSelectionChange.event; private _onTitleChange = new EventEmitter(); public readonly onTitleChange = this._onTitleChange.event; + private _onWriteParsed = new EventEmitter(); + public readonly onWriteParsed = this._onWriteParsed.event; // #endregion // #region Simple properties From 91456fff04b9f3abf2d75ed3d21f95916ad89bb7 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 2 Sep 2023 06:06:16 -0700 Subject: [PATCH 22/40] More proxy impl --- test/playwright/TestUtils.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/playwright/TestUtils.ts b/test/playwright/TestUtils.ts index b1a98eb8..f700392b 100644 --- a/test/playwright/TestUtils.ts +++ b/test/playwright/TestUtils.ts @@ -10,7 +10,7 @@ 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, IBufferNamespace, IBufferRange, IModes, ITerminalOptions, Terminal } from 'xterm'; +import { IBuffer, IBufferCell, IBufferLine, IBufferNamespace, IBufferRange, IDecoration, IDecorationOptions, IModes, ITerminalOptions, Terminal } from 'xterm'; import { EventEmitter } from '../../out/common/EventEmitter'; // TODO: We could avoid needing this import deepEqual = require('deep-equal'); @@ -64,9 +64,9 @@ interface ITerminalProxyCustomMethods { } type TerminalProxyAsyncPropOverrides = 'cols' | 'rows' | 'modes'; -type TerminalProxyAsyncMethodOverrides = 'hasSelection' | 'getSelection' | 'getSelectionPosition' | 'registerMarker'; +type TerminalProxyAsyncMethodOverrides = 'hasSelection' | 'getSelection' | 'getSelectionPosition' | 'registerMarker' | 'registerDecoration'; type TerminalProxyCustomOverrides = 'buffer' | ( - // TODO: Review and implement below if needed + // The below are not implemented yet 'element' | 'textarea' | 'markers' | @@ -78,10 +78,6 @@ type TerminalProxyCustomOverrides = 'buffer' | ( 'registerLinkProvider' | 'registerCharacterJoiner' | 'deregisterCharacterJoiner' | - 'registerDecoration' | - 'selectLines' | - 'refresh' | - 'clearTextureAtlas' | 'loadAddon' ); export class TerminalProxy implements ITerminalProxyCustomMethods, PlaywrightApiProxy { @@ -192,9 +188,11 @@ export class TerminalProxy implements ITerminalProxyCustomMethods, PlaywrightApi public async getSelection(): Promise { return this.evaluate(([term]) => term.getSelection()); } public async getSelectionPosition(): Promise { return this.evaluate(([term]) => term.getSelectionPosition()); } public async selectAll(): Promise { return this.evaluate(([term]) => term.selectAll()); } + public async selectLines(start: number, end: number): Promise { return this._page.evaluate(([term, start, end]) => term.selectLines(start, end), [await this.getHandle(), start, end] as const); } public async clearSelection(): Promise { return this.evaluate(([term]) => term.clearSelection()); } public async select(column: number, row: number, length: number): Promise { 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 { return this._page.evaluate(([term, data]) => term.paste(data), [await this.getHandle(), data] as const); } + public async refresh(start: number, end: number): Promise { return this._page.evaluate(([term, start, end]) => term.refresh(start, end), [await this.getHandle(), start, end] as const); } public async getOption(key: T): Promise { return this._page.evaluate(([term, key]) => term.options[key as T], [await this.getHandle(), key] as const); } public async setOption(key: T, value: ITerminalOptions[T]): Promise { 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): Promise { @@ -219,6 +217,8 @@ export class TerminalProxy implements ITerminalProxyCustomMethods, PlaywrightApi } public async resize(cols: number, rows: number): Promise { return this._page.evaluate(([term, cols, rows]) => term.resize(cols, rows), [await this.getHandle(), cols, rows] as const); } public async registerMarker(y?: number | undefined): Promise { return this._page.evaluate(([term, y]) => term.registerMarker(y), [await this.getHandle(), y] as const); } + public async registerDecoration(decorationOptions: IDecorationOptions): Promise { return this._page.evaluate(([term, decorationOptions]) => term.registerDecoration(decorationOptions), [await this.getHandle(), decorationOptions] as const); } + public async clearTextureAtlas(): Promise { return this.evaluate(([term]) => term.clearTextureAtlas()); } // #endregion public async evaluate(pageFunction: PageFunction[], T>): Promise { From a29ab32876556c98c1a29d975b01ae5e9649c72a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 2 Sep 2023 07:10:47 -0700 Subject: [PATCH 23/40] Migrate MouseTracking api tests to playwright --- .../MouseTracking.test.ts} | 606 +++++++++--------- test/playwright/TestUtils.ts | 17 +- 2 files changed, 314 insertions(+), 309 deletions(-) rename test/{api/MouseTracking.api.ts => playwright/MouseTracking.test.ts} (72%) diff --git a/test/api/MouseTracking.api.ts b/test/playwright/MouseTracking.test.ts similarity index 72% rename from test/api/MouseTracking.api.ts rename to test/playwright/MouseTracking.test.ts index 84b4402f..ba96a4e6 100644 --- a/test/api/MouseTracking.api.ts +++ b/test/playwright/MouseTracking.test.ts @@ -2,14 +2,18 @@ * Copyright (c) 2019 The xterm.js authors. All rights reserved. * @license MIT */ +import { test } from '@playwright/test'; +import { deepStrictEqual, ok, strictEqual } from 'assert'; +import type { IDisposable } from 'xterm'; +import { createTestContext, ITestContext, openTerminal, pollFor } from './TestUtils'; -import { pollFor, writeSync, openTerminal, getBrowserType, launchBrowser } from './TestUtils'; -import { Browser, Page } from '@playwright/test'; +let ctx: ITestContext; +test.beforeAll(async ({ browser }) => { + ctx = await createTestContext(browser); + await openTerminal(ctx); +}); +test.afterAll(async () => await ctx.page.close()); -const APP = 'http://127.0.0.1:3001/test'; - -let browser: Browser; -let page: Page; // adjusted to work inside devcontainer // see https://github.com/xtermjs/xterm.js/issues/2379 const width = 1280; @@ -28,15 +32,13 @@ const noShift = process.platform === 'darwin' ? false : true; * Helper functions. */ async function resetMouseModes(): Promise { - return await page.evaluate(` - window.term.write('\x1b[?9l\x1b[?1000l\x1b[?1001l\x1b[?1002l\x1b[?1003l'); - window.term.write('\x1b[?1005l\x1b[?1006l\x1b[?1015l'); - `); + await ctx.proxy.write('\x1b[?9l\x1b[?1000l\x1b[?1001l\x1b[?1002l\x1b[?1003l'); + await ctx.proxy.write('\x1b[?1005l\x1b[?1006l\x1b[?1015l'); } async function getReports(encoding: string): Promise { - const reports: any = await page.evaluate(`window.calls`); - await page.evaluate(`window.calls = [];`); + const reports: any = await ctx.page.evaluate(`window.calls`); + await ctx.page.evaluate(`window.calls = [];`); return reports.map((report: number[]) => parseReport(encoding, report)); } @@ -44,7 +46,7 @@ async function getReports(encoding: string): Promise { // always adds +2 in each direction so we dont end up in the wrong cell // due to rounding issues async function cellPos(col: number, row: number): Promise { - const coords: any = await page.evaluate(` + const coords: any = await ctx.page.evaluate(` (function() { const rect = window.term.element.getBoundingClientRect(); const dim = term._core._renderService.dimensions; @@ -63,40 +65,13 @@ async function cellPos(col: number, row: number): Promise { */ async function mouseMove(col: number, row: number): Promise { const [xPixels, yPixels] = await cellPos(col, row); - return await page.mouse.move(xPixels, yPixels); + await ctx.page.mouse.move(xPixels, yPixels); } async function mouseDown(button: 'left' | 'right' | 'middle' | undefined): Promise { - return await page.mouse.down({ button }); + await ctx.page.mouse.down({ button }); } async function mouseUp(button: 'left' | 'right' | 'middle' | undefined): Promise { - return await page.mouse.up({ button }); -} -// FIXME: wheel event dont work anymore (playwright issue?) -// --> commented out in the tests below -// async function wheelUp(): Promise { -// return await page.mouse.wheel(0, -10); -// } -// async function wheelDown(): Promise { -// return await page.mouse.wheel(0, 10); -// } - -// FIXME: complex Shift tests dont work anymore, also commented out - -function toModifiersMask(modifiers: Set): number { - let mask = 0; - if (modifiers.has('Alt')) { - mask |= 1; - } - if (modifiers.has('Control')) { - mask |= 2; - } - if (modifiers.has('Meta')) { - mask |= 4; - } - if (modifiers.has('Shift')) { - mask |= 8; - } - return mask; + await ctx.page.mouse.up({ button }); } // button definitions @@ -189,39 +164,28 @@ function parseReport(encoding: string, msg: number[]): { state: any, row: number } } -/** - * Mouse tracking tests. - */ -describe('Mouse Tracking Tests', async () => { - const browserType = getBrowserType(); - browserType.name() === 'chromium'; - const itMouse = it; - - before(async function(): Promise { - browser = await launchBrowser(); - page = await (await browser.newContext()).newPage(); - await page.setViewportSize({ width, height }); - }); - - after(async () => browser.close()); - - beforeEach(async () => { - await page.goto(APP); - await openTerminal(page); +test.describe('Mouse Tracking Tests', function (): void { + test.beforeAll(async function(): Promise { + await ctx.page.setViewportSize({ width, height }); // patch terminal to get the onData calls // we encode the msg here to an array of codes to not lose bytes // (transmission strips non utf8 bytes) // also resize so we can properly test the edge cases - await page.evaluate(` - window.calls = []; - window.term.onData(e => calls.push( Array.from(e).map(el => el.charCodeAt(0)) )); - window.term.onBinary(e => calls.push( Array.from(e).map(el => el.charCodeAt(0)) )); - window.term.options.fontSize = ${fontSize}; - window.term.resize(${cols}, ${rows}); + await ctx.page.evaluate(` + window.term.onData(e => window.calls.push( Array.from(e).map(el => el.charCodeAt(0)) )); + window.term.onBinary(e => window.calls.push( Array.from(e).map(el => el.charCodeAt(0)) )); `); }); - describe('DECSET 9 (X10)', async () => { + test.beforeEach(async () => { + await ctx.page.evaluate(` + window.calls = []; + window.term.options.fontSize = ${fontSize}; + `); + await ctx.proxy.resize(cols, rows); + }); + + test.describe('DECSET 9 (X10)', async () => { /** * X10 protocol: * - only press events @@ -229,39 +193,43 @@ describe('Mouse Tracking Tests', async () => { * - no move * - no modifiers */ - itMouse('default encoding', async () => { + test('default encoding', async () => { + if (ctx.browser.browserType().name() === 'webkit') { + test.skip(); + return; + } const encoding = 'DEFAULT'; await resetMouseModes(); await mouseMove(0, 0); - await writeSync(page, '\x1b[?9h'); + await ctx.proxy.write('\x1b[?9h'); // test at 0,0 await mouseDown('left'); - await pollFor(page, () => getReports(encoding), [{ col: 1, row: 1, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }]); + await pollFor(ctx.page, () => getReports(encoding), [{ col: 1, row: 1, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }]); // mouseup should not report await mouseUp('left'); - await pollFor(page, () => getReports(encoding), []); + await pollFor(ctx.page, () => getReports(encoding), []); // mousemove should not report await mouseMove(50, 10); - await pollFor(page, () => getReports(encoding), []); + await pollFor(ctx.page, () => getReports(encoding), []); await mouseDown('left'); await mouseUp('left'); - await pollFor(page, () => getReports(encoding), [{ col: 51, row: 11, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }]); + await pollFor(ctx.page, () => getReports(encoding), [{ col: 51, row: 11, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }]); // test at max rows/cols // capped at 223 (1-based) await mouseMove(223 - 1, rows - 1); await mouseDown('left'); await mouseUp('left'); - await pollFor(page, () => getReports(encoding), [{ col: 223, row: rows, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }]); + await pollFor(ctx.page, () => getReports(encoding), [{ col: 223, row: rows, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }]); // higher than 223 should not report at all await mouseMove(257, rows - 1); await mouseDown('left'); await mouseUp('left'); - await pollFor(page, () => getReports(encoding), []); + await pollFor(ctx.page, () => getReports(encoding), []); // button press/move/release tests // left button @@ -270,7 +238,7 @@ describe('Mouse Tracking Tests', async () => { await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); - await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }]); + await pollFor(ctx.page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }]); // middle button // bug: default action not cancelled (adds data to getReports from clipboard under X11) // await mouseMove(43, 24); @@ -278,7 +246,7 @@ describe('Mouse Tracking Tests', async () => { // await mouseDown('middle'); // await mouseMove(44, 24); // await mouseUp('middle'); - // await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); + // await pollFor(ctx.page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); // right button // bug: default action not cancelled (popup shown) await mouseMove(43, 24); @@ -286,39 +254,39 @@ describe('Mouse Tracking Tests', async () => { await mouseDown('right'); await mouseMove(44, 24); await mouseUp('right'); - await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'press', button: 'right', modifier: { control: false, shift: false, meta: false } } }]); + await pollFor(ctx.page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'press', button: 'right', modifier: { control: false, shift: false, meta: false } } }]); // wheel // await mouseMove(43, 24); // await getReports(encoding); // clear reports // await wheelUp(); - // await pollFor(page, () => getReports(encoding), []); + // await pollFor(ctx.page, () => getReports(encoding), []); // await wheelDown(); - // await pollFor(page, () => getReports(encoding), []); + // await pollFor(ctx.page, () => getReports(encoding), []); // modifiers // CTRL await mouseMove(43, 24); await getReports(encoding); // clear reports - await page.keyboard.down('Control'); + await ctx.page.keyboard.down('Control'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); // await wheelDown(); - await page.keyboard.up('Control'); - await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }]); + await ctx.page.keyboard.up('Control'); + await pollFor(ctx.page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }]); // ALT await mouseMove(43, 24); await getReports(encoding); // clear reports - await page.keyboard.down('Alt'); + await ctx.page.keyboard.down('Alt'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); // await wheelDown(); - await page.keyboard.up('Alt'); - await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }]); + await ctx.page.keyboard.up('Alt'); + await pollFor(ctx.page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }]); // SHIFT // note: caught by selection manager @@ -326,16 +294,16 @@ describe('Mouse Tracking Tests', async () => { // bug: no modifier reported await mouseMove(43, 24); await getReports(encoding); // clear reports - await page.keyboard.down('Shift'); // defaults to ShiftLeft + await ctx.page.keyboard.down('Shift'); // defaults to ShiftLeft await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); // await wheelDown(); - await page.keyboard.up('Shift'); + await ctx.page.keyboard.up('Shift'); if (noShift) { - await pollFor(page, () => getReports(encoding), []); + await pollFor(ctx.page, () => getReports(encoding), []); } else { - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } } ]); } @@ -344,44 +312,48 @@ describe('Mouse Tracking Tests', async () => { // bug: Shift not working - reporting totally wrong coords and modifiers - selection manager again? await mouseMove(43, 24); await getReports(encoding); // clear reports - await page.keyboard.down('Control'); - await page.keyboard.down('Alt'); - // await page.keyboard.down('Shift'); + await ctx.page.keyboard.down('Control'); + await ctx.page.keyboard.down('Alt'); + // await ctx.page.keyboard.down('Shift'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); // await wheelDown(); - await page.keyboard.up('Control'); - await page.keyboard.up('Alt'); - // await page.keyboard.up('Shift'); - await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }]); + await ctx.page.keyboard.up('Control'); + await ctx.page.keyboard.up('Alt'); + // await ctx.page.keyboard.up('Shift'); + await pollFor(ctx.page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }]); }); - itMouse('SGR encoding', async () => { + test('SGR encoding', async () => { + if (ctx.browser.browserType().name() === 'webkit') { + test.skip(); + return; + } const encoding = 'SGR'; await resetMouseModes(); await mouseMove(0, 0); - await writeSync(page, '\x1b[?9h\x1b[?1006h'); + await ctx.proxy.write('\x1b[?9h\x1b[?1006h'); // test at 0,0 await mouseDown('left'); - await pollFor(page, () => getReports(encoding), [{ col: 1, row: 1, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }]); + await pollFor(ctx.page, () => getReports(encoding), [{ col: 1, row: 1, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }]); // mouseup should not report await mouseUp('left'); - await pollFor(page, () => getReports(encoding), []); + await pollFor(ctx.page, () => getReports(encoding), []); // mousemove should not report await mouseMove(50, 10); - await pollFor(page, () => getReports(encoding), []); + await pollFor(ctx.page, () => getReports(encoding), []); await mouseDown('left'); await mouseUp('left'); - await pollFor(page, () => getReports(encoding), [{ col: 51, row: 11, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }]); + await pollFor(ctx.page, () => getReports(encoding), [{ col: 51, row: 11, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }]); // test at max rows/cols await mouseMove(cols - 1, rows - 1); await mouseDown('left'); await mouseUp('left'); - await pollFor(page, () => getReports(encoding), [{ col: cols, row: rows, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }]); + await pollFor(ctx.page, () => getReports(encoding), [{ col: cols, row: rows, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }]); // button press/move/release tests // left button @@ -390,7 +362,7 @@ describe('Mouse Tracking Tests', async () => { await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); - await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }]); + await pollFor(ctx.page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }]); // middle button // bug: default action not cancelled (adds data to getReports from clipboard under X11) // await mouseMove(43, 24); @@ -398,7 +370,7 @@ describe('Mouse Tracking Tests', async () => { // await mouseDown('middle'); // await mouseMove(44, 24); // await mouseUp('middle'); - // await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); + // await pollFor(ctx.page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); // right button // bug: default action not cancelled (popup shown) await mouseMove(43, 24); @@ -406,54 +378,54 @@ describe('Mouse Tracking Tests', async () => { await mouseDown('right'); await mouseMove(44, 24); await mouseUp('right'); - await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'press', button: 'right', modifier: { control: false, shift: false, meta: false } } }]); + await pollFor(ctx.page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'press', button: 'right', modifier: { control: false, shift: false, meta: false } } }]); // wheel // await mouseMove(43, 24); // await getReports(encoding); // clear reports // await wheelUp(); - // await pollFor(page, () => getReports(encoding), []); + // await pollFor(ctx.page, () => getReports(encoding), []); // await wheelDown(); - // await pollFor(page, () => getReports(encoding), []); + // await pollFor(ctx.page, () => getReports(encoding), []); // modifiers // CTRL await mouseMove(43, 24); await getReports(encoding); // clear reports - await page.keyboard.down('Control'); + await ctx.page.keyboard.down('Control'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); // await wheelDown(); - await page.keyboard.up('Control'); - await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }]); + await ctx.page.keyboard.up('Control'); + await pollFor(ctx.page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }]); // ALT await mouseMove(43, 24); await getReports(encoding); // clear reports - await page.keyboard.down('Alt'); + await ctx.page.keyboard.down('Alt'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); // await wheelDown(); - await page.keyboard.up('Alt'); - await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }]); + await ctx.page.keyboard.up('Alt'); + await pollFor(ctx.page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }]); // SHIFT // note: caught by selection manager await mouseMove(43, 24); await getReports(encoding); // clear reports - await page.keyboard.down('Shift'); // defaults to ShiftLeft + await ctx.page.keyboard.down('Shift'); // defaults to ShiftLeft await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); // await wheelDown(); - await page.keyboard.up('Shift'); + await ctx.page.keyboard.up('Shift'); if (noShift) { - await pollFor(page, () => getReports(encoding), []); + await pollFor(ctx.page, () => getReports(encoding), []); } else { - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } } ]); } @@ -462,20 +434,20 @@ describe('Mouse Tracking Tests', async () => { // bug: Shift not working - reporting totally wrong coords and modifiers - selection manager again? await mouseMove(43, 24); await getReports(encoding); // clear reports - await page.keyboard.down('Control'); - await page.keyboard.down('Alt'); - // await page.keyboard.down('Shift'); + await ctx.page.keyboard.down('Control'); + await ctx.page.keyboard.down('Alt'); + // await ctx.page.keyboard.down('Shift'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); // await wheelDown(); - await page.keyboard.up('Control'); - await page.keyboard.up('Alt'); - // await page.keyboard.up('Shift'); - await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }]); + await ctx.page.keyboard.up('Control'); + await ctx.page.keyboard.up('Alt'); + // await ctx.page.keyboard.up('Shift'); + await pollFor(ctx.page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }]); }); }); - describe('DECSET 1000 (VT200 mouse)', () => { + test.describe('DECSET 1000 (VT200 mouse)', () => { /** * VT200 protocol: * - press and release events @@ -483,30 +455,34 @@ describe('Mouse Tracking Tests', async () => { * - no move * - all modifiers */ - itMouse('default encoding', async () => { + test('default encoding', async () => { + if (ctx.browser.browserType().name() === 'webkit') { + test.skip(); + return; + } const encoding = 'DEFAULT'; await resetMouseModes(); await mouseMove(0, 0); - await writeSync(page, '\x1b[?1000h'); + await ctx.proxy.write('\x1b[?1000h'); // test at 0,0 await mouseDown('left'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 1, row: 1, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } } ]); // mouseup should report, encoding cannot report released button await mouseUp('left'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 1, row: 1, state: { action: 'release', button: '', modifier: { control: false, shift: false, meta: false } } } ]); // mousemove should not report await mouseMove(50, 10); - await pollFor(page, () => getReports(encoding), []); + await pollFor(ctx.page, () => getReports(encoding), []); await mouseDown('left'); await mouseUp('left'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 51, row: 11, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }, { col: 51, row: 11, state: { action: 'release', button: '', modifier: { control: false, shift: false, meta: false } } } ]); @@ -516,7 +492,7 @@ describe('Mouse Tracking Tests', async () => { await mouseMove(223 - 1, rows - 1); await mouseDown('left'); await mouseUp('left'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 223, row: rows, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }, { col: 223, row: rows, state: { action: 'release', button: '', modifier: { control: false, shift: false, meta: false } } } ]); @@ -528,7 +504,7 @@ describe('Mouse Tracking Tests', async () => { await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }, { col: 45, row: 25, state: { action: 'release', button: '', modifier: { control: false, shift: false, meta: false } } } ]); @@ -539,7 +515,7 @@ describe('Mouse Tracking Tests', async () => { // await mouseDown('middle'); // await mouseMove(44, 24); // await mouseUp('middle'); - // await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); + // await pollFor(ctx.page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); // right button // bug: default action not cancelled (popup shown) await mouseMove(43, 24); @@ -547,7 +523,7 @@ describe('Mouse Tracking Tests', async () => { await mouseDown('right'); await mouseMove(44, 24); await mouseUp('right'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'right', modifier: { control: false, shift: false, meta: false } } }, { col: 45, row: 25, state: { action: 'release', button: '', modifier: { control: false, shift: false, meta: false } } } ]); @@ -556,21 +532,21 @@ describe('Mouse Tracking Tests', async () => { // await mouseMove(43, 24); // await getReports(encoding); // clear reports // await wheelUp(); - // await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'up', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]); + // await pollFor(ctx.page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'up', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]); // await wheelDown(); - // await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]); + // await pollFor(ctx.page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]); // modifiers // CTRL await mouseMove(43, 24); await getReports(encoding); // clear reports - await page.keyboard.down('Control'); + await ctx.page.keyboard.down('Control'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); // await wheelDown(); - await page.keyboard.up('Control'); - await pollFor(page, () => getReports(encoding), [ + await ctx.page.keyboard.up('Control'); + await pollFor(ctx.page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: false } } }, { col: 45, row: 25, state: { action: 'release', button: '', modifier: { control: true, shift: false, meta: false } } } // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: false } } } @@ -579,13 +555,13 @@ describe('Mouse Tracking Tests', async () => { // ALT await mouseMove(43, 24); await getReports(encoding); // clear reports - await page.keyboard.down('Alt'); + await ctx.page.keyboard.down('Alt'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); // await wheelDown(); - await page.keyboard.up('Alt'); - await pollFor(page, () => getReports(encoding), [ + await ctx.page.keyboard.up('Alt'); + await pollFor(ctx.page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: true } } }, { col: 45, row: 25, state: { action: 'release', button: '', modifier: { control: false, shift: false, meta: true } } } // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: true } } } @@ -596,18 +572,18 @@ describe('Mouse Tracking Tests', async () => { // bug: modifier not reported for passed events // await mouseMove(43, 24); // await getReports(encoding); // clear reports - // await page.keyboard.down('Shift'); // defaults to ShiftLeft + // await ctx.page.keyboard.down('Shift'); // defaults to ShiftLeft // await mouseDown('left'); // await mouseMove(44, 24); // await mouseUp('left'); // // await wheelDown(); - // await page.keyboard.up('Shift'); + // await ctx.page.keyboard.up('Shift'); // // if (noShift) { - // // await pollFor(page, () => getReports(encoding), [ + // // await pollFor(ctx.page, () => getReports(encoding), [ // // // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: true, meta: false } } } // // ]); // // } else { - // await pollFor(page, () => getReports(encoding), [ + // await pollFor(ctx.page, () => getReports(encoding), [ // { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: true, meta: false } } }, // { col: 45, row: 25, state: { action: 'release', button: '', modifier: { control: false, shift: true, meta: false } } }, // // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: true, meta: false } } } @@ -618,46 +594,50 @@ describe('Mouse Tracking Tests', async () => { // bug: Shift not working - selection manager? await mouseMove(43, 24); await getReports(encoding); // clear reports - await page.keyboard.down('Control'); - await page.keyboard.down('Alt'); - // await page.keyboard.down('Shift'); + await ctx.page.keyboard.down('Control'); + await ctx.page.keyboard.down('Alt'); + // await ctx.page.keyboard.down('Shift'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); // await wheelDown(); - await page.keyboard.up('Control'); - await page.keyboard.up('Alt'); - // await page.keyboard.up('Shift'); - await pollFor(page, () => getReports(encoding), [ + await ctx.page.keyboard.up('Control'); + await ctx.page.keyboard.up('Alt'); + // await ctx.page.keyboard.up('Shift'); + await pollFor(ctx.page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: true } } }, { col: 45, row: 25, state: { action: 'release', button: '', modifier: { control: true, shift: false, meta: true } } } // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: true } } } ]); }); - itMouse('SGR encoding', async () => { + test('SGR encoding', async () => { + if (ctx.browser.browserType().name() === 'webkit') { + test.skip(); + return; + } const encoding = 'SGR'; await resetMouseModes(); await mouseMove(0, 0); - await writeSync(page, '\x1b[?1000h\x1b[?1006h'); + await ctx.proxy.write('\x1b[?1000h\x1b[?1006h'); // test at 0,0 await mouseDown('left'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 1, row: 1, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } } ]); // mouseup should report await mouseUp('left'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 1, row: 1, state: { action: 'release', button: 'left', modifier: { control: false, shift: false, meta: false } } } ]); // mousemove should not report await mouseMove(50, 10); - await pollFor(page, () => getReports(encoding), []); + await pollFor(ctx.page, () => getReports(encoding), []); await mouseDown('left'); await mouseUp('left'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 51, row: 11, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }, { col: 51, row: 11, state: { action: 'release', button: 'left', modifier: { control: false, shift: false, meta: false } } } ]); @@ -666,7 +646,7 @@ describe('Mouse Tracking Tests', async () => { await mouseMove(cols - 1, rows - 1); await mouseDown('left'); await mouseUp('left'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: cols, row: rows, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }, { col: cols, row: rows, state: { action: 'release', button: 'left', modifier: { control: false, shift: false, meta: false } } } ]); @@ -678,7 +658,7 @@ describe('Mouse Tracking Tests', async () => { await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }, { col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: false, shift: false, meta: false } } } ]); @@ -689,7 +669,7 @@ describe('Mouse Tracking Tests', async () => { // await mouseDown('middle'); // await mouseMove(44, 24); // await mouseUp('middle'); - // await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); + // await pollFor(ctx.page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); // right button // bug: default action not cancelled (popup shown) await mouseMove(43, 24); @@ -697,7 +677,7 @@ describe('Mouse Tracking Tests', async () => { await mouseDown('right'); await mouseMove(44, 24); await mouseUp('right'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'right', modifier: { control: false, shift: false, meta: false } } }, { col: 45, row: 25, state: { action: 'release', button: 'right', modifier: { control: false, shift: false, meta: false } } } ]); @@ -706,21 +686,21 @@ describe('Mouse Tracking Tests', async () => { // await mouseMove(43, 24); // await getReports(encoding); // clear reports // await wheelUp(); - // await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'up', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]); + // await pollFor(ctx.page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'up', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]); // await wheelDown(); - // await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]); + // await pollFor(ctx.page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]); // modifiers // CTRL await mouseMove(43, 24); await getReports(encoding); // clear reports - await page.keyboard.down('Control'); + await ctx.page.keyboard.down('Control'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); // await wheelDown(); - await page.keyboard.up('Control'); - await pollFor(page, () => getReports(encoding), [ + await ctx.page.keyboard.up('Control'); + await pollFor(ctx.page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: false } } }, { col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: true, shift: false, meta: false } } } // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: false } } } @@ -729,13 +709,13 @@ describe('Mouse Tracking Tests', async () => { // ALT await mouseMove(43, 24); await getReports(encoding); // clear reports - await page.keyboard.down('Alt'); + await ctx.page.keyboard.down('Alt'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); // await wheelDown(); - await page.keyboard.up('Alt'); - await pollFor(page, () => getReports(encoding), [ + await ctx.page.keyboard.up('Alt'); + await pollFor(ctx.page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: true } } }, { col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: false, shift: false, meta: true } } } // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: true } } } @@ -745,18 +725,18 @@ describe('Mouse Tracking Tests', async () => { // note: press/release caught by selection manager // await mouseMove(43, 24); // await getReports(encoding); // clear reports - // await page.keyboard.down('Shift'); // defaults to ShiftLeft + // await ctx.page.keyboard.down('Shift'); // defaults to ShiftLeft // await mouseDown('left'); // await mouseMove(44, 24); // await mouseUp('left'); // await wheelDown(); - // await page.keyboard.up('Shift'); + // await ctx.page.keyboard.up('Shift'); // if (noShift) { - // await pollFor(page, () => getReports(encoding), [ + // await pollFor(ctx.page, () => getReports(encoding), [ // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: true, meta: false } } } // ]); // } else { - // await pollFor(page, () => getReports(encoding), [ + // await pollFor(ctx.page, () => getReports(encoding), [ // { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: true, meta: false } } }, // { col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: false, shift: true, meta: false } } }, // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: true, meta: false } } } @@ -767,24 +747,24 @@ describe('Mouse Tracking Tests', async () => { // bug: Shift not working - selection manager? await mouseMove(43, 24); await getReports(encoding); // clear reports - await page.keyboard.down('Control'); - await page.keyboard.down('Alt'); - // await page.keyboard.down('Shift'); + await ctx.page.keyboard.down('Control'); + await ctx.page.keyboard.down('Alt'); + // await ctx.page.keyboard.down('Shift'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); // await wheelDown(); - await page.keyboard.up('Control'); - await page.keyboard.up('Alt'); - // await page.keyboard.up('Shift'); - await pollFor(page, () => getReports(encoding), [ + await ctx.page.keyboard.up('Control'); + await ctx.page.keyboard.up('Alt'); + // await ctx.page.keyboard.up('Shift'); + await pollFor(ctx.page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: true } } }, { col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: true, shift: false, meta: true } } } // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: true } } } ]); }); }); - describe('DECSET 1002 (xterm with drag)', () => { + test.describe('DECSET 1002 (xterm with drag)', () => { /** * - press and release events * - wheel up/down @@ -792,30 +772,34 @@ describe('Mouse Tracking Tests', async () => { * - all modifiers * Note: tmux runs this with SGR encoding. */ - itMouse('default encoding', async () => { + test('default encoding', async () => { + if (ctx.browser.browserType().name() === 'webkit') { + test.skip(); + return; + } const encoding = 'DEFAULT'; await resetMouseModes(); await mouseMove(0, 0); - await writeSync(page, '\x1b[?1002h'); + await ctx.proxy.write('\x1b[?1002h'); // test at 0,0 await mouseDown('left'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 1, row: 1, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } } ]); // mouseup should report, encoding cannot report released button await mouseUp('left'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 1, row: 1, state: { action: 'release', button: '', modifier: { control: false, shift: false, meta: false } } } ]); // mousemove should not report await mouseMove(50, 10); - await pollFor(page, () => getReports(encoding), []); + await pollFor(ctx.page, () => getReports(encoding), []); await mouseDown('left'); await mouseUp('left'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 51, row: 11, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }, { col: 51, row: 11, state: { action: 'release', button: '', modifier: { control: false, shift: false, meta: false } } } ]); @@ -825,7 +809,7 @@ describe('Mouse Tracking Tests', async () => { await mouseMove(223 - 1, rows - 1); await mouseDown('left'); await mouseUp('left'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 223, row: rows, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }, { col: 223, row: rows, state: { action: 'release', button: '', modifier: { control: false, shift: false, meta: false } } } ]); @@ -837,7 +821,7 @@ describe('Mouse Tracking Tests', async () => { await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }, { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: false, shift: false, meta: false } } }, { col: 45, row: 25, state: { action: 'release', button: '', modifier: { control: false, shift: false, meta: false } } } @@ -849,7 +833,7 @@ describe('Mouse Tracking Tests', async () => { // await mouseDown('middle'); // await mouseMove(44, 24); // await mouseUp('middle'); - // await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); + // await pollFor(ctx.page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); // right button // bug: default action not cancelled (popup shown) await mouseMove(43, 24); @@ -857,7 +841,7 @@ describe('Mouse Tracking Tests', async () => { await mouseDown('right'); await mouseMove(44, 24); await mouseUp('right'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'right', modifier: { control: false, shift: false, meta: false } } }, { col: 45, row: 25, state: { action: 'move', button: 'right', modifier: { control: false, shift: false, meta: false } } }, { col: 45, row: 25, state: { action: 'release', button: '', modifier: { control: false, shift: false, meta: false } } } @@ -867,21 +851,21 @@ describe('Mouse Tracking Tests', async () => { // await mouseMove(43, 24); // await getReports(encoding); // clear reports // await wheelUp(); - // await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'up', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]); + // await pollFor(ctx.page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'up', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]); // await wheelDown(); - // await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]); + // await pollFor(ctx.page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]); // modifiers // CTRL await mouseMove(43, 24); await getReports(encoding); // clear reports - await page.keyboard.down('Control'); + await ctx.page.keyboard.down('Control'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); // await wheelDown(); - await page.keyboard.up('Control'); - await pollFor(page, () => getReports(encoding), [ + await ctx.page.keyboard.up('Control'); + await pollFor(ctx.page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: false } } }, { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: true, shift: false, meta: false } } }, { col: 45, row: 25, state: { action: 'release', button: '', modifier: { control: true, shift: false, meta: false } } } @@ -891,13 +875,13 @@ describe('Mouse Tracking Tests', async () => { // ALT await mouseMove(43, 24); await getReports(encoding); // clear reports - await page.keyboard.down('Alt'); + await ctx.page.keyboard.down('Alt'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); // await wheelDown(); - await page.keyboard.up('Alt'); - await pollFor(page, () => getReports(encoding), [ + await ctx.page.keyboard.up('Alt'); + await pollFor(ctx.page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: true } } }, { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: false, shift: false, meta: true } } }, { col: 45, row: 25, state: { action: 'release', button: '', modifier: { control: false, shift: false, meta: true } } } @@ -908,18 +892,18 @@ describe('Mouse Tracking Tests', async () => { // note: press/release/drag caught by selection manager // await mouseMove(43, 24); // await getReports(encoding); // clear reports - // await page.keyboard.down('Shift'); // defaults to ShiftLeft + // await ctx.page.keyboard.down('Shift'); // defaults to ShiftLeft // await mouseDown('left'); // await mouseMove(44, 24); // await mouseUp('left'); // await wheelDown(); - // await page.keyboard.up('Shift'); + // await ctx.page.keyboard.up('Shift'); // if (noShift) { - // await pollFor(page, () => getReports(encoding), [ + // await pollFor(ctx.page, () => getReports(encoding), [ // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: true, meta: false } } } // ]); // } else { - // await pollFor(page, () => getReports(encoding), [ + // await pollFor(ctx.page, () => getReports(encoding), [ // { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: true, meta: false } } }, // { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: false, shift: true, meta: false } } }, // { col: 45, row: 25, state: { action: 'release', button: '', modifier: { control: false, shift: true, meta: false } } }, @@ -931,48 +915,52 @@ describe('Mouse Tracking Tests', async () => { // bug: Shift not working await mouseMove(43, 24); await getReports(encoding); // clear reports - await page.keyboard.down('Control'); - await page.keyboard.down('Alt'); - // await page.keyboard.down('Shift'); + await ctx.page.keyboard.down('Control'); + await ctx.page.keyboard.down('Alt'); + // await ctx.page.keyboard.down('Shift'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); // await wheelDown(); - await page.keyboard.up('Control'); - await page.keyboard.up('Alt'); - // await page.keyboard.up('Shift'); - await pollFor(page, () => getReports(encoding), [ + await ctx.page.keyboard.up('Control'); + await ctx.page.keyboard.up('Alt'); + // await ctx.page.keyboard.up('Shift'); + await pollFor(ctx.page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: true } } }, { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: true, shift: false, meta: true } } }, { col: 45, row: 25, state: { action: 'release', button: '', modifier: { control: true, shift: false, meta: true } } } // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: true } } } ]); }); - itMouse('SGR encoding', async () => { + test('SGR encoding', async () => { + if (ctx.browser.browserType().name() === 'webkit') { + test.skip(); + return; + } const encoding = 'SGR'; await resetMouseModes(); await mouseMove(0, 0); - await writeSync(page, '\x1b[?1002h\x1b[?1006h'); + await ctx.proxy.write('\x1b[?1002h\x1b[?1006h'); // test at 0,0 // bug: release is fired immediately await mouseDown('left'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 1, row: 1, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } } ]); // mouseup should report, encoding cannot report released button await mouseUp('left'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 1, row: 1, state: { action: 'release', button: 'left', modifier: { control: false, shift: false, meta: false } } } ]); // mousemove should not report await mouseMove(50, 10); - await pollFor(page, () => getReports(encoding), []); + await pollFor(ctx.page, () => getReports(encoding), []); await mouseDown('left'); await mouseUp('left'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 51, row: 11, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }, { col: 51, row: 11, state: { action: 'release', button: 'left', modifier: { control: false, shift: false, meta: false } } } ]); @@ -981,7 +969,7 @@ describe('Mouse Tracking Tests', async () => { await mouseMove(cols - 1, rows - 1); await mouseDown('left'); await mouseUp('left'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: cols, row: rows, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }, { col: cols, row: rows, state: { action: 'release', button: 'left', modifier: { control: false, shift: false, meta: false } } } ]); @@ -993,7 +981,7 @@ describe('Mouse Tracking Tests', async () => { await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }, { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: false, shift: false, meta: false } } }, { col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: false, shift: false, meta: false } } } @@ -1005,7 +993,7 @@ describe('Mouse Tracking Tests', async () => { // await mouseDown('middle'); // await mouseMove(44, 24); // await mouseUp('middle'); - // await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); + // await pollFor(ctx.page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); // right button // bug: default action not cancelled (popup shown) await mouseMove(43, 24); @@ -1013,7 +1001,7 @@ describe('Mouse Tracking Tests', async () => { await mouseDown('right'); await mouseMove(44, 24); await mouseUp('right'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'right', modifier: { control: false, shift: false, meta: false } } }, { col: 45, row: 25, state: { action: 'move', button: 'right', modifier: { control: false, shift: false, meta: false } } }, { col: 45, row: 25, state: { action: 'release', button: 'right', modifier: { control: false, shift: false, meta: false } } } @@ -1023,21 +1011,21 @@ describe('Mouse Tracking Tests', async () => { // await mouseMove(43, 24); // await getReports(encoding); // clear reports // await wheelUp(); - // await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'up', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]); + // await pollFor(ctx.page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'up', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]); // await wheelDown(); - // await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]); + // await pollFor(ctx.page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]); // modifiers // CTRL await mouseMove(43, 24); await getReports(encoding); // clear reports - await page.keyboard.down('Control'); + await ctx.page.keyboard.down('Control'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); // await wheelDown(); - await page.keyboard.up('Control'); - await pollFor(page, () => getReports(encoding), [ + await ctx.page.keyboard.up('Control'); + await pollFor(ctx.page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: false } } }, { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: true, shift: false, meta: false } } }, { col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: true, shift: false, meta: false } } } @@ -1047,13 +1035,13 @@ describe('Mouse Tracking Tests', async () => { // ALT await mouseMove(43, 24); await getReports(encoding); // clear reports - await page.keyboard.down('Alt'); + await ctx.page.keyboard.down('Alt'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); // await wheelDown(); - await page.keyboard.up('Alt'); - await pollFor(page, () => getReports(encoding), [ + await ctx.page.keyboard.up('Alt'); + await pollFor(ctx.page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: true } } }, { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: false, shift: false, meta: true } } }, { col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: false, shift: false, meta: true } } } @@ -1064,18 +1052,18 @@ describe('Mouse Tracking Tests', async () => { // note: press/release/drag caught by selection manager // await mouseMove(43, 24); // await getReports(encoding); // clear reports - // await page.keyboard.down('Shift'); // defaults to ShiftLeft + // await ctx.page.keyboard.down('Shift'); // defaults to ShiftLeft // await mouseDown('left'); // await mouseMove(44, 24); // await mouseUp('left'); // await wheelDown(); - // await page.keyboard.up('Shift'); + // await ctx.page.keyboard.up('Shift'); // if (noShift) { - // await pollFor(page, () => getReports(encoding), [ + // await pollFor(ctx.page, () => getReports(encoding), [ // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: true, meta: false } } } // ]); // } else { - // await pollFor(page, () => getReports(encoding), [ + // await pollFor(ctx.page, () => getReports(encoding), [ // { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: true, meta: false } } }, // { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: false, shift: true, meta: false } } }, // { col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: false, shift: true, meta: false } } }, @@ -1087,17 +1075,17 @@ describe('Mouse Tracking Tests', async () => { // bug: this is totally broken with wrong coords and messed up modifiers await mouseMove(43, 24); await getReports(encoding); // clear reports - await page.keyboard.down('Control'); - await page.keyboard.down('Alt'); - // await page.keyboard.down('Shift'); + await ctx.page.keyboard.down('Control'); + await ctx.page.keyboard.down('Alt'); + // await ctx.page.keyboard.down('Shift'); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); // await wheelDown(); - await page.keyboard.up('Control'); - await page.keyboard.up('Alt'); - // await page.keyboard.up('Shift'); - await pollFor(page, () => getReports(encoding), [ + await ctx.page.keyboard.up('Control'); + await ctx.page.keyboard.up('Alt'); + // await ctx.page.keyboard.up('Shift'); + await pollFor(ctx.page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: true } } }, { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: true, shift: false, meta: true } } }, { col: 45, row: 25, state: { action: 'release', button: 'left', modifier: { control: true, shift: false, meta: true } } } @@ -1105,37 +1093,41 @@ describe('Mouse Tracking Tests', async () => { ]); }); }); - describe('DECSET 1003 (xterm any event)', () => { + test.describe('DECSET 1003 (xterm any event)', () => { /** * - all events (press, release, wheel, move) * - all modifiers */ - itMouse('default encoding', async () => { + test('default encoding', async () => { + if (ctx.browser.browserType().name() === 'webkit') { + test.skip(); + return; + } const encoding = 'DEFAULT'; await resetMouseModes(); await mouseMove(0, 0); - await writeSync(page, '\x1b[?1003h'); + await ctx.proxy.write('\x1b[?1003h'); // test at 0,0 await mouseDown('left'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 1, row: 1, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } } ]); // mouseup should report, encoding cannot report released button await mouseUp('left'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 1, row: 1, state: { action: 'release', button: '', modifier: { control: false, shift: false, meta: false } } } ]); // mousemove should report await mouseMove(50, 10); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 51, row: 11, state: { action: 'move', button: '', modifier: { control: false, shift: false, meta: false } } } ]); await mouseDown('left'); await mouseUp('left'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 51, row: 11, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }, { col: 51, row: 11, state: { action: 'release', button: '', modifier: { control: false, shift: false, meta: false } } } ]); @@ -1145,7 +1137,7 @@ describe('Mouse Tracking Tests', async () => { await mouseMove(223 - 1, rows - 1); await mouseDown('left'); await mouseUp('left'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 223, row: rows, state: { action: 'move', button: '', modifier: { control: false, shift: false, meta: false } } }, { col: 223, row: rows, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }, { col: 223, row: rows, state: { action: 'release', button: '', modifier: { control: false, shift: false, meta: false } } } @@ -1157,7 +1149,7 @@ describe('Mouse Tracking Tests', async () => { await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'move', button: '', modifier: { control: false, shift: false, meta: false } } }, { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }, { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: false, shift: false, meta: false } } }, @@ -1170,14 +1162,14 @@ describe('Mouse Tracking Tests', async () => { // await mouseDown('middle'); // await mouseMove(44, 24); // await mouseUp('middle'); - // await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); + // await pollFor(ctx.page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); // right button // bug: default action not cancelled (popup shown) await mouseMove(43, 24); await mouseDown('right'); await mouseMove(44, 24); await mouseUp('right'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'move', button: '', modifier: { control: false, shift: false, meta: false } } }, { col: 44, row: 25, state: { action: 'press', button: 'right', modifier: { control: false, shift: false, meta: false } } }, { col: 45, row: 25, state: { action: 'move', button: 'right', modifier: { control: false, shift: false, meta: false } } }, @@ -1188,20 +1180,20 @@ describe('Mouse Tracking Tests', async () => { // await mouseMove(43, 24); // await getReports(encoding); // clear reports // await wheelUp(); - // await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'up', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]); + // await pollFor(ctx.page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'up', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]); // await wheelDown(); - // await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]); + // await pollFor(ctx.page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]); // modifiers // CTRL - await page.keyboard.down('Control'); + await ctx.page.keyboard.down('Control'); await mouseMove(43, 24); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); // await wheelDown(); - await page.keyboard.up('Control'); - await pollFor(page, () => getReports(encoding), [ + await ctx.page.keyboard.up('Control'); + await pollFor(ctx.page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'move', button: '', modifier: { control: true, shift: false, meta: false } } }, { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: false } } }, { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: true, shift: false, meta: false } } }, @@ -1210,14 +1202,14 @@ describe('Mouse Tracking Tests', async () => { ]); // ALT - await page.keyboard.down('Alt'); + await ctx.page.keyboard.down('Alt'); await mouseMove(43, 24); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); // await wheelDown(); - await page.keyboard.up('Alt'); - await pollFor(page, () => getReports(encoding), [ + await ctx.page.keyboard.up('Alt'); + await pollFor(ctx.page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'move', button: '', modifier: { control: false, shift: false, meta: true } } }, { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: true } } }, { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: false, shift: false, meta: true } } }, @@ -1227,20 +1219,20 @@ describe('Mouse Tracking Tests', async () => { // SHIFT // note: press/release/drag caught by selection manager - // await page.keyboard.down('Shift'); + // await ctx.page.keyboard.down('Shift'); // await mouseMove(43, 24); // await mouseDown('left'); // await mouseMove(44, 24); // await mouseUp('left'); // await wheelDown(); - // await page.keyboard.up('Shift'); + // await ctx.page.keyboard.up('Shift'); // if (noShift) { - // await pollFor(page, () => getReports(encoding), [ + // await pollFor(ctx.page, () => getReports(encoding), [ // { col: 44, row: 25, state: { action: 'move', button: '', modifier: { control: false, shift: true, meta: false } } }, // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: true, meta: false } } } // ]); // } else { - // await pollFor(page, () => getReports(encoding), [ + // await pollFor(ctx.page, () => getReports(encoding), [ // { col: 44, row: 25, state: { action: 'move', button: '', modifier: { control: false, shift: true, meta: false } } }, // { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: true, meta: false } } }, // { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: false, shift: true, meta: false } } }, @@ -1251,18 +1243,18 @@ describe('Mouse Tracking Tests', async () => { // all modifiers // bug: Shift not working - await page.keyboard.down('Control'); - await page.keyboard.down('Alt'); - // await page.keyboard.down('Shift'); + await ctx.page.keyboard.down('Control'); + await ctx.page.keyboard.down('Alt'); + // await ctx.page.keyboard.down('Shift'); await mouseMove(43, 24); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); // await wheelDown(); - await page.keyboard.up('Control'); - await page.keyboard.up('Alt'); - // await page.keyboard.up('Shift'); - await pollFor(page, () => getReports(encoding), [ + await ctx.page.keyboard.up('Control'); + await ctx.page.keyboard.up('Alt'); + // await ctx.page.keyboard.up('Shift'); + await pollFor(ctx.page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'move', button: '', modifier: { control: true, shift: false, meta: true } } }, { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: true } } }, { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: true, shift: false, meta: true } } }, @@ -1270,32 +1262,36 @@ describe('Mouse Tracking Tests', async () => { // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: true, shift: false, meta: true } } } ]); }); - itMouse('SGR encoding', async () => { + test('SGR encoding', async () => { + if (ctx.browser.browserType().name() === 'webkit') { + test.skip(); + return; + } const encoding = 'SGR'; await resetMouseModes(); await mouseMove(0, 0); - await writeSync(page, '\x1b[?1003h\x1b[?1006h'); + await ctx.proxy.write('\x1b[?1003h\x1b[?1006h'); // test at 0,0 await mouseDown('left'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 1, row: 1, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } } ]); // mouseup should report, encoding cannot report released button await mouseUp('left'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 1, row: 1, state: { action: 'release', button: 'left', modifier: { control: false, shift: false, meta: false } } } ]); // mousemove should report await mouseMove(50, 10); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 51, row: 11, state: { action: 'move', button: '', modifier: { control: false, shift: false, meta: false } } } ]); await mouseDown('left'); await mouseUp('left'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 51, row: 11, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }, { col: 51, row: 11, state: { action: 'release', button: 'left', modifier: { control: false, shift: false, meta: false } } } ]); @@ -1306,7 +1302,7 @@ describe('Mouse Tracking Tests', async () => { await mouseMove(cols - 1, rows - 1); await mouseDown('left'); await mouseUp('left'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: cols, row: rows, state: { action: 'move', button: '', modifier: { control: false, shift: false, meta: false } } }, { col: cols, row: rows, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }, { col: cols, row: rows, state: { action: 'release', button: 'left', modifier: { control: false, shift: false, meta: false } } } @@ -1318,7 +1314,7 @@ describe('Mouse Tracking Tests', async () => { await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'move', button: '', modifier: { control: false, shift: false, meta: false } } }, { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: false } } }, { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: false, shift: false, meta: false } } }, @@ -1331,14 +1327,14 @@ describe('Mouse Tracking Tests', async () => { // await mouseDown('middle'); // await mouseMove(44, 24); // await mouseUp('middle'); - // await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); + // await pollFor(ctx.page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); // right button // bug: default action not cancelled (popup shown) await mouseMove(43, 24); await mouseDown('right'); await mouseMove(44, 24); await mouseUp('right'); - await pollFor(page, () => getReports(encoding), [ + await pollFor(ctx.page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'move', button: '', modifier: { control: false, shift: false, meta: false } } }, { col: 44, row: 25, state: { action: 'press', button: 'right', modifier: { control: false, shift: false, meta: false } } }, { col: 45, row: 25, state: { action: 'move', button: 'right', modifier: { control: false, shift: false, meta: false } } }, @@ -1349,20 +1345,20 @@ describe('Mouse Tracking Tests', async () => { // await mouseMove(43, 24); // await getReports(encoding); // clear reports // await wheelUp(); - // await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'up', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]); + // await pollFor(ctx.page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'up', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]); // await wheelDown(); - // await pollFor(page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]); + // await pollFor(ctx.page, () => getReports(encoding), [{ col: 44, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: false, meta: false } } }]); // modifiers // CTRL - await page.keyboard.down('Control'); + await ctx.page.keyboard.down('Control'); await mouseMove(43, 24); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); // await wheelDown(); - await page.keyboard.up('Control'); - await pollFor(page, () => getReports(encoding), [ + await ctx.page.keyboard.up('Control'); + await pollFor(ctx.page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'move', button: '', modifier: { control: true, shift: false, meta: false } } }, { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: false } } }, { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: true, shift: false, meta: false } } }, @@ -1371,14 +1367,14 @@ describe('Mouse Tracking Tests', async () => { ]); // ALT - await page.keyboard.down('Alt'); + await ctx.page.keyboard.down('Alt'); await mouseMove(43, 24); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); // await wheelDown(); - await page.keyboard.up('Alt'); - await pollFor(page, () => getReports(encoding), [ + await ctx.page.keyboard.up('Alt'); + await pollFor(ctx.page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'move', button: '', modifier: { control: false, shift: false, meta: true } } }, { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: false, meta: true } } }, { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: false, shift: false, meta: true } } }, @@ -1388,20 +1384,20 @@ describe('Mouse Tracking Tests', async () => { // SHIFT // note: press/release/drag caught by selection manager - // await page.keyboard.down('Shift'); + // await ctx.page.keyboard.down('Shift'); // await mouseMove(43, 24); // await mouseDown('left'); // await mouseMove(44, 24); // await mouseUp('left'); // await wheelDown(); - // await page.keyboard.up('Shift'); + // await ctx.page.keyboard.up('Shift'); // if (noShift) { - // await pollFor(page, () => getReports(encoding), [ + // await pollFor(ctx.page, () => getReports(encoding), [ // { col: 44, row: 25, state: { action: 'move', button: '', modifier: { control: false, shift: true, meta: false } } }, // { col: 45, row: 25, state: { action: 'down', button: 'wheel', modifier: { control: false, shift: true, meta: false } } } // ]); // } else { - // await pollFor(page, () => getReports(encoding), [ + // await pollFor(ctx.page, () => getReports(encoding), [ // { col: 44, row: 25, state: { action: 'move', button: '', modifier: { control: false, shift: true, meta: false } } }, // { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: false, shift: true, meta: false } } }, // { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: false, shift: true, meta: false } } }, @@ -1412,18 +1408,18 @@ describe('Mouse Tracking Tests', async () => { // all modifiers // bug: Shift not working - await page.keyboard.down('Control'); - await page.keyboard.down('Alt'); - // await page.keyboard.down('Shift'); + await ctx.page.keyboard.down('Control'); + await ctx.page.keyboard.down('Alt'); + // await ctx.page.keyboard.down('Shift'); await mouseMove(43, 24); await mouseDown('left'); await mouseMove(44, 24); await mouseUp('left'); // await wheelDown(); - await page.keyboard.up('Control'); - await page.keyboard.up('Alt'); - // await page.keyboard.up('Shift'); - await pollFor(page, () => getReports(encoding), [ + await ctx.page.keyboard.up('Control'); + await ctx.page.keyboard.up('Alt'); + // await ctx.page.keyboard.up('Shift'); + await pollFor(ctx.page, () => getReports(encoding), [ { col: 44, row: 25, state: { action: 'move', button: '', modifier: { control: true, shift: false, meta: true } } }, { col: 44, row: 25, state: { action: 'press', button: 'left', modifier: { control: true, shift: false, meta: true } } }, { col: 45, row: 25, state: { action: 'move', button: 'left', modifier: { control: true, shift: false, meta: true } } }, diff --git a/test/playwright/TestUtils.ts b/test/playwright/TestUtils.ts index f700392b..5111612e 100644 --- a/test/playwright/TestUtils.ts +++ b/test/playwright/TestUtils.ts @@ -16,6 +16,7 @@ import { EventEmitter } from '../../out/common/EventEmitter'; import deepEqual = require('deep-equal'); export interface ITestContext { + browser: Browser; page: Page; termHandle: JSHandle; proxy: TerminalProxy; @@ -29,6 +30,7 @@ export async function createTestContext(browser: Browser): Promise const proxy = new TerminalProxy(page); proxy.initPage(); return { + browser, page, termHandle: await page.evaluateHandle('window.term'), proxy @@ -366,14 +368,15 @@ export async function openTerminal(ctx: ITestContext, options: ITerminalOptions -export async function pollFor(page: playwright.Page, evalOrFn: string | (() => Promise), val: T, preFn?: () => Promise, maxDuration?: number): Promise { +export async function pollFor(page: playwright.Page, evalOrFn: string | (() => Promise), val: T, preFn?: () => Promise, maxDuration?: number, stack?: string): Promise { + stack ??= new Error().stack; if (preFn) { await preFn(); } const result = typeof evalOrFn === 'string' ? await page.evaluate(evalOrFn) : await evalOrFn(); if (process.env.DEBUG) { - console.log('pollFor result: ', result); + console.log('pollFor result: ', JSON.stringify(result)); } if (!deepEqual(result, val)) { @@ -381,10 +384,16 @@ export async function pollFor(page: playwright.Page, evalOrFn: string | (() = maxDuration = 2000; } if (maxDuration <= 0) { - deepStrictEqual(result, val, 'pollFor max duration exceeded'); + deepStrictEqual(result, val, ([ + `pollFor max duration exceeded.`, + (`Last comparison: ` + + `${typeof result === 'object' ? JSON.stringify(result) : result} !== ` + + `${typeof val === 'object' ? JSON.stringify(val) : val}`), + `Stack: ${stack}` + ].join('\n'))); } return new Promise(r => { - setTimeout(() => r(pollFor(page, evalOrFn, val, preFn, maxDuration! - 10)), 10); + setTimeout(() => r(pollFor(page, evalOrFn, val, preFn, maxDuration! - 10, stack)), 10); }); } } From 1c57b98c9a191e0c6fe4aad1aa521aaa9a653152 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 2 Sep 2023 07:19:32 -0700 Subject: [PATCH 24/40] Migrate InputHandler api tests to playwright --- test/api/InputHandler.api.ts | 1427 -------------------------- test/playwright/InputHandler.test.ts | 1416 +++++++++++++++++++++++++ test/playwright/tsconfig.json | 1 - 3 files changed, 1416 insertions(+), 1428 deletions(-) delete mode 100644 test/api/InputHandler.api.ts create mode 100644 test/playwright/InputHandler.test.ts diff --git a/test/api/InputHandler.api.ts b/test/api/InputHandler.api.ts deleted file mode 100644 index 4059edae..00000000 --- a/test/api/InputHandler.api.ts +++ /dev/null @@ -1,1427 +0,0 @@ -/** - * Copyright (c) 2019 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { assert } from 'chai'; -import { pollFor, openTerminal, getBrowserType, launchBrowser, writeSync } from './TestUtils'; -import { Browser, Page } from '@playwright/test'; -import { IRenderDimensions } from 'browser/renderer/shared/Types'; - -const APP = 'http://127.0.0.1:3001/test'; - -let browser: Browser; -let page: Page; -const width = 800; -const height = 600; - -let isChromium = false; - -describe('InputHandler Integration Tests', function(): void { - before(async () => { - const browserType = getBrowserType(); - isChromium = browserType.name() === 'chromium'; - browser = await launchBrowser(); - page = await (await browser.newContext()).newPage(); - await page.setViewportSize({ width, height }); - await page.goto(APP); - await openTerminal(page); - }); - - after(() => { - browser.close(); - }); - - describe('CSI', () => { - beforeEach(async () => { - await page.evaluate(`window.term.reset()`); - }); - - it('CSI Ps @ - ICH: Insert Ps (Blank) Character(s) (default = 1)', async () => { - await page.evaluate(` - // Default - window.term.write('foo\\x1b[3D\\x1b[@\\n\\r') - // Explicit - window.term.write('bar\\x1b[3D\\x1b[4@') - `); - await pollFor(page, () => getLinesAsArray(2), [' foo', ' bar']); - }); - it.skip('CSI Ps SP @ - SL: Shift left Ps columns(s) (default = 1), ECMA-48', async () => { - // TODO: Implement - }); - it('CSI Ps A - CUU: Cursor Up Ps Times (default = 1)', async () => { - await page.evaluate(` - // Default - window.term.write('\\n\\n\\n\\n\x1b[Aa') - // Explicit - window.term.write('\x1b[2Ab') - `); - await pollFor(page, () => getLinesAsArray(4), ['', ' b', '', 'a']); - }); - it('CSI Ps B - CUD: Cursor Down Ps Times (default = 1)', async () => { - await page.evaluate(` - // Default - window.term.write('\x1b[Ba') - // Explicit - window.term.write('\x1b[2Bb') - `); - await pollFor(page, () => getLinesAsArray(4), ['', 'a', '', ' b']); - }); - it('CSI Ps C - CUF: Cursor Forward Ps Times (default = 1)', async () => { - await page.evaluate(` - // Default - window.term.write('\x1b[Ca') - // Explicit - window.term.write('\x1b[2Cb') - `); - await pollFor(page, () => getLinesAsArray(1), [' a b']); - }); - it('CSI Ps D - CUB: Cursor Backward Ps Times (default = 1)', async () => { - await page.evaluate(` - // Default - window.term.write('foo\x1b[Da') - // Explicit - window.term.write('\x1b[2Db') - `); - await pollFor(page, () => getLinesAsArray(1), ['fba']); - }); - it('CSI Ps E - CNL: Cursor Next Line Ps Times (default = 1)', async () => { - await page.evaluate(` - // Default - window.term.write('\x1b[Ea') - // Explicit - window.term.write('\x1b[2Eb') - `); - await pollFor(page, () => getLinesAsArray(4), ['', 'a', '', 'b']); - }); - it('CSI Ps F - CPL: Cursor Preceding Line Ps Times (default = 1)', async () => { - await page.evaluate(` - // Default - window.term.write('\\n\\n\\n\\n\x1b[Fa') - // Explicit - window.term.write('\x1b[2Fb') - `); - await pollFor(page, () => getLinesAsArray(5), ['', 'b', '', 'a', '']); - }); - it('CSI Ps G - CHA: Cursor Character Absolute [column] (default = [row,1])', async () => { - await page.evaluate(` - // Default - window.term.write('foo\x1b[Ga') - // Explicit - window.term.write('\x1b[10Gb') - `); - await pollFor(page, () => getLinesAsArray(1), ['aoo b']); - }); - it('CSI Ps ; Ps H - CUP: Cursor Position [row;column] (default = [1,1])', async () => { - await page.evaluate(` - // Default - window.term.write('foo\x1b[Ha') - // Explicit - window.term.write('\x1b[3;3Hb') - `); - await pollFor(page, () => getLinesAsArray(3), ['aoo', '', ' b']); - }); - it('CSI Ps I - CHT: Cursor Forward Tabulation Ps tab stops (default = 1)', async () => { - await page.evaluate(` - // Default - window.term.write('\x1b[Ia') - // Explicit - window.term.write('\\n\\r\x1b[2Ib') - `); - await pollFor(page, () => getLinesAsArray(2), [' a', ' b']); - }); - it('CSI Ps J - ED: Erase in Display, VT100', async () => { - const fixture = 'abc\\n\\rdef\\n\\rghi\x1b[2;2H'; - await page.evaluate(` - // Default: Erase Below - window.term.resize(5, 5); - window.term.write('${fixture}\x1b[J') - `); - await pollFor(page, () => getLinesAsArray(3), ['abc', 'd', '']); - await page.evaluate(` - // 0: Erase Below - window.term.reset() - window.term.write('${fixture}\x1b[0J') - `); - await pollFor(page, () => getLinesAsArray(3), ['abc', 'd', '']); - await page.evaluate(` - // 1: Erase Above - window.term.reset() - window.term.write('${fixture}\x1b[1J') - `); - await pollFor(page, () => getLinesAsArray(3), ['', ' f', 'ghi']); - await page.evaluate(` - // 2: Erase Saved Lines (scrollback) - window.term.reset() - window.term.write('1\\n2\\n3\\n4\\n5${fixture}\x1b[3J') - `); - await pollFor(page, () => page.evaluate(`window.term.buffer.active.length`), 5); - await pollFor(page, () => getLinesAsArray(5), [' 4', ' 5', 'abc', 'def', 'ghi']); - }); - it('CSI ? Ps J - DECSED: Erase in Display, VT220', async () => { - const fixture = 'abc\\n\\rdef\\n\\rghi\x1b[2;2H'; - await page.evaluate(` - // Default: Erase Below - window.term.resize(5, 5); - window.term.write('${fixture}\x1b[?J') - `); - await pollFor(page, () => getLinesAsArray(3), ['abc', 'd', '']); - await page.evaluate(` - // 0: Erase Below - window.term.reset() - window.term.write('${fixture}\x1b[?0J') - `); - await pollFor(page, () => getLinesAsArray(3), ['abc', 'd', '']); - await page.evaluate(` - // 1: Erase Above - window.term.reset() - window.term.write('${fixture}\x1b[?1J') - `); - await pollFor(page, () => getLinesAsArray(3), ['', ' f', 'ghi']); - await page.evaluate(` - // 2: Erase Saved Lines (scrollback) - window.term.reset() - window.term.write('1\\n2\\n3\\n4\\n5${fixture}\x1b[?3J') - `); - await pollFor(page, () => page.evaluate(`window.term.buffer.active.length`), 5); - await pollFor(page, () => getLinesAsArray(5), [' 4', ' 5', 'abc', 'def', 'ghi']); - }); - it.skip('CSI Ps K - EL: Erase in Line, VT100', async () => { - // TODO: Implement - }); - it.skip('CSI ? Ps K - DECSEL: Erase in Line, VT220', async () => { - // TODO: Implement - }); - it('CSI Ps L - IL: Insert Ps Line(s) (default = 1)', async () => { - await page.evaluate(` - // Default - window.term.write('foo\x1b[La') - // Explicit - window.term.write('\x1b[2Lb') - `); - await pollFor(page, () => getLinesAsArray(4), ['b', '', 'a', 'foo']); - }); - it('CSI Ps M - DL: Delete Ps Line(s) (default = 1)', async () => { - await page.evaluate(` - // Default - window.term.write('a\\nb\x1b[1F\x1b[M') - // Explicit - window.term.write('\x1b[1Ed\\ne\\nf\x1b[2F\x1b[2M') - `); - await pollFor(page, () => getLinesAsArray(5), [' b', ' f', '', '', '']); - }); - it('CSI Ps P - DCH: Delete Ps Character(s) (default = 1)', async () => { - await page.evaluate(` - // Default - window.term.write('abc\x1b[1;1H\x1b[P') - // Explicit - window.term.write('\\n\\rdef\x1b[2;1H\x1b[2P') - `); - await pollFor(page, () => getLinesAsArray(2), ['bc', 'f']); - }); - it.skip('CSI Pm # P - XTPUSHCOLORS: Push current dynamic- and ANSI-palette colors onto stack, xterm', async () => { - // TODO: Implement - }); - it.skip('CSI Pm # Q - XTPOPCOLORS: Pop stack to set dynamic- and ANSI-palette colors, xterm', async () => { - // TODO: Implement - }); - it.skip('CSI # R - XTREPORTCOLORS: Report the current entry on the palette stack, and the number of palettes stored on the stack, using the same form as XTPOPCOLOR (default = 0), xterm', async () => { - // TODO: Implement - }); - it.skip('CSI Ps S - SU: Scroll up Ps lines (default = 1), VT420, ECMA-48', async () => { - // TODO: Implement - }); - it.skip('CSI ? Pi ; Pa ; Pv S - XTSMGRAPHICS: Set or request graphics attribute, xterm', async () => { - // TODO: Implement - }); - it.skip('CSI Ps T - SD: Scroll down Ps lines (default = 1), VT420', async () => { - // TODO: Implement - }); - it.skip('CSI Ps ; Ps ; Ps ; Ps ; Ps T - XTHIMOUSE: Initiate highlight mouse tracking (XTHIMOUSE), xterm', async () => { - // TODO: Implement - }); - it.skip('CSI > Pm T - XTRMTITLE: Reset title mode features to default value, xterm', async () => { - // TODO: Implement - }); - it.skip('CSI Ps X - ECH: Erase Ps Character(s) (default = 1)', async () => { - // TODO: Implement - }); - it.skip('CSI Ps Z - CBT: Cursor Backward Tabulation Ps tab stops (default = 1)', async () => { - // TODO: Implement - }); - it.skip('CSI Ps ^ - SD: Scroll down Ps lines (default = 1) (SD), ECMA-48', async () => { - // TODO: Implement - }); - it.skip('CSI Ps ` - HPA: Character Position Absolute [column] (default = [row,1])', async () => { - // TODO: Implement - }); - it.skip('CSI Ps a - ', async () => { - // TODO: Implement - }); - it('CSI Ps b - REP: Repeat preceding character, ECMA48', async () => { - // default to 1 - await page.evaluate(` - window.term.resize(10, 10); - window.term.write('#\x1b[b'); - window.term.writeln(''); - window.term.write('#\x1b[0b'); - window.term.writeln(''); - window.term.write('#\x1b[1b'); - window.term.writeln(''); - window.term.write('#\x1b[5b'); - `); - await pollFor(page, () => getLinesAsArray(4), ['##', '##', '##', '######']); - await pollFor(page, () => getCursor(), { col: 6, row: 3 }); - // should not repeat on fullwidth chars - await page.evaluate(` - window.term.reset(); - window.term.write('¥\x1b[10b'); - `); - await pollFor(page, () => getLinesAsArray(1), ['¥']); - // should repeat only base char of combining - await page.evaluate(` - window.term.reset(); - window.term.write('e\u0301\x1b[5b'); - `); - await pollFor(page, () => getLinesAsArray(1), ['e\u0301eeeee']); - // should wrap correctly - await page.evaluate(` - window.term.reset(); - window.term.write('#\x1b[15b'); - `); - await pollFor(page, () => getLinesAsArray(2), ['##########', '######']); - await page.evaluate(` - window.term.reset(); - window.term.write('\x1b[?7l'); // disable wrap around - window.term.write('#\x1b[15b'); - `); - await pollFor(page, () => getLinesAsArray(2), ['##########', '']); - // any successful sequence should reset REP - await page.evaluate(` - window.term.reset(); - window.term.write('\x1b[?7h'); // re-enable wrap around - window.term.write('#\\n\x1b[3b'); - window.term.write('#\\r\x1b[3b'); - window.term.writeln(''); - window.term.write('abcdefg\x1b[3D\x1b[10b#\x1b[3b'); - `); - await pollFor(page, () => getLinesAsArray(3), ['#', ' #', 'abcd####']); - }); - it.skip('CSI Ps c - ', async () => { - // TODO: Implement - }); - it.skip('CSI = Ps c - ', async () => { - // TODO: Implement - }); - it.skip('CSI > Ps c - ', async () => { - // TODO: Implement - }); - it.skip('CSI Ps d - ', async () => { - // TODO: Implement - }); - it.skip('CSI Ps e - ', async () => { - // TODO: Implement - }); - it.skip('CSI Ps ; Ps f - ', async () => { - // TODO: Implement - }); - it.skip('CSI Ps g - ', async () => { - // TODO: Implement - }); - it.skip('CSI Ps h - ', async () => { - // TODO: Implement - }); - describe('CSI ? Pm h - DECSET: Private Mode Set', () => { - it.skip('Ps = 1 - Application Cursor Keys (DECCKM), VT100', async () => { - // TODO: Implement - }); - it.skip('Ps = 2 - Designate USASCII for character sets G0-G3 (DECANM), VT100, and set VT100 mode', async () => { - // TODO: Implement - }); - it.skip('Ps = 3 - 132 Column Mode (DECCOLM), VT100', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 - Smooth (Slow) Scroll (DECSCLM), VT100', async () => { - // TODO: Implement - }); - it.skip('Ps = 5 - Reverse Video (DECSCNM), VT100', async () => { - // TODO: Implement - }); - it.skip('Ps = 6 - Origin Mode (DECOM), VT100', async () => { - // TODO: Implement - }); - it.skip('Ps = 7 - Auto-Wrap Mode (DECAWM), VT100', async () => { - // TODO: Implement - }); - it.skip('Ps = 8 - Auto-Repeat Keys (DECARM), VT100', async () => { - // TODO: Implement - }); - it.skip('Ps = 9 - Send Mouse X & Y on button press', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 - Show toolbar (rxvt)', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 2 - Start blinking cursor (AT&T 610)', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 3 - Start blinking cursor (set only via resource or menu)', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 4 - Enable XOR of blinking cursor control sequence and menu', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 8 - Print Form Feed (DECPFF), VT220', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 9 - Set print extent to full screen (DECPEX), VT220', async () => { - // TODO: Implement - }); - it.skip('Ps = 2 5 - Show cursor (DECTCEM), VT220', async () => { - // TODO: Implement - }); - it.skip('Ps = 3 0 - Show scrollbar (rxvt)', async () => { - // TODO: Implement - }); - it.skip('Ps = 3 5 - Enable font-shifting functions (rxvt)', async () => { - // TODO: Implement - }); - it.skip('Ps = 3 8 - Enter Tektronix mode (DECTEK), VT240, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 0 - Allow 80 ⇒ 132 mode, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 1 - more(1) fix (see curses resource)', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 2 - Enable National Replacement Character sets (DECNRCM), VT220', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 3 - Enable Graphic Expanded Print Mode (DECGEPM), VT340', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 4 - Turn on margin bell, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 4 - Enable Graphic Print Color Mode (DECGPCM), VT340', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 5 - Reverse-wraparound mode (XTREVWRAP), xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 5 - Enable Graphic Print Color Syntax (DECGPCS), VT340', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 6 - Start logging (XTLOGGING), xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 6 - Graphic Print Background Mode, VT340', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 7 - Use Alternate Screen Buffer, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 7 - Enable Graphic Rotated Print Mode (DECGRPM), VT340', async () => { - // TODO: Implement - }); - it.skip('Ps = 6 6 - Application keypad mode (DECNKM), VT320', async () => { - // TODO: Implement - }); - it.skip('Ps = 6 7 - Backarrow key sends backspace (DECBKM), VT340, VT420', async () => { - // TODO: Implement - }); - it.skip('Ps = 6 9 - Enable left and right margin mode (DECLRMM), VT420 and up', async () => { - // TODO: Implement - }); - it.skip('Ps = 8 0 - Enable Sixel Display Mode (DECSDM), VT330, VT340, VT382', async () => { - // TODO: Implement - }); - it.skip('Ps = 9 5 - Do not clear screen when DECCOLM is set/reset (DECNCSM), VT510 and up', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 0 0 - Send Mouse X & Y on button press and release', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 0 1 - Use Hilite Mouse Tracking, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 0 2 - Use Cell Motion Mouse Tracking, xterm', async () => { - // TODO: Implement - }); - it('Ps = 1 0 0 3 - Set Use All Motion (any event) Mouse Tracking', async () => { - const coords: { left: number, top: number, bottom: number, right: number } = await page.evaluate(` - (function() { - const rect = window.term.element.getBoundingClientRect(); - return { left: rect.left, top: rect.top, bottom: rect.bottom, right: rect.right }; - })(); - `); - // Click and drag and ensure there is a selection - await page.mouse.click((coords.left + coords.right) / 2, (coords.top + coords.bottom) / 2); - await page.mouse.down(); - await page.mouse.move((coords.left + coords.right) / 2, (coords.top + coords.bottom) / 4); - assert.ok(await page.evaluate(`window.term.getSelection().length`) as number > 0, 'mouse events are off so there should be a selection'); - await page.mouse.up(); - // Clear selection - await page.mouse.click((coords.left + coords.right) / 2, (coords.top + coords.bottom) / 2); - await pollFor(page, () => page.evaluate(`window.term.getSelection().length`), 0); - // Enable mouse events - await page.evaluate(`window.term.write('\x1b[?1003h')`); - // Click and drag and ensure there is no selection - await page.mouse.click((coords.left + coords.right) / 2, (coords.top + coords.bottom) / 2); - await page.mouse.down(); - await page.mouse.move((coords.left + coords.right) / 2, (coords.top + coords.bottom) / 4); - // mouse events are on so there should be no selection - await pollFor(page, () => page.evaluate(`window.term.getSelection().length`), 0); - await page.mouse.up(); - }); - it.skip('Ps = 1 0 0 4 - Send FocusIn/FocusOut events, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 0 5 - Enable UTF-8 Mouse Mode, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 0 6 - Enable SGR Mouse Mode, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 0 7 - Enable Alternate Scroll Mode, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 1 0 - Scroll to bottom on tty output (rxvt)', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 1 1 - Scroll to bottom on key press (rxvt)', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 1 5 - Enable urxvt Mouse Mode', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 1 6 - Enable SGR Mouse PixelMode, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 3 4 - Interpret "meta" key, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 3 5 - Enable special modifiers for Alt and NumLock keys, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 3 6 - Send ESC when Meta modifies a key, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 3 7 - Send DEL from the editing-keypad Delete key, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 3 9 - Send ESC when Alt modifies a key, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 4 0 - Keep selection even if not highlighted, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 4 1 - Use the CLIPBOARD selection, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 4 2 - Enable Urgency window manager hint when Control-G is received, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 4 3 - Enable raising of the window when Control-G is received, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 4 4 - Reuse the most recent data copied to CLIPBOARD, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 4 5 - XTREVWRAP2: Extended Reverse-wraparound mode, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 4 6 - Enable switching to/from Alternate Screen Buffer, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 4 7 - Use Alternate Screen Buffer, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 4 8 - Save cursor as in DECSC, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 4 9 - Save cursor as in DECSC, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 5 0 - Set terminfo/termcap function-key mode, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 5 1 - Set Sun function-key mode, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 5 2 - Set HP function-key mode, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 5 3 - Set SCO function-key mode, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 6 0 - Set legacy keyboard emulation, i.e, X11R6, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 6 1 - Set VT220 keyboard emulation, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 2 0 0 1 - Enable readline mouse button-1, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 2 0 0 2 - Enable readline mouse button-2, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 2 0 0 3 - Enable readline mouse button-3, xterm', async () => { - // TODO: Implement - }); - (isChromium ? it : it.skip)('Pm = 2 0 0 4, Set bracketed paste mode', async () => { - await pollFor(page, () => simulatePaste('foo'), 'foo'); - await page.evaluate(`window.term.write('\x1b[?2004h')`); - await pollFor(page, () => simulatePaste('bar'), '\x1b[200~bar\x1b[201~'); - await page.evaluate(`window.term.write('\x1b[?2004l')`); - await pollFor(page, () => simulatePaste('baz'), 'baz'); - }); - it.skip('Ps = 2 0 0 5 - Enable readline character-quoting, xterm', async () => { - // TODO: Implement - }); - it.skip('Ps = 2 0 0 6 - Enable readline newline pasting, xterm', async () => { - // TODO: Implement - }); - }); - it.skip('CSI Ps i - MC: Media Copy', async () => { - // TODO: Implement - }); - it.skip('CSI ? Ps i - MC: Media Copy, DEC-specified', async () => { - // TODO: Implement - }); - it.skip('CSI Pm l - RM: Reset Mode', async () => { - // TODO: Implement - }); - describe('CSI ? Pm l - DECRST: DEC Private Mode Reset', async () => { - it.skip('Ps = 1 - Normal Cursor Keys (DECCKM), VT100.', async () => { - // TODO: Implement - }); - it.skip('Ps = 2 - Designate VT52 mode (DECANM), VT100.', async () => { - // TODO: Implement - }); - it.skip('Ps = 3 - 80 Column Mode (DECCOLM), VT100.', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 - Jump (Fast) Scroll (DECSCLM), VT100.', async () => { - // TODO: Implement - }); - it.skip('Ps = 5 - Normal Video (DECSCNM), VT100.', async () => { - // TODO: Implement - }); - it.skip('Ps = 6 - Normal Cursor Mode (DECOM), VT100.', async () => { - // TODO: Implement - }); - it.skip('Ps = 7 - No Auto-Wrap Mode (DECAWM), VT100.', async () => { - // TODO: Implement - }); - it.skip('Ps = 8 - No Auto-Repeat Keys (DECARM), VT100.', async () => { - // TODO: Implement - }); - it.skip('Ps = 9 - Don\'t send Mouse X & Y on button press, xterm.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 - Hide toolbar (rxvt).', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 2 - Stop blinking cursor (AT&T 610).', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 3 - Disable blinking cursor (reset only via resource or menu).', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 4 - Disable XOR of blinking cursor control sequence and menu.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 8 - Don\'t Print Form Feed (DECPFF), VT220.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 9 - Limit print to scrolling region (DECPEX), VT220.', async () => { - // TODO: Implement - }); - it.skip('Ps = 2 5 - Hide cursor (DECTCEM), VT220.', async () => { - // TODO: Implement - }); - it.skip('Ps = 3 0 - Don\'t show scrollbar (rxvt).', async () => { - // TODO: Implement - }); - it.skip('Ps = 3 5 - Disable font-shifting functions (rxvt).', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 0 - Disallow 80 ⇒ 132 mode, xterm.', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 1 - No more(1) fix (see curses resource).', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 2 - Disable National Replacement Character sets (DECNRCM), VT220.', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 3 - Disable Graphic Expanded Print Mode (DECGEPM), VT340.', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 4 - Turn off margin bell, xterm.', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 4 - Disable Graphic Print Color Mode (DECGPCM), VT340.', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 5 - No Reverse-wraparound mode (XTREVWRAP), xterm.', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 5 - Disable Graphic Print Color Syntax (DECGPCS), VT340.', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 6 - Stop logging (XTLOGGING), xterm. This is normally disabled by a compile-time option.', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 7 - Use Normal Screen Buffer, xterm.', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 7 - Disable Graphic Rotated Print Mode (DECGRPM), VT340.', async () => { - // TODO: Implement - }); - it.skip('Ps = 6 6 - Numeric keypad mode (DECNKM), VT320.', async () => { - // TODO: Implement - }); - it.skip('Ps = 6 7 - Backarrow key sends delete (DECBKM), VT340, VT420. This sets the backarrowKey resource to "false".', async () => { - // TODO: Implement - }); - it.skip('Ps = 6 9 - Disable left and right margin mode (DECLRMM), VT420 and up.', async () => { - // TODO: Implement - }); - it.skip('Ps = 8 0 - Disable Sixel Display Mode (DECSDM), VT330, VT340, VT382. Turns on "Sixel Scrolling". See the section Sixel Graphics and mode 8 4 5 2 .', async () => { - // TODO: Implement - }); - it.skip('Ps = 9 5 - Clear screen when DECCOLM is set/reset (DECNCSM), VT510 and up.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 0 0 - Don\'t send Mouse X & Y on button press and release. See the section Mouse Tracking.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 0 1 - Don\'t use Hilite Mouse Tracking, xterm.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 0 2 - Don\'t use Cell Motion Mouse Tracking, xterm. See the section Button-event tracking.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 0 3 - Don\'t use All Motion Mouse Tracking, xterm. See the section Any-event tracking.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 0 4 - Don\'t send FocusIn/FocusOut events, xterm.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 0 5 - Disable UTF-8 Mouse Mode, xterm.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 0 6 - Disable SGR Mouse Mode, xterm.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 0 7 - Disable Alternate Scroll Mode, xterm. This corresponds to the alternateScroll resource.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 1 0 - Don\'t scroll to bottom on tty output (rxvt). This sets the scrollTtyOutput resource to "false".', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 1 1 - Don\'t scroll to bottom on key press (rxvt). This sets the scrollKey resource to "false".', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 1 5 - Disable urxvt Mouse Mode.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 1 6 - Disable SGR Mouse Pixel-Mode, xterm.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 3 4 - Don\'t interpret "meta" key, xterm. This disables the eightBitInput resource.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 3 5 - Disable special modifiers for Alt and NumLock keys, xterm. This disables the numLock resource.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 3 6 - Don\'t send ESC when Meta modifies a key, xterm. This disables the metaSendsEscape resource.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 3 7 - Send VT220 Remove from the editing-keypad Delete key, xterm.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 3 9 - Don\'t send ESC when Alt modifies a key, xterm. This disables the altSendsEscape resource.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 4 0 - Do not keep selection when not highlighted, xterm. This disables the keepSelection resource.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 4 1 - Use the PRIMARY selection, xterm. This disables the selectToClipboard resource.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 4 2 - Disable Urgency window manager hint when Control-G is received, xterm. This disables the bellIsUrgent resource.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 4 3 - Disable raising of the window when Control- G is received, xterm. This disables the popOnBell resource.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 4 5 - No Extended Reverse-wraparound mode (XTREVWRAP2), xterm.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 4 6 - Disable switching to/from Alternate Screen Buffer, xterm. This works for terminfo-based systems, updating the titeInhibit resource. If currently using the Alternate Screen Buffer, xterm switches to the Normal Screen Buffer.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 4 7 - Use Normal Screen Buffer, xterm. Clear the screen first if in the Alternate Screen Buffer. This may be disabled by the titeInhibit resource.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 4 8 - Restore cursor as in DECRC, xterm. This may be disabled by the titeInhibit resource.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 4 9 - Use Normal Screen Buffer and restore cursor as in DECRC, xterm. This may be disabled by the titeInhibit resource. This combines the effects of the 1 0 4 7 and 1 0 4 8 modes. Use this with terminfo-based applications rather than the 4 7 mode.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 5 0 - Reset terminfo/termcap function-key mode, xterm.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 5 1 - Reset Sun function-key mode, xterm.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 5 2 - Reset HP function-key mode, xterm.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 5 3 - Reset SCO function-key mode, xterm.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 6 0 - Reset legacy keyboard emulation, i.e, X11R6, xterm.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 6 1 - Reset keyboard emulation to Sun/PC style, xterm.', async () => { - // TODO: Implement - }); - it.skip('Ps = 2 0 0 1 - Disable readline mouse button-1, xterm.', async () => { - // TODO: Implement - }); - it.skip('Ps = 2 0 0 2 - Disable readline mouse button-2, xterm.', async () => { - // TODO: Implement - }); - it.skip('Ps = 2 0 0 3 - Disable readline mouse button-3, xterm.', async () => { - // TODO: Implement - }); - it.skip('Ps = 2 0 0 4 - Reset bracketed paste mode, xterm.', async () => { - // TODO: Implement - }); - it.skip('Ps = 2 0 0 5 - Disable readline character-quoting, xterm.', async () => { - // TODO: Implement - }); - it.skip('Ps = 2 0 0 6 - Disable readline newline pasting, xterm.', async () => { - // TODO: Implement - }); - }); - describe('CSI Pm m - SGR: Character Attributes', () => { - it.skip('Ps = 0 - Normal (default), VT100.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 - Bold, VT100.', async () => { - // TODO: Implement - }); - it.skip('Ps = 2 - Faint, decreased intensity, ECMA-48 2nd.', async () => { - // TODO: Implement - }); - it.skip('Ps = 3 - Italicized, ECMA-48 2nd.', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 - Underlined, VT100.', async () => { - // TODO: Implement - }); - it.skip('Ps = 5 - Blink, VT100. This appears as Bold in X11R6 xterm.', async () => { - // TODO: Implement - }); - it.skip('Ps = 7 - Inverse, VT100.', async () => { - // TODO: Implement - }); - it.skip('Ps = 8 - Invisible, i.e., hidden, ECMA-48 2nd, VT300.', async () => { - // TODO: Implement - }); - it.skip('Ps = 9 - Crossed-out characters, ECMA-48 3rd.', async () => { - // TODO: Implement - }); - it.skip('Ps = 2 1 - Doubly-underlined, ECMA-48 3rd.', async () => { - // TODO: Implement - }); - it.skip('Ps = 2 2 - Normal (neither bold nor faint), ECMA-48 3rd.', async () => { - // TODO: Implement - }); - it.skip('Ps = 2 3 - Not italicized, ECMA-48 3rd.', async () => { - // TODO: Implement - }); - it.skip('Ps = 2 4 - Not underlined, ECMA-48 3rd.', async () => { - // TODO: Implement - }); - it.skip('Ps = 2 5 - Steady (not blinking), ECMA-48 3rd.', async () => { - // TODO: Implement - }); - it.skip('Ps = 2 7 - Positive (not inverse), ECMA-48 3rd.', async () => { - // TODO: Implement - }); - it.skip('Ps = 2 8 - Visible, i.e., not hidden, ECMA-48 3rd, VT300.', async () => { - // TODO: Implement - }); - it.skip('Ps = 2 9 - Not crossed-out, ECMA-48 3rd.', async () => { - // TODO: Implement - }); - it.skip('Ps = 3 0 - Set foreground color to Black.', async () => { - // TODO: Implement - }); - it.skip('Ps = 3 1 - Set foreground color to Red.', async () => { - // TODO: Implement - }); - it.skip('Ps = 3 2 - Set foreground color to Green.', async () => { - // TODO: Implement - }); - it.skip('Ps = 3 3 - Set foreground color to Yellow.', async () => { - // TODO: Implement - }); - it.skip('Ps = 3 4 - Set foreground color to Blue.', async () => { - // TODO: Implement - }); - it.skip('Ps = 3 5 - Set foreground color to Magenta.', async () => { - // TODO: Implement - }); - it.skip('Ps = 3 6 - Set foreground color to Cyan.', async () => { - // TODO: Implement - }); - it.skip('Ps = 3 7 - Set foreground color to White.', async () => { - // TODO: Implement - }); - it.skip('Ps = 3 9 - Set foreground color to default, ECMA-48 3rd.', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 0 - Set background color to Black.', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 1 - Set background color to Red.', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 2 - Set background color to Green.', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 3 - Set background color to Yellow.', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 4 - Set background color to Blue.', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 5 - Set background color to Magenta.', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 6 - Set background color to Cyan.', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 7 - Set background color to White.', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 9 - Set background color to default, ECMA-48 3rd.', async () => { - // TODO: Implement - }); - it.skip('Ps = 9 0 - Set foreground color to Black.', async () => { - // TODO: Implement - }); - it.skip('Ps = 9 1 - Set foreground color to Red.', async () => { - // TODO: Implement - }); - it.skip('Ps = 9 2 - Set foreground color to Green.', async () => { - // TODO: Implement - }); - it.skip('Ps = 9 3 - Set foreground color to Yellow.', async () => { - // TODO: Implement - }); - it.skip('Ps = 9 4 - Set foreground color to Blue.', async () => { - // TODO: Implement - }); - it.skip('Ps = 9 5 - Set foreground color to Magenta.', async () => { - // TODO: Implement - }); - it.skip('Ps = 9 6 - Set foreground color to Cyan.', async () => { - // TODO: Implement - }); - it.skip('Ps = 9 7 - Set foreground color to White.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 0 - Set background color to Black.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 1 - Set background color to Red.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 2 - Set background color to Green.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 3 - Set background color to Yellow.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 4 - Set background color to Blue.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 5 - Set background color to Magenta.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 6 - Set background color to Cyan.', async () => { - // TODO: Implement - }); - it.skip('Ps = 1 0 7 - Set background color to White.', async () => { - // TODO: Implement - }); - it.skip('Ps = 3 8 : 2 : Pi : Pr : Pg : Pb- Set foreground color using RGB values', async () => { - // TODO: Implement - }); - it.skip('Ps = 3 8 : 5 : Ps- Set foreground color to Ps, using indexed color', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 8 : 2 : Pi : Pr : Pg : Pb- Set background color using RGB values', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 8 : 5 : Ps- Set background color to Ps, using indexed color', async () => { - // TODO: Implement - }); - it.skip('Ps = 3 8 ; 2 ; Pr ; Pg ; Pb- Set foreground color using RGB values', async () => { - // TODO: Implement - }); - it.skip('Ps = 4 8 ; 2 ; Pr ; Pg ; Pb- Set background color using RGB values', async () => { - // TODO: Implement - }); - }); - it.skip('CSI > Pp [; Pv] m - XTMODKEYS: Set/reset key modifier options, xterm', () => { - // TODO: Implement - }); - it.skip('CSI ? Pp m - XTQMODKEYS: Query key modifier options, xterm', () => { - // TODO: Implement - }); - describe('CSI Ps n - DSR: Device Status Report', () => { - it('Status Report - CSI 5 n', async () => { - await page.evaluate(` - window.term.onData(e => window.result = e); - window.term.write('\\x1b[5n'); - `); - await pollFor(page, () => page.evaluate(`window.result`), '\x1b[0n'); - }); - - it('Report Cursor Position (CPR) - CSI 6 n', async () => { - await page.evaluate(`window.term.write('\\n\\nfoo')`); - await pollFor(page, () => page.evaluate(` - [window.term.buffer.active.cursorY, window.term.buffer.active.cursorX] - `), [2, 3]); - await page.evaluate(` - window.term.onData(e => window.result = e); - window.term.write('\\x1b[6n'); - `); - await pollFor(page, () => page.evaluate(`window.result`), '\x1b[3;4R'); - }); - - it('Report Cursor Position (DECXCPR) - CSI ? 6 n', async () => { - await page.evaluate(`window.term.write('\\n\\nfoo')`); - await pollFor(page, () => page.evaluate(` - [window.term.buffer.active.cursorY, window.term.buffer.active.cursorX] - `), [2, 3]); - await page.evaluate(` - window.term.onData(e => window.result = e); - window.term.write('\\x1b[?6n'); - `); - await pollFor(page, () => page.evaluate(`window.result`), '\x1b[?3;4R'); - }); - }); - it.skip('CSI > Ps n - Disable key modifier options, xterm', () => { - // TODO: Implement - }); - describe.skip('CSI ? Ps n - DSR: Device Status Report (DEC-specific).', () => { - // TODO: Implement - }); - it.skip('CSI > Ps p - XTSMPOINTER: Set resource value pointerMode, xterm', () => { - // TODO: Implement - }); - it.skip('CSI ! p - DECSTR: Soft terminal reset, VT220 and up.', () => { - // TODO: Implement - }); - it.skip('CSI Pl ; Pc " p - DECSCL: Set conformance level, VT220 and up.', () => { - // TODO: Implement - }); - it.skip('CSI Ps $ p - DECRQM: Request ANSI mode', () => { - // TODO: Implement - }); - it.skip('CSI ? Ps $ p - Request DEC private mode (DECRQM).', () => { - // TODO: Implement - }); - it.skip('CSI [Pm] # p - Push video attributes onto stack (XTPUSHSGR), xterm. This is an alias for CSI # { , used to work around language limitations of C#.', async () => { - // TODO: Implement - }); - it.skip('CSI > Ps q - Report xterm name and version (XTVERSION).', async () => { - // TODO: Implement - }); - it.skip('CSI Ps q - Load LEDs (DECLL), VT100.', async () => { - // TODO: Implement - }); - it.skip('CSI Ps SP q - Set cursor style (DECSCUSR), VT520.', async () => { - // TODO: Implement - }); - it.skip('CSI Ps " q - Select character protection attribute (DECSCA), VT220.', async () => { - // TODO: Implement - }); - it.skip('CSI # q - Pop video attributes from stack (XTPOPSGR), xterm.', async () => { - // TODO: Implement - }); - it.skip('CSI Ps ; Ps r - Set Scrolling Region [top;bottom] (default = full size of window) (DECSTBM), VT100.', async () => { - // TODO: Implement - }); - it.skip('CSI ? Pm r - Restore DEC Private Mode Values (XTRESTORE), xterm.', async () => { - // TODO: Implement - }); - it.skip('CSI Pt ; Pl ; Pb ; Pr ; Pm $ r - Change Attributes in Rectangular Area (DECCARA), VT400 and up.', async () => { - // TODO: Implement - }); - it.skip('CSI s - Save cursor, available only when DECLRMM is disabled (SCOSC, also ANSI.SYS).', async () => { - // TODO: Implement - }); - it.skip('CSI Pl ; Pr s - Set left and right margins (DECSLRM), VT420 and up.', async () => { - // TODO: Implement - }); - it.skip('CSI > Ps s - Set/reset shift-escape options (XTSHIFTESCAPE), xterm.', async () => { - // TODO: Implement - }); - it.skip('CSI ? Pm s - Save DEC Private Mode Values (XTSAVE), xterm. Ps values are the same as for DECSET.', async () => { - // TODO: Implement - }); - it.skip('CSI > Pm t - This xterm control sets one or more features of the title modes (XTSMTITLE), xterm.', async () => { - // TODO: Implement - }); - it.skip('CSI Ps SP t - Set warning-bell volume (DECSWBV), VT520.', async () => { - // TODO: Implement - }); - it.skip('CSI Pt ; Pl ; Pb ; Pr ; Pm $ t - Reverse Attributes in Rectangular Area (DECRARA), VT400 and up.', async () => { - // TODO: Implement - }); - it.skip('CSI u - Restore cursor (SCORC, also ANSI.SYS).', async () => { - // TODO: Implement - }); - it.skip('CSI Ps SP u - Set margin-bell volume (DECSMBV), VT520.', async () => { - // TODO: Implement - }); - it.skip('CSI Pt ; Pl ; Pb ; Pr ; Pp ; Pt ; Pl ; Pp $ v - Copy Rectangular Area (DECCRA), VT400 and up.', async () => { - // TODO: Implement - }); - it.skip('CSI Ps $ w - Request presentation state report (DECRQPSR), VT320 and up.', async () => { - // TODO: Implement - }); - it.skip('CSI Pt ; Pl ; Pb ; Pr \' w - Enable Filter Rectangle (DECEFR), VT420 and up.', async () => { - // TODO: Implement - }); - it.skip('CSI Ps x - Request Terminal Parameters (DECREQTPARM).', async () => { - // TODO: Implement - }); - it.skip('CSI Ps * x - Select Attribute Change Extent (DECSACE), VT420 and up.', async () => { - // TODO: Implement - }); - it.skip('CSI Pc ; Pt ; Pl ; Pb ; Pr $ x - Fill Rectangular Area (DECFRA), VT420 and up.', async () => { - // TODO: Implement - }); - it.skip('CSI Ps # y - Select checksum extension (XTCHECKSUM), xterm.', async () => { - // TODO: Implement - }); - it.skip('CSI Pi ; Pg ; Pt ; Pl ; Pb ; Pr * y - Request Checksum of Rectangular Area (DECRQCRA), VT420 and up.', async () => { - // TODO: Implement - }); - it.skip('CSI Ps ; Pu \' z - Enable Locator Reporting (DECELR).', async () => { - // TODO: Implement - }); - it.skip('CSI Pt ; Pl ; Pb ; Pr $ z - Erase Rectangular Area (DECERA), VT400 and up.', async () => { - // TODO: Implement - }); - it.skip('CSI Pm \' { - Select Locator Events (DECSLE).', async () => { - // TODO: Implement - }); - it.skip('CSI [Pm] # { Push video attributes onto stack (XTPUSHSGR), xterm.', async () => { - // TODO: Implement - }); - it.skip('CSI Pt ; Pl ; Pb ; Pr $ { - Selective Erase Rectangular Area (DECSERA), VT400 and up.', async () => { - // TODO: Implement - }); - it.skip('CSI Pt ; Pl ; Pb ; Pr # | - Report selected graphic rendition (XTREPORTSGR), xterm.', async () => { - // TODO: Implement - }); - it.skip('CSI Ps $ | - Select columns per page (DECSCPP), VT340.', async () => { - // TODO: Implement - }); - it.skip('CSI Ps \' | - Request Locator Position (DECRQLP).', async () => { - // TODO: Implement - }); - it.skip('CSI Ps * | - Select number of lines per screen (DECSNLS), VT420 and up.', async () => { - // TODO: Implement - }); - it.skip('CSI # } - Pop video attributes from stack (XTPOPSGR), xterm.', async () => { - // TODO: Implement - }); - it.skip('CSI Ps \' } - Insert Ps Column(s) (default = 1) (DECIC), VT420 and up.', async () => { - // TODO: Implement - }); - it.skip('CSI Ps $ } - Select active status display (DECSASD), VT320 and up.', async () => { - // TODO: Implement - }); - it.skip('CSI Ps \' ~ - Delete Ps Column(s) (default = 1) (DECDC), VT420 and up.', async () => { - // TODO: Implement - }); - it.skip('CSI Ps $ ~ - Select status line type (DECSSDT), VT320 and up.', async () => { - // TODO: Implement - }); - describe('CSI Ps ; Ps ; Ps t - Window Options', () => { - it('should be disabled by default', async () => { - await page.evaluate(`(() => { - window._stack = []; - const _h = window.term.onData(data => window._stack.push(data)); - window.term.write('\x1b[14t'); - window.term.write('\x1b[16t'); - window.term.write('\x1b[18t'); - window.term.write('\x1b[20t'); - window.term.write('\x1b[21t'); - return new Promise((r) => window.term.write('', () => { _h.dispose(); r(); })); - })()`); - await pollFor(page, async () => await page.evaluate(`(() => _stack)()`), []); - }); - it('14 - GetWinSizePixels', async () => { - await page.evaluate(`window.term.options.windowOptions = { getWinSizePixels: true }; `); - await page.evaluate(`(() => { - window._stack = []; - const _h = window.term.onData(data => window._stack.push(data)); - window.term.write('\x1b[14t'); - return new Promise((r) => window.term.write('', () => { _h.dispose(); r(); })); - })()`); - const d = await getDimensions(); - await pollFor(page, async () => await page.evaluate(`(() => _stack)()`), [`\x1b[4;${d.height};${d.width}t`]); - }); - it('16 - GetCellSizePixels', async () => { - await page.evaluate(`window.term.options.windowOptions = { getCellSizePixels: true }; `); - await page.evaluate(`(() => { - window._stack = []; - const _h = window.term.onData(data => window._stack.push(data)); - window.term.write('\x1b[16t'); - return new Promise((r) => window.term.write('', () => { _h.dispose(); r(); })); - })()`); - const d = await getDimensions(); - await pollFor(page, async () => await page.evaluate(`(() => _stack)()`), [`\x1b[6;${d.cellHeight};${d.cellWidth}t`]); - }); - }); - }); - - describe('OSC', () => { - describe('OSC 4', () => { - before(async () => { - await page.evaluate('(() => {window._recordedData = []; window._h = term.onData(d => window._recordedData.push(d));})()'); - }); - after(async () => { - await page.evaluate('window._h.dispose()'); - }); - beforeEach(async () => { - await page.evaluate('window._recordedData.length = 0;'); - }); - it('query single color', async () => { - await writeSync(page, '\x1b]4;0;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]4;0;rgb:2e2e/3434/3636\x1b\\']); - await writeSync(page, '\x1b]4;77;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]4;0;rgb:2e2e/3434/3636\x1b\\', '\x1b]4;77;rgb:5f5f/d7d7/5f5f\x1b\\']); - }); - it('query multiple colors', async () => { - await writeSync(page, '\x1b]4;0;?;77;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]4;0;rgb:2e2e/3434/3636\x1b\\', '\x1b]4;77;rgb:5f5f/d7d7/5f5f\x1b\\']); - }); - it('set & query single color', async () => { - await writeSync(page, '\x1b]4;0;?\x07'); - const restore: string[] = await page.evaluate('window._recordedData'); - assert.deepEqual(await page.evaluate('window._recordedData'), restore); - // set new color & query - await writeSync(page, '\x1b]4;0;rgb:01/02/03\x07\x1b]4;0;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), [restore[0], '\x1b]4;0;rgb:0101/0202/0303\x1b\\']); - // restore should set old color - await writeSync(page, restore[0] + '\x1b]4;0;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), [restore[0], '\x1b]4;0;rgb:0101/0202/0303\x1b\\', restore[0]]); - }); - it('query & set colors mixed', async () => { - await writeSync(page, '\x1b]4;0;?;77;?\x07'); - const restore: string[] = await page.evaluate('window._recordedData'); - await page.evaluate('window._recordedData.length = 0;'); - // mixed call - change 0, query 43, change 77 - await writeSync(page, '\x1b]4;0;rgb:01/02/03;43;?;77;#aabbcc\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]4;43;rgb:0000/d7d7/afaf\x1b\\']); - await page.evaluate('window._recordedData.length = 0;'); - // query new values for 0 + 77 - await writeSync(page, '\x1b]4;0;?;77;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]4;0;rgb:0101/0202/0303\x1b\\', '\x1b]4;77;rgb:aaaa/bbbb/cccc\x1b\\']); - await page.evaluate('window._recordedData.length = 0;'); - // restore old values for 0 + 77 - await writeSync(page, restore[0] + restore[1] + '\x1b]4;0;?;77;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), restore); - }); - }); - describe('OSC 4 & 104', () => { - before(async () => { - await page.evaluate('(() => {window._recordedData = []; window._h = term.onData(d => window._recordedData.push(d));})()'); - }); - after(async () => { - await page.evaluate('window._h.dispose()'); - }); - beforeEach(async () => { - await page.evaluate('window._recordedData.length = 0;'); - }); - it('change & restore single color', async () => { - // test for some random color slots - for (const i of [0, 43, 77, 255]) { - await writeSync(page, `\x1b]4;${i};?\x07`); - const restore: string[] = await page.evaluate('window._recordedData'); - await writeSync(page, `\x1b]4;${i};rgb:01/02/03\x07\x1b]4;${i};?\x07`); - assert.deepEqual(await page.evaluate('window._recordedData'), [restore[0], `\x1b]4;${i};rgb:0101/0202/0303\x1b\\`]); - // restore slot color - await writeSync(page, `\x1b]104;${i}\x07\x1b]4;${i};?\x07`); - assert.deepEqual(await page.evaluate('window._recordedData'), [restore[0], `\x1b]4;${i};rgb:0101/0202/0303\x1b\\`, restore[0]]); - await page.evaluate('window._recordedData.length = 0;'); - } - }); - it('restore multiple at once', async () => { - // change 3 random slots - await writeSync(page, `\x1b]4;0;?;43;?;77;?\x07`); - const restore: string[] = await page.evaluate('window._recordedData'); - await page.evaluate('window._recordedData.length = 0;'); - await writeSync(page, `\x1b]4;0;rgb:01/02/03;43;#aabbcc;77;#123456\x07`); - // restore specific slots - await writeSync(page, `\x1b]104;0;43;77\x07` + `\x1b]4;0;?;43;?;77;?\x07`); - assert.deepEqual(await page.evaluate('window._recordedData'), restore); - }); - it('restore full table', async () => { - // change 3 random slots - await writeSync(page, `\x1b]4;0;?;43;?;77;?\x07`); - const restore: string[] = await page.evaluate('window._recordedData'); - await page.evaluate('window._recordedData.length = 0;'); - await writeSync(page, `\x1b]4;0;rgb:01/02/03;43;#aabbcc;77;#123456\x07`); - // restore all - await writeSync(page, `\x1b]104\x07` + `\x1b]4;0;?;43;?;77;?\x07`); - assert.deepEqual(await page.evaluate('window._recordedData'), restore); - }); - }); - describe('OSC 10 & 11 + 110 | 111 | 112', () => { - before(async () => { - await page.evaluate('(() => {window._recordedData = []; window._h = term.onData(d => window._recordedData.push(d));})()'); - }); - after(async () => { - await page.evaluate('window._h.dispose()'); - }); - beforeEach(async () => { - await page.evaluate('window._recordedData.length = 0;'); - }); - it('query FG color', async () => { - await writeSync(page, '\x1b]10;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:ffff/ffff/ffff\x1b\\']); - }); - it('query BG color', async () => { - await writeSync(page, '\x1b]11;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]11;rgb:0000/0000/0000\x1b\\']); - }); - it('query FG & BG color in one call', async () => { - await writeSync(page, '\x1b]10;?;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:ffff/ffff/ffff\x1b\\', '\x1b]11;rgb:0000/0000/0000\x1b\\']); - }); - it('set & query FG', async () => { - await writeSync(page, '\x1b]10;rgb:1/2/3\x07\x1b]10;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:1111/2222/3333\x1b\\']); - await writeSync(page, '\x1b]10;#ffffff\x07\x1b]10;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:1111/2222/3333\x1b\\', '\x1b]10;rgb:ffff/ffff/ffff\x1b\\']); - }); - it('set & query BG', async () => { - await writeSync(page, '\x1b]11;rgb:1/2/3\x07\x1b]11;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]11;rgb:1111/2222/3333\x1b\\']); - await writeSync(page, '\x1b]11;#000000\x07\x1b]11;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]11;rgb:1111/2222/3333\x1b\\', '\x1b]11;rgb:0000/0000/0000\x1b\\']); - }); - it('set & query cursor color', async () => { - await writeSync(page, '\x1b]12;rgb:1/2/3\x07\x1b]12;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]12;rgb:1111/2222/3333\x1b\\']); - await writeSync(page, '\x1b]12;#ffffff\x07\x1b]12;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]12;rgb:1111/2222/3333\x1b\\', '\x1b]12;rgb:ffff/ffff/ffff\x1b\\']); - }); - it('set & query FG & BG color in one call', async () => { - await writeSync(page, '\x1b]10;#123456;rgb:aa/bb/cc\x07\x1b]10;?;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:1212/3434/5656\x1b\\', '\x1b]11;rgb:aaaa/bbbb/cccc\x1b\\']); - await writeSync(page, '\x1b]10;#ffffff;#000000\x07'); - }); - it('OSC 110: restore FG color', async () => { - await writeSync(page, '\x1b]10;rgb:1/2/3\x07\x1b]10;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:1111/2222/3333\x1b\\']); - await page.evaluate('window._recordedData.length = 0;'); - // restore - await writeSync(page, '\x1b]110\x07\x1b]10;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:ffff/ffff/ffff\x1b\\']); - }); - it('OSC 111: restore BG color', async () => { - await writeSync(page, '\x1b]11;rgb:1/2/3\x07\x1b]11;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]11;rgb:1111/2222/3333\x1b\\']); - await page.evaluate('window._recordedData.length = 0;'); - // restore - await writeSync(page, '\x1b]111\x07\x1b]11;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]11;rgb:0000/0000/0000\x1b\\']); - }); - it('OSC 112: restore cursor color', async () => { - await writeSync(page, '\x1b]12;rgb:1/2/3\x07\x1b]12;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]12;rgb:1111/2222/3333\x1b\\']); - await page.evaluate('window._recordedData.length = 0;'); - // restore - await writeSync(page, '\x1b]112\x07\x1b]12;?\x07'); - assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]12;rgb:ffff/ffff/ffff\x1b\\']); - }); - }); - }); - - describe('ESC', () => { - describe('DECRC: Save cursor, ESC 7', () => { - it('should save the absolute cursor position so resizing restores to the correct position', async () => { - await page.evaluate(` - window.term.resize(10, 2); - window.term.write('1\\n\\r2\\n\\r3\\n\\r4\\n\\r5'); - window.term.write('\\x1b7\\x1b[?47h'); - `); - await page.evaluate(` - window.term.resize(10, 4); - window.term.write('\\x1b[?47l\\x1b8'); - `); - await pollFor(page, () => getCursor(), { col: 1, row: 3 }); - }); - }); - }); -}); - -async function getLinesAsArray(count: number, start: number = 0): Promise { - let text = ''; - for (let i = start; i < start + count; i++) { - text += `window.term.buffer.active.getLine(${i}).translateToString(true),`; - } - return await page.evaluate(`[${text}]`); -} - -async function simulatePaste(text: string): Promise { - const id = Math.floor(Math.random() * 1000000); - await page.evaluate(` - (function() { - window.term.onData(e => window.result_${id} = e); - const clipboardData = new DataTransfer(); - clipboardData.setData('text/plain', '${text}'); - window.term.textarea.dispatchEvent(new ClipboardEvent('paste', { clipboardData })); - })(); - `); - return await page.evaluate(`window.result_${id} `); -} - -async function getCursor(): Promise<{ col: number, row: number }> { - return page.evaluate(` - (function() { - return {col: term.buffer.active.cursorX, row: term.buffer.active.cursorY}; - })(); - `); -} - -async function getDimensions(): Promise { - const dim: IRenderDimensions = await page.evaluate(`term._core._renderService.dimensions`); - return { - cellWidth: dim.css.cell.width.toFixed(0), - cellHeight: dim.css.cell.height.toFixed(0), - width: dim.css.canvas.width.toFixed(0), - height: dim.css.canvas.height.toFixed(0) - }; -} diff --git a/test/playwright/InputHandler.test.ts b/test/playwright/InputHandler.test.ts new file mode 100644 index 00000000..1b5f225a --- /dev/null +++ b/test/playwright/InputHandler.test.ts @@ -0,0 +1,1416 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ +import { test } from '@playwright/test'; +import { deepStrictEqual, ok, strictEqual } from 'assert'; +import type { IDisposable } from 'xterm'; +import { createTestContext, ITestContext, openTerminal, pollFor } from './TestUtils'; +import { IRenderDimensions } from 'browser/renderer/shared/Types'; + +let ctx: ITestContext; +test.beforeAll(async ({ browser }) => { + ctx = await createTestContext(browser); + await openTerminal(ctx); +}); +test.afterAll(async () => await ctx.page.close()); + + +test.describe('InputHandler Integration Tests', function (): void { + + test.describe('CSI', () => { + test.beforeEach(async () => await ctx.proxy.reset()); + + test('CSI Ps @ - ICH: Insert Ps (Blank) Character(s) (default = 1)', async () => { + await ctx.page.evaluate(` + // Default + window.term.write('foo\\x1b[3D\\x1b[@\\n\\r') + // Explicit + window.term.write('bar\\x1b[3D\\x1b[4@') + `); + await pollFor(ctx.page, () => getLinesAsArray(2), [' foo', ' bar']); + }); + test.skip('CSI Ps SP @ - SL: Shift left Ps columns(s) (default = 1), ECMA-48', async () => { + // TODO: Implement + }); + test('CSI Ps A - CUU: Cursor Up Ps Times (default = 1)', async () => { + await ctx.page.evaluate(` + // Default + window.term.write('\\n\\n\\n\\n\x1b[Aa') + // Explicit + window.term.write('\x1b[2Ab') + `); + await pollFor(ctx.page, () => getLinesAsArray(4), ['', ' b', '', 'a']); + }); + test('CSI Ps B - CUD: Cursor Down Ps Times (default = 1)', async () => { + await ctx.page.evaluate(` + // Default + window.term.write('\x1b[Ba') + // Explicit + window.term.write('\x1b[2Bb') + `); + await pollFor(ctx.page, () => getLinesAsArray(4), ['', 'a', '', ' b']); + }); + test('CSI Ps C - CUF: Cursor Forward Ps Times (default = 1)', async () => { + await ctx.page.evaluate(` + // Default + window.term.write('\x1b[Ca') + // Explicit + window.term.write('\x1b[2Cb') + `); + await pollFor(ctx.page, () => getLinesAsArray(1), [' a b']); + }); + test('CSI Ps D - CUB: Cursor Backward Ps Times (default = 1)', async () => { + await ctx.page.evaluate(` + // Default + window.term.write('foo\x1b[Da') + // Explicit + window.term.write('\x1b[2Db') + `); + await pollFor(ctx.page, () => getLinesAsArray(1), ['fba']); + }); + test('CSI Ps E - CNL: Cursor Next Line Ps Times (default = 1)', async () => { + await ctx.page.evaluate(` + // Default + window.term.write('\x1b[Ea') + // Explicit + window.term.write('\x1b[2Eb') + `); + await pollFor(ctx.page, () => getLinesAsArray(4), ['', 'a', '', 'b']); + }); + test('CSI Ps F - CPL: Cursor Preceding Line Ps Times (default = 1)', async () => { + await ctx.page.evaluate(` + // Default + window.term.write('\\n\\n\\n\\n\x1b[Fa') + // Explicit + window.term.write('\x1b[2Fb') + `); + await pollFor(ctx.page, () => getLinesAsArray(5), ['', 'b', '', 'a', '']); + }); + test('CSI Ps G - CHA: Cursor Character Absolute [column] (default = [row,1])', async () => { + await ctx.page.evaluate(` + // Default + window.term.write('foo\x1b[Ga') + // Explicit + window.term.write('\x1b[10Gb') + `); + await pollFor(ctx.page, () => getLinesAsArray(1), ['aoo b']); + }); + test('CSI Ps ; Ps H - CUP: Cursor Position [row;column] (default = [1,1])', async () => { + await ctx.page.evaluate(` + // Default + window.term.write('foo\x1b[Ha') + // Explicit + window.term.write('\x1b[3;3Hb') + `); + await pollFor(ctx.page, () => getLinesAsArray(3), ['aoo', '', ' b']); + }); + test('CSI Ps I - CHT: Cursor Forward Tabulation Ps tab stops (default = 1)', async () => { + await ctx.page.evaluate(` + // Default + window.term.write('\x1b[Ia') + // Explicit + window.term.write('\\n\\r\x1b[2Ib') + `); + await pollFor(ctx.page, () => getLinesAsArray(2), [' a', ' b']); + }); + test('CSI Ps J - ED: Erase in Display, VT100', async () => { + const fixture = 'abc\\n\\rdef\\n\\rghi\x1b[2;2H'; + await ctx.page.evaluate(` + // Default: Erase Below + window.term.resize(5, 5); + window.term.write('${fixture}\x1b[J') + `); + await pollFor(ctx.page, () => getLinesAsArray(3), ['abc', 'd', '']); + await ctx.page.evaluate(` + // 0: Erase Below + window.term.reset() + window.term.write('${fixture}\x1b[0J') + `); + await pollFor(ctx.page, () => getLinesAsArray(3), ['abc', 'd', '']); + await ctx.page.evaluate(` + // 1: Erase Above + window.term.reset() + window.term.write('${fixture}\x1b[1J') + `); + await pollFor(ctx.page, () => getLinesAsArray(3), ['', ' f', 'ghi']); + await ctx.page.evaluate(` + // 2: Erase Saved Lines (scrollback) + window.term.reset() + window.term.write('1\\n2\\n3\\n4\\n5${fixture}\x1b[3J') + `); + await pollFor(ctx.page, () => ctx.page.evaluate(`window.term.buffer.active.length`), 5); + await pollFor(ctx.page, () => getLinesAsArray(5), [' 4', ' 5', 'abc', 'def', 'ghi']); + }); + test('CSI ? Ps J - DECSED: Erase in Display, VT220', async () => { + const fixture = 'abc\\n\\rdef\\n\\rghi\x1b[2;2H'; + await ctx.page.evaluate(` + // Default: Erase Below + window.term.resize(5, 5); + window.term.write('${fixture}\x1b[?J') + `); + await pollFor(ctx.page, () => getLinesAsArray(3), ['abc', 'd', '']); + await ctx.page.evaluate(` + // 0: Erase Below + window.term.reset() + window.term.write('${fixture}\x1b[?0J') + `); + await pollFor(ctx.page, () => getLinesAsArray(3), ['abc', 'd', '']); + await ctx.page.evaluate(` + // 1: Erase Above + window.term.reset() + window.term.write('${fixture}\x1b[?1J') + `); + await pollFor(ctx.page, () => getLinesAsArray(3), ['', ' f', 'ghi']); + await ctx.page.evaluate(` + // 2: Erase Saved Lines (scrollback) + window.term.reset() + window.term.write('1\\n2\\n3\\n4\\n5${fixture}\x1b[?3J') + `); + await pollFor(ctx.page, () => ctx.page.evaluate(`window.term.buffer.active.length`), 5); + await pollFor(ctx.page, () => getLinesAsArray(5), [' 4', ' 5', 'abc', 'def', 'ghi']); + }); + test.skip('CSI Ps K - EL: Erase in Line, VT100', async () => { + // TODO: Implement + }); + test.skip('CSI ? Ps K - DECSEL: Erase in Line, VT220', async () => { + // TODO: Implement + }); + test('CSI Ps L - IL: Insert Ps Line(s) (default = 1)', async () => { + await ctx.page.evaluate(` + // Default + window.term.write('foo\x1b[La') + // Explicit + window.term.write('\x1b[2Lb') + `); + await pollFor(ctx.page, () => getLinesAsArray(4), ['b', '', 'a', 'foo']); + }); + test('CSI Ps M - DL: Delete Ps Line(s) (default = 1)', async () => { + await ctx.page.evaluate(` + // Default + window.term.write('a\\nb\x1b[1F\x1b[M') + // Explicit + window.term.write('\x1b[1Ed\\ne\\nf\x1b[2F\x1b[2M') + `); + await pollFor(ctx.page, () => getLinesAsArray(5), [' b', ' f', '', '', '']); + }); + test('CSI Ps P - DCH: Delete Ps Character(s) (default = 1)', async () => { + await ctx.page.evaluate(` + // Default + window.term.write('abc\x1b[1;1H\x1b[P') + // Explicit + window.term.write('\\n\\rdef\x1b[2;1H\x1b[2P') + `); + await pollFor(ctx.page, () => getLinesAsArray(2), ['bc', 'f']); + }); + test.skip('CSI Pm # P - XTPUSHCOLORS: Push current dynamic- and ANSI-palette colors onto stack, xterm', async () => { + // TODO: Implement + }); + test.skip('CSI Pm # Q - XTPOPCOLORS: Pop stack to set dynamic- and ANSI-palette colors, xterm', async () => { + // TODO: Implement + }); + test.skip('CSI # R - XTREPORTCOLORS: Report the current entry on the palette stack, and the number of palettes stored on the stack, using the same form as XTPOPCOLOR (default = 0), xterm', async () => { + // TODO: Implement + }); + test.skip('CSI Ps S - SU: Scroll up Ps lines (default = 1), VT420, ECMA-48', async () => { + // TODO: Implement + }); + test.skip('CSI ? Pi ; Pa ; Pv S - XTSMGRAPHICS: Set or request graphics attribute, xterm', async () => { + // TODO: Implement + }); + test.skip('CSI Ps T - SD: Scroll down Ps lines (default = 1), VT420', async () => { + // TODO: Implement + }); + test.skip('CSI Ps ; Ps ; Ps ; Ps ; Ps T - XTHIMOUSE: Initiate highlight mouse tracking (XTHIMOUSE), xterm', async () => { + // TODO: Implement + }); + test.skip('CSI > Pm T - XTRMTITLE: Reset title mode features to default value, xterm', async () => { + // TODO: Implement + }); + test.skip('CSI Ps X - ECH: Erase Ps Character(s) (default = 1)', async () => { + // TODO: Implement + }); + test.skip('CSI Ps Z - CBT: Cursor Backward Tabulation Ps tab stops (default = 1)', async () => { + // TODO: Implement + }); + test.skip('CSI Ps ^ - SD: Scroll down Ps lines (default = 1) (SD), ECMA-48', async () => { + // TODO: Implement + }); + test.skip('CSI Ps ` - HPA: Character Position Absolute [column] (default = [row,1])', async () => { + // TODO: Implement + }); + test.skip('CSI Ps a - ', async () => { + // TODO: Implement + }); + test('CSI Ps b - REP: Repeat preceding character, ECMA48', async () => { + // default to 1 + await ctx.page.evaluate(` + window.term.resize(10, 10); + window.term.write('#\x1b[b'); + window.term.writeln(''); + window.term.write('#\x1b[0b'); + window.term.writeln(''); + window.term.write('#\x1b[1b'); + window.term.writeln(''); + window.term.write('#\x1b[5b'); + `); + await pollFor(ctx.page, () => getLinesAsArray(4), ['##', '##', '##', '######']); + await pollFor(ctx.page, () => getCursor(), { col: 6, row: 3 }); + // should not repeat on fullwidth chars + await ctx.page.evaluate(` + window.term.reset(); + window.term.write('¥\x1b[10b'); + `); + await pollFor(ctx.page, () => getLinesAsArray(1), ['¥']); + // should repeat only base char of combining + await ctx.page.evaluate(` + window.term.reset(); + window.term.write('e\u0301\x1b[5b'); + `); + await pollFor(ctx.page, () => getLinesAsArray(1), ['e\u0301eeeee']); + // should wrap correctly + await ctx.page.evaluate(` + window.term.reset(); + window.term.write('#\x1b[15b'); + `); + await pollFor(ctx.page, () => getLinesAsArray(2), ['##########', '######']); + await ctx.page.evaluate(` + window.term.reset(); + window.term.write('\x1b[?7l'); // disable wrap around + window.term.write('#\x1b[15b'); + `); + await pollFor(ctx.page, () => getLinesAsArray(2), ['##########', '']); + // any successful sequence should reset REP + await ctx.page.evaluate(` + window.term.reset(); + window.term.write('\x1b[?7h'); // re-enable wrap around + window.term.write('#\\n\x1b[3b'); + window.term.write('#\\r\x1b[3b'); + window.term.writeln(''); + window.term.write('abcdefg\x1b[3D\x1b[10b#\x1b[3b'); + `); + await pollFor(ctx.page, () => getLinesAsArray(3), ['#', ' #', 'abcd####']); + }); + test.skip('CSI Ps c - ', async () => { + // TODO: Implement + }); + test.skip('CSI = Ps c - ', async () => { + // TODO: Implement + }); + test.skip('CSI > Ps c - ', async () => { + // TODO: Implement + }); + test.skip('CSI Ps d - ', async () => { + // TODO: Implement + }); + test.skip('CSI Ps e - ', async () => { + // TODO: Implement + }); + test.skip('CSI Ps ; Ps f - ', async () => { + // TODO: Implement + }); + test.skip('CSI Ps g - ', async () => { + // TODO: Implement + }); + test.skip('CSI Ps h - ', async () => { + // TODO: Implement + }); + test.describe('CSI ? Pm h - DECSET: Private Mode Set', () => { + test.skip('Ps = 1 - Application Cursor Keys (DECCKM), VT100', async () => { + // TODO: Implement + }); + test.skip('Ps = 2 - Designate USASCII for character sets G0-G3 (DECANM), VT100, and set VT100 mode', async () => { + // TODO: Implement + }); + test.skip('Ps = 3 - 132 Column Mode (DECCOLM), VT100', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 - Smooth (Slow) Scroll (DECSCLM), VT100', async () => { + // TODO: Implement + }); + test.skip('Ps = 5 - Reverse Video (DECSCNM), VT100', async () => { + // TODO: Implement + }); + test.skip('Ps = 6 - Origin Mode (DECOM), VT100', async () => { + // TODO: Implement + }); + test.skip('Ps = 7 - Auto-Wrap Mode (DECAWM), VT100', async () => { + // TODO: Implement + }); + test.skip('Ps = 8 - Auto-Repeat Keys (DECARM), VT100', async () => { + // TODO: Implement + }); + test.skip('Ps = 9 - Send Mouse X & Y on button press', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 - Show toolbar (rxvt)', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 2 - Start blinking cursor (AT&T 610)', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 3 - Start blinking cursor (set only via resource or menu)', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 4 - Enable XOR of blinking cursor control sequence and menu', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 8 - Print Form Feed (DECPFF), VT220', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 9 - Set print extent to full screen (DECPEX), VT220', async () => { + // TODO: Implement + }); + test.skip('Ps = 2 5 - Show cursor (DECTCEM), VT220', async () => { + // TODO: Implement + }); + test.skip('Ps = 3 0 - Show scrollbar (rxvt)', async () => { + // TODO: Implement + }); + test.skip('Ps = 3 5 - Enable font-shifting functions (rxvt)', async () => { + // TODO: Implement + }); + test.skip('Ps = 3 8 - Enter Tektronix mode (DECTEK), VT240, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 0 - Allow 80 ⇒ 132 mode, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 1 - more(1) fix (see curses resource)', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 2 - Enable National Replacement Character sets (DECNRCM), VT220', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 3 - Enable Graphic Expanded Print Mode (DECGEPM), VT340', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 4 - Turn on margin bell, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 4 - Enable Graphic Print Color Mode (DECGPCM), VT340', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 5 - Reverse-wraparound mode (XTREVWRAP), xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 5 - Enable Graphic Print Color Syntax (DECGPCS), VT340', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 6 - Start logging (XTLOGGING), xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 6 - Graphic Print Background Mode, VT340', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 7 - Use Alternate Screen Buffer, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 7 - Enable Graphic Rotated Print Mode (DECGRPM), VT340', async () => { + // TODO: Implement + }); + test.skip('Ps = 6 6 - Application keypad mode (DECNKM), VT320', async () => { + // TODO: Implement + }); + test.skip('Ps = 6 7 - Backarrow key sends backspace (DECBKM), VT340, VT420', async () => { + // TODO: Implement + }); + test.skip('Ps = 6 9 - Enable left and right margin mode (DECLRMM), VT420 and up', async () => { + // TODO: Implement + }); + test.skip('Ps = 8 0 - Enable Sixel Display Mode (DECSDM), VT330, VT340, VT382', async () => { + // TODO: Implement + }); + test.skip('Ps = 9 5 - Do not clear screen when DECCOLM is set/reset (DECNCSM), VT510 and up', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 0 0 - Send Mouse X & Y on button press and release', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 0 1 - Use Hilite Mouse Tracking, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 0 2 - Use Cell Motion Mouse Tracking, xterm', async () => { + // TODO: Implement + }); + test('Ps = 1 0 0 3 - Set Use All Motion (any event) Mouse Tracking', async () => { + const coords: { left: number, top: number, bottom: number, right: number } = await ctx.page.evaluate(` + (function() { + const rect = window.term.element.getBoundingClientRect(); + return { left: rect.left, top: rect.top, bottom: rect.bottom, right: rect.right }; + })(); + `); + // Click and drag and ensure there is a selection + await ctx.page.mouse.click((coords.left + coords.right) / 2, (coords.top + coords.bottom) / 2); + await ctx.page.mouse.down(); + await ctx.page.mouse.move((coords.left + coords.right) / 2, (coords.top + coords.bottom) / 4); + ok(await ctx.page.evaluate(`window.term.getSelection().length`) as number > 0, 'mouse events are off so there should be a selection'); + await ctx.page.mouse.up(); + // Clear selection + await ctx.page.mouse.click((coords.left + coords.right) / 2, (coords.top + coords.bottom) / 2); + await pollFor(ctx.page, () => ctx.page.evaluate(`window.term.getSelection().length`), 0); + // Enable mouse events + await ctx.page.evaluate(`window.term.write('\x1b[?1003h')`); + // Click and drag and ensure there is no selection + await ctx.page.mouse.click((coords.left + coords.right) / 2, (coords.top + coords.bottom) / 2); + await ctx.page.mouse.down(); + await ctx.page.mouse.move((coords.left + coords.right) / 2, (coords.top + coords.bottom) / 4); + // mouse events are on so there should be no selection + await pollFor(ctx.page, () => ctx.page.evaluate(`window.term.getSelection().length`), 0); + await ctx.page.mouse.up(); + }); + test.skip('Ps = 1 0 0 4 - Send FocusIn/FocusOut events, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 0 5 - Enable UTF-8 Mouse Mode, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 0 6 - Enable SGR Mouse Mode, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 0 7 - Enable Alternate Scroll Mode, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 1 0 - Scroll to bottom on tty output (rxvt)', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 1 1 - Scroll to bottom on key press (rxvt)', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 1 5 - Enable urxvt Mouse Mode', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 1 6 - Enable SGR Mouse PixelMode, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 3 4 - Interpret "meta" key, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 3 5 - Enable special modifiers for Alt and NumLock keys, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 3 6 - Send ESC when Meta modifies a key, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 3 7 - Send DEL from the editing-keypad Delete key, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 3 9 - Send ESC when Alt modifies a key, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 4 0 - Keep selection even if not highlighted, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 4 1 - Use the CLIPBOARD selection, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 4 2 - Enable Urgency window manager hint when Control-G is received, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 4 3 - Enable raising of the window when Control-G is received, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 4 4 - Reuse the most recent data copied to CLIPBOARD, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 4 5 - XTREVWRAP2: Extended Reverse-wraparound mode, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 4 6 - Enable switching to/from Alternate Screen Buffer, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 4 7 - Use Alternate Screen Buffer, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 4 8 - Save cursor as in DECSC, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 4 9 - Save cursor as in DECSC, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 5 0 - Set terminfo/termcap function-key mode, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 5 1 - Set Sun function-key mode, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 5 2 - Set HP function-key mode, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 5 3 - Set SCO function-key mode, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 6 0 - Set legacy keyboard emulation, i.e, X11R6, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 6 1 - Set VT220 keyboard emulation, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 2 0 0 1 - Enable readline mouse button-1, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 2 0 0 2 - Enable readline mouse button-2, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 2 0 0 3 - Enable readline mouse button-3, xterm', async () => { + // TODO: Implement + }); + test('Pm = 2 0 0 4, Set bracketed paste mode', async () => { + if (ctx.browser.browserType().name() !== 'chromium') { + test.skip(); + return; + } + await pollFor(ctx.page, () => simulatePaste('foo'), 'foo'); + await ctx.page.evaluate(`window.term.write('\x1b[?2004h')`); + await pollFor(ctx.page, () => simulatePaste('bar'), '\x1b[200~bar\x1b[201~'); + await ctx.page.evaluate(`window.term.write('\x1b[?2004l')`); + await pollFor(ctx.page, () => simulatePaste('baz'), 'baz'); + }); + test.skip('Ps = 2 0 0 5 - Enable readline character-quoting, xterm', async () => { + // TODO: Implement + }); + test.skip('Ps = 2 0 0 6 - Enable readline newline pasting, xterm', async () => { + // TODO: Implement + }); + }); + test.skip('CSI Ps i - MC: Media Copy', async () => { + // TODO: Implement + }); + test.skip('CSI ? Ps i - MC: Media Copy, DEC-specified', async () => { + // TODO: Implement + }); + test.skip('CSI Pm l - RM: Reset Mode', async () => { + // TODO: Implement + }); + test.describe('CSI ? Pm l - DECRST: DEC Private Mode Reset', async () => { + test.skip('Ps = 1 - Normal Cursor Keys (DECCKM), VT100.', async () => { + // TODO: Implement + }); + test.skip('Ps = 2 - Designate VT52 mode (DECANM), VT100.', async () => { + // TODO: Implement + }); + test.skip('Ps = 3 - 80 Column Mode (DECCOLM), VT100.', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 - Jump (Fast) Scroll (DECSCLM), VT100.', async () => { + // TODO: Implement + }); + test.skip('Ps = 5 - Normal Video (DECSCNM), VT100.', async () => { + // TODO: Implement + }); + test.skip('Ps = 6 - Normal Cursor Mode (DECOM), VT100.', async () => { + // TODO: Implement + }); + test.skip('Ps = 7 - No Auto-Wrap Mode (DECAWM), VT100.', async () => { + // TODO: Implement + }); + test.skip('Ps = 8 - No Auto-Repeat Keys (DECARM), VT100.', async () => { + // TODO: Implement + }); + test.skip('Ps = 9 - Don\'t send Mouse X & Y on button press, xterm.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 - Hide toolbar (rxvt).', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 2 - Stop blinking cursor (AT&T 610).', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 3 - Disable blinking cursor (reset only via resource or menu).', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 4 - Disable XOR of blinking cursor control sequence and menu.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 8 - Don\'t Print Form Feed (DECPFF), VT220.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 9 - Limit print to scrolling region (DECPEX), VT220.', async () => { + // TODO: Implement + }); + test.skip('Ps = 2 5 - Hide cursor (DECTCEM), VT220.', async () => { + // TODO: Implement + }); + test.skip('Ps = 3 0 - Don\'t show scrollbar (rxvt).', async () => { + // TODO: Implement + }); + test.skip('Ps = 3 5 - Disable font-shifting functions (rxvt).', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 0 - Disallow 80 ⇒ 132 mode, xterm.', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 1 - No more(1) fix (see curses resource).', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 2 - Disable National Replacement Character sets (DECNRCM), VT220.', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 3 - Disable Graphic Expanded Print Mode (DECGEPM), VT340.', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 4 - Turn off margin bell, xterm.', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 4 - Disable Graphic Print Color Mode (DECGPCM), VT340.', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 5 - No Reverse-wraparound mode (XTREVWRAP), xterm.', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 5 - Disable Graphic Print Color Syntax (DECGPCS), VT340.', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 6 - Stop logging (XTLOGGING), xterm. This is normally disabled by a compile-time option.', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 7 - Use Normal Screen Buffer, xterm.', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 7 - Disable Graphic Rotated Print Mode (DECGRPM), VT340.', async () => { + // TODO: Implement + }); + test.skip('Ps = 6 6 - Numeric keypad mode (DECNKM), VT320.', async () => { + // TODO: Implement + }); + test.skip('Ps = 6 7 - Backarrow key sends delete (DECBKM), VT340, VT420. This sets the backarrowKey resource to "false".', async () => { + // TODO: Implement + }); + test.skip('Ps = 6 9 - Disable left and right margin mode (DECLRMM), VT420 and up.', async () => { + // TODO: Implement + }); + test.skip('Ps = 8 0 - Disable Sixel Display Mode (DECSDM), VT330, VT340, VT382. Turns on "Sixel Scrolling". See the section Sixel Graphics and mode 8 4 5 2 .', async () => { + // TODO: Implement + }); + test.skip('Ps = 9 5 - Clear screen when DECCOLM is set/reset (DECNCSM), VT510 and up.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 0 0 - Don\'t send Mouse X & Y on button press and release. See the section Mouse Tracking.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 0 1 - Don\'t use Hilite Mouse Tracking, xterm.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 0 2 - Don\'t use Cell Motion Mouse Tracking, xterm. See the section Button-event tracking.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 0 3 - Don\'t use All Motion Mouse Tracking, xterm. See the section Any-event tracking.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 0 4 - Don\'t send FocusIn/FocusOut events, xterm.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 0 5 - Disable UTF-8 Mouse Mode, xterm.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 0 6 - Disable SGR Mouse Mode, xterm.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 0 7 - Disable Alternate Scroll Mode, xterm. This corresponds to the alternateScroll resource.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 1 0 - Don\'t scroll to bottom on tty output (rxvt). This sets the scrollTtyOutput resource to "false".', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 1 1 - Don\'t scroll to bottom on key press (rxvt). This sets the scrollKey resource to "false".', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 1 5 - Disable urxvt Mouse Mode.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 1 6 - Disable SGR Mouse Pixel-Mode, xterm.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 3 4 - Don\'t interpret "meta" key, xterm. This disables the eightBitInput resource.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 3 5 - Disable special modifiers for Alt and NumLock keys, xterm. This disables the numLock resource.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 3 6 - Don\'t send ESC when Meta modifies a key, xterm. This disables the metaSendsEscape resource.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 3 7 - Send VT220 Remove from the editing-keypad Delete key, xterm.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 3 9 - Don\'t send ESC when Alt modifies a key, xterm. This disables the altSendsEscape resource.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 4 0 - Do not keep selection when not highlighted, xterm. This disables the keepSelection resource.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 4 1 - Use the PRIMARY selection, xterm. This disables the selectToClipboard resource.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 4 2 - Disable Urgency window manager hint when Control-G is received, xterm. This disables the bellIsUrgent resource.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 4 3 - Disable raising of the window when Control- G is received, xterm. This disables the popOnBell resource.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 4 5 - No Extended Reverse-wraparound mode (XTREVWRAP2), xterm.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 4 6 - Disable switching to/from Alternate Screen Buffer, xterm. This works for terminfo-based systems, updating the titeInhibit resource. If currently using the Alternate Screen Buffer, xterm switches to the Normal Screen Buffer.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 4 7 - Use Normal Screen Buffer, xterm. Clear the screen first if in the Alternate Screen Buffer. This may be disabled by the titeInhibit resource.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 4 8 - Restore cursor as in DECRC, xterm. This may be disabled by the titeInhibit resource.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 4 9 - Use Normal Screen Buffer and restore cursor as in DECRC, xterm. This may be disabled by the titeInhibit resource. This combines the effects of the 1 0 4 7 and 1 0 4 8 modes. Use this with terminfo-based applications rather than the 4 7 mode.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 5 0 - Reset terminfo/termcap function-key mode, xterm.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 5 1 - Reset Sun function-key mode, xterm.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 5 2 - Reset HP function-key mode, xterm.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 5 3 - Reset SCO function-key mode, xterm.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 6 0 - Reset legacy keyboard emulation, i.e, X11R6, xterm.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 6 1 - Reset keyboard emulation to Sun/PC style, xterm.', async () => { + // TODO: Implement + }); + test.skip('Ps = 2 0 0 1 - Disable readline mouse button-1, xterm.', async () => { + // TODO: Implement + }); + test.skip('Ps = 2 0 0 2 - Disable readline mouse button-2, xterm.', async () => { + // TODO: Implement + }); + test.skip('Ps = 2 0 0 3 - Disable readline mouse button-3, xterm.', async () => { + // TODO: Implement + }); + test.skip('Ps = 2 0 0 4 - Reset bracketed paste mode, xterm.', async () => { + // TODO: Implement + }); + test.skip('Ps = 2 0 0 5 - Disable readline character-quoting, xterm.', async () => { + // TODO: Implement + }); + test.skip('Ps = 2 0 0 6 - Disable readline newline pasting, xterm.', async () => { + // TODO: Implement + }); + }); + test.describe('CSI Pm m - SGR: Character Attributes', () => { + test.skip('Ps = 0 - Normal (default), VT100.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 - Bold, VT100.', async () => { + // TODO: Implement + }); + test.skip('Ps = 2 - Faint, decreased intensity, ECMA-48 2nd.', async () => { + // TODO: Implement + }); + test.skip('Ps = 3 - Italicized, ECMA-48 2nd.', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 - Underlined, VT100.', async () => { + // TODO: Implement + }); + test.skip('Ps = 5 - Blink, VT100. This appears as Bold in X11R6 xterm.', async () => { + // TODO: Implement + }); + test.skip('Ps = 7 - Inverse, VT100.', async () => { + // TODO: Implement + }); + test.skip('Ps = 8 - Invisible, i.e., hidden, ECMA-48 2nd, VT300.', async () => { + // TODO: Implement + }); + test.skip('Ps = 9 - Crossed-out characters, ECMA-48 3rd.', async () => { + // TODO: Implement + }); + test.skip('Ps = 2 1 - Doubly-underlined, ECMA-48 3rd.', async () => { + // TODO: Implement + }); + test.skip('Ps = 2 2 - Normal (neither bold nor faint), ECMA-48 3rd.', async () => { + // TODO: Implement + }); + test.skip('Ps = 2 3 - Not italicized, ECMA-48 3rd.', async () => { + // TODO: Implement + }); + test.skip('Ps = 2 4 - Not underlined, ECMA-48 3rd.', async () => { + // TODO: Implement + }); + test.skip('Ps = 2 5 - Steady (not blinking), ECMA-48 3rd.', async () => { + // TODO: Implement + }); + test.skip('Ps = 2 7 - Positive (not inverse), ECMA-48 3rd.', async () => { + // TODO: Implement + }); + test.skip('Ps = 2 8 - Visible, i.e., not hidden, ECMA-48 3rd, VT300.', async () => { + // TODO: Implement + }); + test.skip('Ps = 2 9 - Not crossed-out, ECMA-48 3rd.', async () => { + // TODO: Implement + }); + test.skip('Ps = 3 0 - Set foreground color to Black.', async () => { + // TODO: Implement + }); + test.skip('Ps = 3 1 - Set foreground color to Red.', async () => { + // TODO: Implement + }); + test.skip('Ps = 3 2 - Set foreground color to Green.', async () => { + // TODO: Implement + }); + test.skip('Ps = 3 3 - Set foreground color to Yellow.', async () => { + // TODO: Implement + }); + test.skip('Ps = 3 4 - Set foreground color to Blue.', async () => { + // TODO: Implement + }); + test.skip('Ps = 3 5 - Set foreground color to Magenta.', async () => { + // TODO: Implement + }); + test.skip('Ps = 3 6 - Set foreground color to Cyan.', async () => { + // TODO: Implement + }); + test.skip('Ps = 3 7 - Set foreground color to White.', async () => { + // TODO: Implement + }); + test.skip('Ps = 3 9 - Set foreground color to default, ECMA-48 3rd.', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 0 - Set background color to Black.', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 1 - Set background color to Red.', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 2 - Set background color to Green.', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 3 - Set background color to Yellow.', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 4 - Set background color to Blue.', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 5 - Set background color to Magenta.', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 6 - Set background color to Cyan.', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 7 - Set background color to White.', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 9 - Set background color to default, ECMA-48 3rd.', async () => { + // TODO: Implement + }); + test.skip('Ps = 9 0 - Set foreground color to Black.', async () => { + // TODO: Implement + }); + test.skip('Ps = 9 1 - Set foreground color to Red.', async () => { + // TODO: Implement + }); + test.skip('Ps = 9 2 - Set foreground color to Green.', async () => { + // TODO: Implement + }); + test.skip('Ps = 9 3 - Set foreground color to Yellow.', async () => { + // TODO: Implement + }); + test.skip('Ps = 9 4 - Set foreground color to Blue.', async () => { + // TODO: Implement + }); + test.skip('Ps = 9 5 - Set foreground color to Magenta.', async () => { + // TODO: Implement + }); + test.skip('Ps = 9 6 - Set foreground color to Cyan.', async () => { + // TODO: Implement + }); + test.skip('Ps = 9 7 - Set foreground color to White.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 0 - Set background color to Black.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 1 - Set background color to Red.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 2 - Set background color to Green.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 3 - Set background color to Yellow.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 4 - Set background color to Blue.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 5 - Set background color to Magenta.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 6 - Set background color to Cyan.', async () => { + // TODO: Implement + }); + test.skip('Ps = 1 0 7 - Set background color to White.', async () => { + // TODO: Implement + }); + test.skip('Ps = 3 8 : 2 : Pi : Pr : Pg : Pb- Set foreground color using RGB values', async () => { + // TODO: Implement + }); + test.skip('Ps = 3 8 : 5 : Ps- Set foreground color to Ps, using indexed color', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 8 : 2 : Pi : Pr : Pg : Pb- Set background color using RGB values', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 8 : 5 : Ps- Set background color to Ps, using indexed color', async () => { + // TODO: Implement + }); + test.skip('Ps = 3 8 ; 2 ; Pr ; Pg ; Pb- Set foreground color using RGB values', async () => { + // TODO: Implement + }); + test.skip('Ps = 4 8 ; 2 ; Pr ; Pg ; Pb- Set background color using RGB values', async () => { + // TODO: Implement + }); + }); + test.skip('CSI > Pp [; Pv] m - XTMODKEYS: Set/reset key modifier options, xterm', () => { + // TODO: Implement + }); + test.skip('CSI ? Pp m - XTQMODKEYS: Query key modifier options, xterm', () => { + // TODO: Implement + }); + test.describe('CSI Ps n - DSR: Device Status Report', () => { + test('Status Report - CSI 5 n', async () => { + await ctx.page.evaluate(` + window.term.onData(e => window.result = e); + window.term.write('\\x1b[5n'); + `); + await pollFor(ctx.page, () => ctx.page.evaluate(`window.result`), '\x1b[0n'); + }); + + test('Report Cursor Position (CPR) - CSI 6 n', async () => { + await ctx.page.evaluate(`window.term.write('\\n\\nfoo')`); + await pollFor(ctx.page, () => ctx.page.evaluate(` + [window.term.buffer.active.cursorY, window.term.buffer.active.cursorX] + `), [2, 3]); + await ctx.page.evaluate(` + window.term.onData(e => window.result = e); + window.term.write('\\x1b[6n'); + `); + await pollFor(ctx.page, () => ctx.page.evaluate(`window.result`), '\x1b[3;4R'); + }); + + test('Report Cursor Position (DECXCPR) - CSI ? 6 n', async () => { + await ctx.page.evaluate(`window.term.write('\\n\\nfoo')`); + await pollFor(ctx.page, () => ctx.page.evaluate(` + [window.term.buffer.active.cursorY, window.term.buffer.active.cursorX] + `), [2, 3]); + await ctx.page.evaluate(` + window.term.onData(e => window.result = e); + window.term.write('\\x1b[?6n'); + `); + await pollFor(ctx.page, () => ctx.page.evaluate(`window.result`), '\x1b[?3;4R'); + }); + }); + test.skip('CSI > Ps n - Disable key modifier options, xterm', () => { + // TODO: Implement + }); + test.describe.skip('CSI ? Ps n - DSR: Device Status Report (DEC-specific).', () => { + // TODO: Implement + }); + test.skip('CSI > Ps p - XTSMPOINTER: Set resource value pointerMode, xterm', () => { + // TODO: Implement + }); + test.skip('CSI ! p - DECSTR: Soft terminal reset, VT220 and up.', () => { + // TODO: Implement + }); + test.skip('CSI Pl ; Pc " p - DECSCL: Set conformance level, VT220 and up.', () => { + // TODO: Implement + }); + test.skip('CSI Ps $ p - DECRQM: Request ANSI mode', () => { + // TODO: Implement + }); + test.skip('CSI ? Ps $ p - Request DEC private mode (DECRQM).', () => { + // TODO: Implement + }); + test.skip('CSI [Pm] # p - Push video attributes onto stack (XTPUSHSGR), xterm. This is an alias for CSI # { , used to work around language limitations of C#.', async () => { + // TODO: Implement + }); + test.skip('CSI > Ps q - Report xterm name and version (XTVERSION).', async () => { + // TODO: Implement + }); + test.skip('CSI Ps q - Load LEDs (DECLL), VT100.', async () => { + // TODO: Implement + }); + test.skip('CSI Ps SP q - Set cursor style (DECSCUSR), VT520.', async () => { + // TODO: Implement + }); + test.skip('CSI Ps " q - Select character protection attribute (DECSCA), VT220.', async () => { + // TODO: Implement + }); + test.skip('CSI # q - Pop video attributes from stack (XTPOPSGR), xterm.', async () => { + // TODO: Implement + }); + test.skip('CSI Ps ; Ps r - Set Scrolling Region [top;bottom] (default = full size of window) (DECSTBM), VT100.', async () => { + // TODO: Implement + }); + test.skip('CSI ? Pm r - Restore DEC Private Mode Values (XTRESTORE), xterm.', async () => { + // TODO: Implement + }); + test.skip('CSI Pt ; Pl ; Pb ; Pr ; Pm $ r - Change Attributes in Rectangular Area (DECCARA), VT400 and up.', async () => { + // TODO: Implement + }); + test.skip('CSI s - Save cursor, available only when DECLRMM is disabled (SCOSC, also ANSI.SYS).', async () => { + // TODO: Implement + }); + test.skip('CSI Pl ; Pr s - Set left and right margins (DECSLRM), VT420 and up.', async () => { + // TODO: Implement + }); + test.skip('CSI > Ps s - Set/reset shift-escape options (XTSHIFTESCAPE), xterm.', async () => { + // TODO: Implement + }); + test.skip('CSI ? Pm s - Save DEC Private Mode Values (XTSAVE), xterm. Ps values are the same as for DECSET.', async () => { + // TODO: Implement + }); + test.skip('CSI > Pm t - This xterm control sets one or more features of the title modes (XTSMTITLE), xterm.', async () => { + // TODO: Implement + }); + test.skip('CSI Ps SP t - Set warning-bell volume (DECSWBV), VT520.', async () => { + // TODO: Implement + }); + test.skip('CSI Pt ; Pl ; Pb ; Pr ; Pm $ t - Reverse Attributes in Rectangular Area (DECRARA), VT400 and up.', async () => { + // TODO: Implement + }); + test.skip('CSI u - Restore cursor (SCORC, also ANSI.SYS).', async () => { + // TODO: Implement + }); + test.skip('CSI Ps SP u - Set margin-bell volume (DECSMBV), VT520.', async () => { + // TODO: Implement + }); + test.skip('CSI Pt ; Pl ; Pb ; Pr ; Pp ; Pt ; Pl ; Pp $ v - Copy Rectangular Area (DECCRA), VT400 and up.', async () => { + // TODO: Implement + }); + test.skip('CSI Ps $ w - Request presentation state report (DECRQPSR), VT320 and up.', async () => { + // TODO: Implement + }); + test.skip('CSI Pt ; Pl ; Pb ; Pr \' w - Enable Filter Rectangle (DECEFR), VT420 and up.', async () => { + // TODO: Implement + }); + test.skip('CSI Ps x - Request Terminal Parameters (DECREQTPARM).', async () => { + // TODO: Implement + }); + test.skip('CSI Ps * x - Select Attribute Change Extent (DECSACE), VT420 and up.', async () => { + // TODO: Implement + }); + test.skip('CSI Pc ; Pt ; Pl ; Pb ; Pr $ x - Fill Rectangular Area (DECFRA), VT420 and up.', async () => { + // TODO: Implement + }); + test.skip('CSI Ps # y - Select checksum extension (XTCHECKSUM), xterm.', async () => { + // TODO: Implement + }); + test.skip('CSI Pi ; Pg ; Pt ; Pl ; Pb ; Pr * y - Request Checksum of Rectangular Area (DECRQCRA), VT420 and up.', async () => { + // TODO: Implement + }); + test.skip('CSI Ps ; Pu \' z - Enable Locator Reporting (DECELR).', async () => { + // TODO: Implement + }); + test.skip('CSI Pt ; Pl ; Pb ; Pr $ z - Erase Rectangular Area (DECERA), VT400 and up.', async () => { + // TODO: Implement + }); + test.skip('CSI Pm \' { - Select Locator Events (DECSLE).', async () => { + // TODO: Implement + }); + test.skip('CSI [Pm] # { Push video attributes onto stack (XTPUSHSGR), xterm.', async () => { + // TODO: Implement + }); + test.skip('CSI Pt ; Pl ; Pb ; Pr $ { - Selective Erase Rectangular Area (DECSERA), VT400 and up.', async () => { + // TODO: Implement + }); + test.skip('CSI Pt ; Pl ; Pb ; Pr # | - Report selected graphic rendition (XTREPORTSGR), xterm.', async () => { + // TODO: Implement + }); + test.skip('CSI Ps $ | - Select columns per page (DECSCPP), VT340.', async () => { + // TODO: Implement + }); + test.skip('CSI Ps \' | - Request Locator Position (DECRQLP).', async () => { + // TODO: Implement + }); + test.skip('CSI Ps * | - Select number of lines per screen (DECSNLS), VT420 and up.', async () => { + // TODO: Implement + }); + test.skip('CSI # } - Pop video attributes from stack (XTPOPSGR), xterm.', async () => { + // TODO: Implement + }); + test.skip('CSI Ps \' } - Insert Ps Column(s) (default = 1) (DECIC), VT420 and up.', async () => { + // TODO: Implement + }); + test.skip('CSI Ps $ } - Select active status display (DECSASD), VT320 and up.', async () => { + // TODO: Implement + }); + test.skip('CSI Ps \' ~ - Delete Ps Column(s) (default = 1) (DECDC), VT420 and up.', async () => { + // TODO: Implement + }); + test.skip('CSI Ps $ ~ - Select status line type (DECSSDT), VT320 and up.', async () => { + // TODO: Implement + }); + test.describe('CSI Ps ; Ps ; Ps t - Window Options', () => { + test('should be disabled by default', async () => { + await ctx.page.evaluate(`(() => { + window._stack = []; + const _h = window.term.onData(data => window._stack.push(data)); + window.term.write('\x1b[14t'); + window.term.write('\x1b[16t'); + window.term.write('\x1b[18t'); + window.term.write('\x1b[20t'); + window.term.write('\x1b[21t'); + return new Promise((r) => window.term.write('', () => { _h.dispose(); r(); })); + })()`); + await pollFor(ctx.page, async () => await ctx.page.evaluate(`(() => _stack)()`), []); + }); + test('14 - GetWinSizePixels', async () => { + await ctx.page.evaluate(`window.term.options.windowOptions = { getWinSizePixels: true }; `); + await ctx.page.evaluate(`(() => { + window._stack = []; + const _h = window.term.onData(data => window._stack.push(data)); + window.term.write('\x1b[14t'); + return new Promise((r) => window.term.write('', () => { _h.dispose(); r(); })); + })()`); + const d = await getDimensions(); + await pollFor(ctx.page, async () => await ctx.page.evaluate(`(() => _stack)()`), [`\x1b[4;${d.height};${d.width}t`]); + }); + test('16 - GetCellSizePixels', async () => { + await ctx.page.evaluate(`window.term.options.windowOptions = { getCellSizePixels: true }; `); + await ctx.page.evaluate(`(() => { + window._stack = []; + const _h = window.term.onData(data => window._stack.push(data)); + window.term.write('\x1b[16t'); + return new Promise((r) => window.term.write('', () => { _h.dispose(); r(); })); + })()`); + const d = await getDimensions(); + await pollFor(ctx.page, async () => await ctx.page.evaluate(`(() => _stack)()`), [`\x1b[6;${d.cellHeight};${d.cellWidth}t`]); + }); + }); + }); + + test.describe('OSC', () => { + test.describe('OSC 4', () => { + test.beforeAll(async () => { + await ctx.page.evaluate('(() => {window._recordedData = []; window._h = term.onData(d => window._recordedData.push(d));})()'); + }); + test.afterAll(async () => { + await ctx.page.evaluate('window._h.dispose()'); + }); + test.beforeEach(async () => { + await ctx.page.evaluate('window._recordedData.length = 0;'); + }); + test('query single color', async () => { + await ctx.proxy.write('\x1b]4;0;?\x07'); + deepStrictEqual(await ctx.page.evaluate('window._recordedData'), ['\x1b]4;0;rgb:2e2e/3434/3636\x1b\\']); + await ctx.proxy.write('\x1b]4;77;?\x07'); + deepStrictEqual(await ctx.page.evaluate('window._recordedData'), ['\x1b]4;0;rgb:2e2e/3434/3636\x1b\\', '\x1b]4;77;rgb:5f5f/d7d7/5f5f\x1b\\']); + }); + test('query multiple colors', async () => { + await ctx.proxy.write('\x1b]4;0;?;77;?\x07'); + deepStrictEqual(await ctx.page.evaluate('window._recordedData'), ['\x1b]4;0;rgb:2e2e/3434/3636\x1b\\', '\x1b]4;77;rgb:5f5f/d7d7/5f5f\x1b\\']); + }); + test('set & query single color', async () => { + await ctx.proxy.write('\x1b]4;0;?\x07'); + const restore: string[] = await ctx.page.evaluate('window._recordedData'); + deepStrictEqual(await ctx.page.evaluate('window._recordedData'), restore); + // set new color & query + await ctx.proxy.write('\x1b]4;0;rgb:01/02/03\x07\x1b]4;0;?\x07'); + deepStrictEqual(await ctx.page.evaluate('window._recordedData'), [restore[0], '\x1b]4;0;rgb:0101/0202/0303\x1b\\']); + // restore should set old color + await ctx.proxy.write(restore[0] + '\x1b]4;0;?\x07'); + deepStrictEqual(await ctx.page.evaluate('window._recordedData'), [restore[0], '\x1b]4;0;rgb:0101/0202/0303\x1b\\', restore[0]]); + }); + test('query & set colors mixed', async () => { + await ctx.proxy.write('\x1b]4;0;?;77;?\x07'); + const restore: string[] = await ctx.page.evaluate('window._recordedData'); + await ctx.page.evaluate('window._recordedData.length = 0;'); + // mixed call - change 0, query 43, change 77 + await ctx.proxy.write('\x1b]4;0;rgb:01/02/03;43;?;77;#aabbcc\x07'); + deepStrictEqual(await ctx.page.evaluate('window._recordedData'), ['\x1b]4;43;rgb:0000/d7d7/afaf\x1b\\']); + await ctx.page.evaluate('window._recordedData.length = 0;'); + // query new values for 0 + 77 + await ctx.proxy.write('\x1b]4;0;?;77;?\x07'); + deepStrictEqual(await ctx.page.evaluate('window._recordedData'), ['\x1b]4;0;rgb:0101/0202/0303\x1b\\', '\x1b]4;77;rgb:aaaa/bbbb/cccc\x1b\\']); + await ctx.page.evaluate('window._recordedData.length = 0;'); + // restore old values for 0 + 77 + await ctx.proxy.write(restore[0] + restore[1] + '\x1b]4;0;?;77;?\x07'); + deepStrictEqual(await ctx.page.evaluate('window._recordedData'), restore); + }); + }); + test.describe('OSC 4 & 104', () => { + test.beforeAll(async () => { + await ctx.page.evaluate('(() => {window._recordedData = []; window._h = term.onData(d => window._recordedData.push(d));})()'); + }); + test.afterAll(async () => { + await ctx.page.evaluate('window._h.dispose()'); + }); + test.beforeEach(async () => { + await ctx.page.evaluate('window._recordedData.length = 0;'); + }); + test('change & restore single color', async () => { + // test for some random color slots + for (const i of [0, 43, 77, 255]) { + await ctx.proxy.write(`\x1b]4;${i};?\x07`); + const restore: string[] = await ctx.page.evaluate('window._recordedData'); + await ctx.proxy.write(`\x1b]4;${i};rgb:01/02/03\x07\x1b]4;${i};?\x07`); + deepStrictEqual(await ctx.page.evaluate('window._recordedData'), [restore[0], `\x1b]4;${i};rgb:0101/0202/0303\x1b\\`]); + // restore slot color + await ctx.proxy.write(`\x1b]104;${i}\x07\x1b]4;${i};?\x07`); + deepStrictEqual(await ctx.page.evaluate('window._recordedData'), [restore[0], `\x1b]4;${i};rgb:0101/0202/0303\x1b\\`, restore[0]]); + await ctx.page.evaluate('window._recordedData.length = 0;'); + } + }); + test('restore multiple at once', async () => { + // change 3 random slots + await ctx.proxy.write(`\x1b]4;0;?;43;?;77;?\x07`); + const restore: string[] = await ctx.page.evaluate('window._recordedData'); + await ctx.page.evaluate('window._recordedData.length = 0;'); + await ctx.proxy.write(`\x1b]4;0;rgb:01/02/03;43;#aabbcc;77;#123456\x07`); + // restore specific slots + await ctx.proxy.write(`\x1b]104;0;43;77\x07` + `\x1b]4;0;?;43;?;77;?\x07`); + deepStrictEqual(await ctx.page.evaluate('window._recordedData'), restore); + }); + test('restore full table', async () => { + // change 3 random slots + await ctx.proxy.write(`\x1b]4;0;?;43;?;77;?\x07`); + const restore: string[] = await ctx.page.evaluate('window._recordedData'); + await ctx.page.evaluate('window._recordedData.length = 0;'); + await ctx.proxy.write(`\x1b]4;0;rgb:01/02/03;43;#aabbcc;77;#123456\x07`); + // restore all + await ctx.proxy.write(`\x1b]104\x07` + `\x1b]4;0;?;43;?;77;?\x07`); + deepStrictEqual(await ctx.page.evaluate('window._recordedData'), restore); + }); + }); + test.describe('OSC 10 & 11 + 110 | 111 | 112', () => { + test.beforeAll(async () => { + await ctx.page.evaluate('(() => {window._recordedData = []; window._h = term.onData(d => window._recordedData.push(d));})()'); + }); + test.afterAll(async () => { + await ctx.page.evaluate('window._h.dispose()'); + }); + test.beforeEach(async () => { + await ctx.page.evaluate('window._recordedData.length = 0;'); + }); + test('query FG color', async () => { + await ctx.proxy.write('\x1b]10;?\x07'); + deepStrictEqual(await ctx.page.evaluate('window._recordedData'), ['\x1b]10;rgb:ffff/ffff/ffff\x1b\\']); + }); + test('query BG color', async () => { + await ctx.proxy.write('\x1b]11;?\x07'); + deepStrictEqual(await ctx.page.evaluate('window._recordedData'), ['\x1b]11;rgb:0000/0000/0000\x1b\\']); + }); + test('query FG & BG color in one call', async () => { + await ctx.proxy.write('\x1b]10;?;?\x07'); + deepStrictEqual(await ctx.page.evaluate('window._recordedData'), ['\x1b]10;rgb:ffff/ffff/ffff\x1b\\', '\x1b]11;rgb:0000/0000/0000\x1b\\']); + }); + test('set & query FG', async () => { + await ctx.proxy.write('\x1b]10;rgb:1/2/3\x07\x1b]10;?\x07'); + deepStrictEqual(await ctx.page.evaluate('window._recordedData'), ['\x1b]10;rgb:1111/2222/3333\x1b\\']); + await ctx.proxy.write('\x1b]10;#ffffff\x07\x1b]10;?\x07'); + deepStrictEqual(await ctx.page.evaluate('window._recordedData'), ['\x1b]10;rgb:1111/2222/3333\x1b\\', '\x1b]10;rgb:ffff/ffff/ffff\x1b\\']); + }); + test('set & query BG', async () => { + await ctx.proxy.write('\x1b]11;rgb:1/2/3\x07\x1b]11;?\x07'); + deepStrictEqual(await ctx.page.evaluate('window._recordedData'), ['\x1b]11;rgb:1111/2222/3333\x1b\\']); + await ctx.proxy.write('\x1b]11;#000000\x07\x1b]11;?\x07'); + deepStrictEqual(await ctx.page.evaluate('window._recordedData'), ['\x1b]11;rgb:1111/2222/3333\x1b\\', '\x1b]11;rgb:0000/0000/0000\x1b\\']); + }); + test('set & query cursor color', async () => { + await ctx.proxy.write('\x1b]12;rgb:1/2/3\x07\x1b]12;?\x07'); + deepStrictEqual(await ctx.page.evaluate('window._recordedData'), ['\x1b]12;rgb:1111/2222/3333\x1b\\']); + await ctx.proxy.write('\x1b]12;#ffffff\x07\x1b]12;?\x07'); + deepStrictEqual(await ctx.page.evaluate('window._recordedData'), ['\x1b]12;rgb:1111/2222/3333\x1b\\', '\x1b]12;rgb:ffff/ffff/ffff\x1b\\']); + }); + test('set & query FG & BG color in one call', async () => { + await ctx.proxy.write('\x1b]10;#123456;rgb:aa/bb/cc\x07\x1b]10;?;?\x07'); + deepStrictEqual(await ctx.page.evaluate('window._recordedData'), ['\x1b]10;rgb:1212/3434/5656\x1b\\', '\x1b]11;rgb:aaaa/bbbb/cccc\x1b\\']); + await ctx.proxy.write('\x1b]10;#ffffff;#000000\x07'); + }); + test('OSC 110: restore FG color', async () => { + await ctx.proxy.write('\x1b]10;rgb:1/2/3\x07\x1b]10;?\x07'); + deepStrictEqual(await ctx.page.evaluate('window._recordedData'), ['\x1b]10;rgb:1111/2222/3333\x1b\\']); + await ctx.page.evaluate('window._recordedData.length = 0;'); + // restore + await ctx.proxy.write('\x1b]110\x07\x1b]10;?\x07'); + deepStrictEqual(await ctx.page.evaluate('window._recordedData'), ['\x1b]10;rgb:ffff/ffff/ffff\x1b\\']); + }); + test('OSC 111: restore BG color', async () => { + await ctx.proxy.write('\x1b]11;rgb:1/2/3\x07\x1b]11;?\x07'); + deepStrictEqual(await ctx.page.evaluate('window._recordedData'), ['\x1b]11;rgb:1111/2222/3333\x1b\\']); + await ctx.page.evaluate('window._recordedData.length = 0;'); + // restore + await ctx.proxy.write('\x1b]111\x07\x1b]11;?\x07'); + deepStrictEqual(await ctx.page.evaluate('window._recordedData'), ['\x1b]11;rgb:0000/0000/0000\x1b\\']); + }); + test('OSC 112: restore cursor color', async () => { + await ctx.proxy.write('\x1b]12;rgb:1/2/3\x07\x1b]12;?\x07'); + deepStrictEqual(await ctx.page.evaluate('window._recordedData'), ['\x1b]12;rgb:1111/2222/3333\x1b\\']); + await ctx.page.evaluate('window._recordedData.length = 0;'); + // restore + await ctx.proxy.write('\x1b]112\x07\x1b]12;?\x07'); + deepStrictEqual(await ctx.page.evaluate('window._recordedData'), ['\x1b]12;rgb:ffff/ffff/ffff\x1b\\']); + }); + }); + }); + + test.describe('ESC', () => { + test.describe('DECRC: Save cursor, ESC 7', () => { + test('should save the absolute cursor position so resizing restores to the correct position', async () => { + await ctx.page.evaluate(` + window.term.resize(10, 2); + window.term.write('1\\n\\r2\\n\\r3\\n\\r4\\n\\r5'); + window.term.write('\\x1b7\\x1b[?47h'); + `); + await ctx.page.evaluate(` + window.term.resize(10, 4); + window.term.write('\\x1b[?47l\\x1b8'); + `); + await pollFor(ctx.page, () => getCursor(), { col: 1, row: 3 }); + }); + }); + }); +}); + + +async function getLinesAsArray(count: number, start: number = 0): Promise { + let text = ''; + for (let i = start; i < start + count; i++) { + text += `window.term.buffer.active.getLine(${i}).translateToString(true),`; + } + return await ctx.page.evaluate(`[${text}]`); +} + +async function simulatePaste(text: string): Promise { + const id = Math.floor(Math.random() * 1000000); + await ctx.page.evaluate(` + (function() { + window.term.onData(e => window.result_${id} = e); + const clipboardData = new DataTransfer(); + clipboardData.setData('text/plain', '${text}'); + window.term.textarea.dispatchEvent(new ClipboardEvent('paste', { clipboardData })); + })(); + `); + return await ctx.page.evaluate(`window.result_${id} `); +} + +async function getCursor(): Promise<{ col: number, row: number }> { + return ctx.page.evaluate(` + (function() { + return {col: term.buffer.active.cursorX, row: term.buffer.active.cursorY}; + })(); + `); +} + +async function getDimensions(): Promise { + const dim: IRenderDimensions = await ctx.page.evaluate(`term._core._renderService.dimensions`); + return { + cellWidth: dim.css.cell.width.toFixed(0), + cellHeight: dim.css.cell.height.toFixed(0), + width: dim.css.canvas.width.toFixed(0), + height: dim.css.canvas.height.toFixed(0) + }; +} diff --git a/test/playwright/tsconfig.json b/test/playwright/tsconfig.json index 2cea437a..2b5e55fa 100644 --- a/test/playwright/tsconfig.json +++ b/test/playwright/tsconfig.json @@ -7,7 +7,6 @@ "rootDir": ".", "outDir": "../../out-test/playwright", "types": [ - "../../node_modules/@types/mocha", "../../node_modules/@types/node" ], "sourceMap": true, From c099cb2a3db303a528712dc42535cec2e7f8d7af Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 2 Sep 2023 08:17:15 -0700 Subject: [PATCH 25/40] Add browser-specific npm scripts --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 853ba45a..e297cb28 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,9 @@ "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-chromium": "playwright test -c ./out-test/playwright/playwright.config.js --workers 4 --project='Chrome Stable'", + "test-playwright-firefox": "playwright test -c ./out-test/playwright/playwright.config.js --workers 4 --project='Firefox Stable'", + "test-playwright-webkit": "playwright test -c ./out-test/playwright/playwright.config.js --workers 4 --project='WebKit'", "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", From 1624854bd7ebc572d6c467ec1ca4d8c9f4d01d49 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 2 Sep 2023 08:18:02 -0700 Subject: [PATCH 26/40] Migrate Terminal api tests to playwright --- src/browser/Viewport.ts | 1 + test/api/Terminal.api.ts | 1062 -------------------------- test/playwright/InputHandler.test.ts | 5 +- test/playwright/Terminal.test.ts | 1053 +++++++++++++++++++++++++ test/playwright/TestUtils.ts | 19 +- 5 files changed, 1069 insertions(+), 1071 deletions(-) delete mode 100644 test/api/Terminal.api.ts create mode 100644 test/playwright/Terminal.test.ts diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index c6e8732f..1611f54b 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -118,6 +118,7 @@ export class Viewport extends Disposable implements IViewport { } private _innerRefresh(): void { + debugger; if (this._charSizeService.height > 0) { this._currentRowHeight = this._renderService.dimensions.device.cell.height / this._coreBrowserService.dpr; this._currentDeviceCellHeight = this._renderService.dimensions.device.cell.height; diff --git a/test/api/Terminal.api.ts b/test/api/Terminal.api.ts deleted file mode 100644 index d0ba1dd6..00000000 --- a/test/api/Terminal.api.ts +++ /dev/null @@ -1,1062 +0,0 @@ -/** - * Copyright (c) 2019 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { assert } from 'chai'; -import { pollFor, timeout, writeSync, openTerminal, launchBrowser } from './TestUtils'; -import { Browser, Page } from '@playwright/test'; -import { fail } from 'assert'; -import { IRenderDimensions } from 'browser/renderer/shared/Types'; - -const APP = 'http://127.0.0.1:3001/test'; - -let browser: Browser; -let page: Page; -const width = 800; -const height = 600; - -describe('API Integration Tests', function(): void { - before(async () => { - browser = await launchBrowser(); - page = await (await browser.newContext()).newPage(); - await page.setViewportSize({ width, height }); - }); - - after(async () => browser.close()); - beforeEach(async () => page.goto(APP)); - - it('Default options', async () => { - await openTerminal(page); - assert.equal(await page.evaluate(`window.term.cols`), 80); - assert.equal(await page.evaluate(`window.term.rows`), 24); - }); - - it('Proposed API check', async () => { - await openTerminal(page, { allowProposedApi: false }); - await page.evaluate(` - try { - window.term.markers; - } catch (e) { - window.throwMessage = e.message; - } - `); - await pollFor(page, 'window.throwMessage', 'You must set the allowProposedApi option to true to use proposed API'); - }); - - it('write', async () => { - await openTerminal(page); - await page.evaluate(` - window.term.write('foo'); - window.term.write('bar'); - window.term.write('文'); - `); - await pollFor(page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foobar文'); - }); - - it('write with callback', async () => { - await openTerminal(page); - await page.evaluate(` - window.term.write('foo', () => { window.__x = 'a'; }); - window.term.write('bar', () => { window.__x += 'b'; }); - window.term.write('文', () => { window.__x += 'c'; }); - `); - await pollFor(page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foobar文'); - await pollFor(page, `window.__x`, 'abc'); - }); - - it('write - bytes (UTF8)', async () => { - await openTerminal(page); - await page.evaluate(` - window.term.write(new Uint8Array([102, 111, 111])); // foo - window.term.write(new Uint8Array([98, 97, 114])); // bar - window.term.write(new Uint8Array([230, 150, 135])); // 文 - `); - await pollFor(page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foobar文'); - }); - - it('write - bytes (UTF8) with callback', async () => { - await openTerminal(page); - await page.evaluate(` - window.term.write(new Uint8Array([102, 111, 111]), () => { window.__x = 'A'; }); // foo - window.term.write(new Uint8Array([98, 97, 114]), () => { window.__x += 'B'; }); // bar - window.term.write(new Uint8Array([230, 150, 135]), () => { window.__x += 'C'; }); // 文 - `); - await pollFor(page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foobar文'); - await pollFor(page, `window.__x`, 'ABC'); - }); - - it('writeln', async () => { - await openTerminal(page); - await page.evaluate(` - window.term.writeln('foo'); - window.term.writeln('bar'); - window.term.writeln('文'); - `); - await pollFor(page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foo'); - await pollFor(page, `window.term.buffer.active.getLine(1).translateToString(true)`, 'bar'); - await pollFor(page, `window.term.buffer.active.getLine(2).translateToString(true)`, '文'); - }); - - it('writeln with callback', async () => { - await openTerminal(page); - await page.evaluate(` - window.term.writeln('foo', () => { window.__x = '1'; }); - window.term.writeln('bar', () => { window.__x += '2'; }); - window.term.writeln('文', () => { window.__x += '3'; }); - `); - await pollFor(page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foo'); - await pollFor(page, `window.term.buffer.active.getLine(1).translateToString(true)`, 'bar'); - await pollFor(page, `window.term.buffer.active.getLine(2).translateToString(true)`, '文'); - await pollFor(page, `window.__x`, '123'); - }); - - it('writeln - bytes (UTF8)', async () => { - await openTerminal(page); - await page.evaluate(` - window.term.writeln(new Uint8Array([102, 111, 111])); - window.term.writeln(new Uint8Array([98, 97, 114])); - window.term.writeln(new Uint8Array([230, 150, 135])); - `); - await pollFor(page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foo'); - await pollFor(page, `window.term.buffer.active.getLine(1).translateToString(true)`, 'bar'); - await pollFor(page, `window.term.buffer.active.getLine(2).translateToString(true)`, '文'); - }); - - it('paste', async () => { - await openTerminal(page); - await page.evaluate(` - window.calls = []; - window.term.onData(e => calls.push(e)); - window.term.paste('foo'); - window.term.paste('\\r\\nfoo\\nbar\\r'); - window.term.write('\\x1b[?2004h', () => { - window.term.paste('foo'); - - window.term.options.ignoreBracketedPasteMode = true; - window.term.paste('check_mode'); - }); - `); - await pollFor(page, `window.calls`, ['foo', '\rfoo\rbar\r', '\x1b[200~foo\x1b[201~', 'check_mode']); - }); - - it('clear', async () => { - await openTerminal(page, { rows: 5 }); - await page.evaluate(` - window.term.write('test0'); - window.parsed = 0; - for (let i = 1; i < 10; i++) { - window.term.write('\\n\\rtest' + i, () => window.parsed++); - } - `); - await pollFor(page, `window.parsed`, 9); - await page.evaluate(`window.term.clear()`); - await pollFor(page, `window.term.buffer.active.length`, 5); - await pollFor(page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'test9'); - for (let i = 1; i < 5; i++) { - await pollFor(page, `window.term.buffer.active.getLine(${i}).translateToString(true)`, ''); - } - }); - - describe('options', () => { - it('getter', async () => { - await openTerminal(page); - assert.equal(await page.evaluate(`window.term.options.cols`), 80); - assert.equal(await page.evaluate(`window.term.options.rows`), 24); - }); - it('setter', async () => { - await openTerminal(page); - try { - await page.evaluate('window.term.options.cols = 40'); - fail(); - } catch {} - try { - await page.evaluate('window.term.options.rows = 20'); - fail(); - } catch {} - await page.evaluate('window.term.options.scrollback = 1'); - assert.equal(await page.evaluate(`window.term.options.scrollback`), 1); - await page.evaluate(` - window.term.options = { - fontSize: 30, - fontFamily: 'Arial' - }; - `); - assert.equal(await page.evaluate(`window.term.options.fontSize`), 30); - assert.equal(await page.evaluate(`window.term.options.fontFamily`), 'Arial'); - }); - it('object.keys return the correct number of options', async () => { - await openTerminal(page); - assert.notEqual(await page.evaluate(`Object.keys(window.term.options).length`), 0); - }); - }); - - describe('renderer', () => { - it('foreground', async () => { - await openTerminal(page); - await writeSync(page, '\\x1b[30m0\\x1b[31m1\\x1b[32m2\\x1b[33m3\\x1b[34m4\\x1b[35m5\\x1b[36m6\\x1b[37m7'); - await pollFor(page, `document.querySelectorAll('.xterm-rows > :nth-child(1) > *').length`, 9); - assert.deepEqual(await page.evaluate(` - [ - document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(1)').className, - document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(2)').className, - document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(3)').className, - document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(4)').className, - document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(5)').className, - document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(6)').className, - document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(7)').className - ] - `), [ - 'xterm-fg-0', - 'xterm-fg-1', - 'xterm-fg-2', - 'xterm-fg-3', - 'xterm-fg-4', - 'xterm-fg-5', - 'xterm-fg-6' - ]); - }); - - it('background', async () => { - await openTerminal(page); - await writeSync(page, '\\x1b[40m0\\x1b[41m1\\x1b[42m2\\x1b[43m3\\x1b[44m4\\x1b[45m5\\x1b[46m6\\x1b[47m7'); - await pollFor(page, `document.querySelectorAll('.xterm-rows > :nth-child(1) > *').length`, 9); - assert.deepEqual(await page.evaluate(` - [ - document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(1)').className, - document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(2)').className, - document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(3)').className, - document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(4)').className, - document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(5)').className, - document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(6)').className, - document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(7)').className - ] - `), [ - 'xterm-bg-0', - 'xterm-bg-1', - 'xterm-bg-2', - 'xterm-bg-3', - 'xterm-bg-4', - 'xterm-bg-5', - 'xterm-bg-6' - ]); - }); - }); - - it('selection', async () => { - await openTerminal(page, { rows: 5, cols: 5 }); - await writeSync(page, `\\n\\nfoo\\n\\n\\rbar\\n\\n\\rbaz`); - assert.equal(await page.evaluate(`window.term.hasSelection()`), false); - assert.equal(await page.evaluate(`window.term.getSelection()`), ''); - assert.deepEqual(await page.evaluate(`window.term.getSelectionPosition()`), undefined); - await page.evaluate(`window.term.selectAll()`); - assert.equal(await page.evaluate(`window.term.hasSelection()`), true); - if (process.platform === 'win32') { - assert.equal(await page.evaluate(`window.term.getSelection()`), '\r\n\r\nfoo\r\n\r\nbar\r\n\r\nbaz'); - } else { - assert.equal(await page.evaluate(`window.term.getSelection()`), '\n\nfoo\n\nbar\n\nbaz'); - } - assert.deepEqual(await page.evaluate(`window.term.getSelectionPosition()`), { start: { x: 0, y: 0 }, end: { x: 5, y: 6 } }); - await page.evaluate(`window.term.clearSelection()`); - assert.equal(await page.evaluate(`window.term.hasSelection()`), false); - assert.equal(await page.evaluate(`window.term.getSelection()`), ''); - assert.deepEqual(await page.evaluate(`window.term.getSelectionPosition()`), undefined); - await page.evaluate(`window.term.select(1, 2, 2)`); - assert.equal(await page.evaluate(`window.term.hasSelection()`), true); - assert.equal(await page.evaluate(`window.term.getSelection()`), 'oo'); - assert.deepEqual(await page.evaluate(`window.term.getSelectionPosition()`), { start: { x: 1, y: 2 }, end: { x: 3, y: 2 } }); - }); - - it('focus, blur', async () => { - await openTerminal(page); - assert.equal(await page.evaluate(`document.activeElement.className`), ''); - await page.evaluate(`window.term.focus()`); - assert.equal(await page.evaluate(`document.activeElement.className`), 'xterm-helper-textarea'); - await page.evaluate(`window.term.blur()`); - assert.equal(await page.evaluate(`document.activeElement.className`), ''); - }); - - describe('loadAddon', () => { - it('constructor', async () => { - await openTerminal(page, { cols: 5 }); - await page.evaluate(` - window.cols = 0; - window.term.loadAddon({ - activate: (t) => window.cols = t.cols, - dispose: () => {} - }); - `); - assert.equal(await page.evaluate(`window.cols`), 5); - }); - - it('dispose (addon)', async () => { - await openTerminal(page); - await page.evaluate(` - window.disposeCalled = false - window.addon = { - activate: () => {}, - dispose: () => window.disposeCalled = true - }; - window.term.loadAddon(window.addon); - `); - assert.equal(await page.evaluate(`window.disposeCalled`), false); - await page.evaluate(`window.addon.dispose()`); - assert.equal(await page.evaluate(`window.disposeCalled`), true); - }); - - it('dispose (terminal)', async () => { - await openTerminal(page); - await page.evaluate(` - window.disposeCalled = false - window.term.loadAddon({ - activate: () => {}, - dispose: () => window.disposeCalled = true - }); - `); - assert.equal(await page.evaluate(`window.disposeCalled`), false); - await page.evaluate(`window.term.dispose()`); - assert.equal(await page.evaluate(`window.disposeCalled`), true); - }); - }); - - describe('Events', () => { - it('onCursorMove', async () => { - await openTerminal(page); - await page.evaluate(` - window.callCount = 0; - window.term.onCursorMove(e => window.callCount++); - window.term.write('foo'); - `); - await pollFor(page, `window.callCount`, 1); - await page.evaluate(`window.term.write('bar')`); - await pollFor(page, `window.callCount`, 2); - }); - - it('onData', async () => { - await openTerminal(page); - await page.evaluate(` - window.calls = []; - window.term.onData(e => calls.push(e)); - `); - await page.type('.xterm-helper-textarea', 'foo'); - assert.deepEqual(await page.evaluate(`window.calls`), ['f', 'o', 'o']); - }); - - it('onKey', async () => { - await openTerminal(page); - await page.evaluate(` - window.calls = []; - window.term.onKey(e => calls.push(e.key)); - `); - await page.type('.xterm-helper-textarea', 'foo'); - assert.deepEqual(await page.evaluate(`window.calls`), ['f', 'o', 'o']); - }); - - it('onLineFeed', async () => { - await openTerminal(page); - await page.evaluate(` - window.callCount = 0; - window.term.onLineFeed(() => callCount++); - window.term.writeln('foo'); - `); - await pollFor(page, `window.callCount`, 1); - await page.evaluate(`window.term.writeln('bar')`); - await pollFor(page, `window.callCount`, 2); - }); - - it('onScroll', async () => { - await openTerminal(page, { rows: 5 }); - await page.evaluate(` - window.calls = []; - window.term.onScroll(e => window.calls.push(e)); - for (let i = 0; i < 4; i++) { - window.term.writeln('foo'); - } - `); - await pollFor(page, `window.calls`, []); - await page.evaluate(`window.term.writeln('bar')`); - await pollFor(page, `window.calls`, [1]); - await page.evaluate(`window.term.writeln('baz')`); - await pollFor(page, `window.calls`, [1, 2]); - }); - - it('onSelectionChange', async () => { - await openTerminal(page); - await page.evaluate(` - window.callCount = 0; - window.term.onSelectionChange(() => window.callCount++); - `); - await pollFor(page, `window.callCount`, 0); - await page.evaluate(`window.term.selectAll()`); - await pollFor(page, `window.callCount`, 1); - await page.evaluate(`window.term.clearSelection()`); - await pollFor(page, `window.callCount`, 2); - }); - - it('onRender', async function(): Promise { - this.retries(3); - await openTerminal(page); - await timeout(20); // Ensure all init events are fired - await page.evaluate(` - window.calls = []; - window.term.onRender(e => window.calls.push([e.start, e.end])); - `); - await pollFor(page, `window.calls`, []); - await page.evaluate(`window.term.write('foo')`); - await pollFor(page, `window.calls`, [[0, 0]]); - await page.evaluate(`window.term.write('bar\\n\\nbaz')`); - await pollFor(page, `window.calls`, [[0, 0], [0, 2]]); - }); - - it('onResize', async () => { - await openTerminal(page); - await timeout(20); // Ensure all init events are fired - await page.evaluate(` - window.calls = []; - window.term.onResize(e => window.calls.push([e.cols, e.rows])); - `); - await pollFor(page, `window.calls`, []); - await page.evaluate(`window.term.resize(10, 5)`); - await pollFor(page, `window.calls`, [[10, 5]]); - await page.evaluate(`window.term.resize(20, 15)`); - await pollFor(page, `window.calls`, [[10, 5], [20, 15]]); - }); - - it('onTitleChange', async () => { - await openTerminal(page); - await page.evaluate(` - window.calls = []; - window.term.onTitleChange(e => window.calls.push(e)); - `); - await pollFor(page, `window.calls`, []); - await page.evaluate(`window.term.write('\\x1b]2;foo\\x9c')`); - await pollFor(page, `window.calls`, ['foo']); - }); - it('onBell', async () => { - await openTerminal(page); - await page.evaluate(` - window.calls = []; - window.term.onBell(() => window.calls.push(true)); - `); - await pollFor(page, `window.calls`, []); - await page.evaluate(`window.term.write('\\x07')`); - await pollFor(page, `window.calls`, [true]); - }); - }); - - describe('buffer', () => { - it('cursorX, cursorY', async () => { - await openTerminal(page, { rows: 5, cols: 5 }); - assert.equal(await page.evaluate(`window.term.buffer.active.cursorX`), 0); - assert.equal(await page.evaluate(`window.term.buffer.active.cursorY`), 0); - await writeSync(page, 'foo'); - assert.equal(await page.evaluate(`window.term.buffer.active.cursorX`), 3); - assert.equal(await page.evaluate(`window.term.buffer.active.cursorY`), 0); - await writeSync(page, '\\n'); - assert.equal(await page.evaluate(`window.term.buffer.active.cursorX`), 3); - assert.equal(await page.evaluate(`window.term.buffer.active.cursorY`), 1); - await writeSync(page, '\\r'); - assert.equal(await page.evaluate(`window.term.buffer.active.cursorX`), 0); - assert.equal(await page.evaluate(`window.term.buffer.active.cursorY`), 1); - await writeSync(page, 'abcde'); - assert.equal(await page.evaluate(`window.term.buffer.active.cursorX`), 5); - assert.equal(await page.evaluate(`window.term.buffer.active.cursorY`), 1); - await writeSync(page, '\\n\\r\\n\\n\\n\\n\\n'); - assert.equal(await page.evaluate(`window.term.buffer.active.cursorX`), 0); - assert.equal(await page.evaluate(`window.term.buffer.active.cursorY`), 4); - }); - - it('viewportY', async () => { - await openTerminal(page, { rows: 5 }); - assert.equal(await page.evaluate(`window.term.buffer.active.viewportY`), 0); - await writeSync(page, '\\n\\n\\n\\n'); - assert.equal(await page.evaluate(`window.term.buffer.active.viewportY`), 0); - await writeSync(page, '\\n'); - assert.equal(await page.evaluate(`window.term.buffer.active.viewportY`), 1); - await writeSync(page, '\\n\\n\\n\\n'); - assert.equal(await page.evaluate(`window.term.buffer.active.viewportY`), 5); - await page.evaluate(`window.term.scrollLines(-1)`); - assert.equal(await page.evaluate(`window.term.buffer.active.viewportY`), 4); - await page.evaluate(`window.term.scrollToTop()`); - assert.equal(await page.evaluate(`window.term.buffer.active.viewportY`), 0); - }); - - it('baseY', async () => { - await openTerminal(page, { rows: 5 }); - assert.equal(await page.evaluate(`window.term.buffer.active.baseY`), 0); - await writeSync(page, '\\n\\n\\n\\n'); - assert.equal(await page.evaluate(`window.term.buffer.active.baseY`), 0); - await writeSync(page, '\\n'); - assert.equal(await page.evaluate(`window.term.buffer.active.baseY`), 1); - await writeSync(page, '\\n\\n\\n\\n'); - assert.equal(await page.evaluate(`window.term.buffer.active.baseY`), 5); - await page.evaluate(`window.term.scrollLines(-1)`); - assert.equal(await page.evaluate(`window.term.buffer.active.baseY`), 5); - await page.evaluate(`window.term.scrollToTop()`); - assert.equal(await page.evaluate(`window.term.buffer.active.baseY`), 5); - }); - - it('length', async () => { - await openTerminal(page, { rows: 5 }); - assert.equal(await page.evaluate(`window.term.buffer.active.length`), 5); - await writeSync(page, '\\n\\n\\n\\n'); - assert.equal(await page.evaluate(`window.term.buffer.active.length`), 5); - await writeSync(page, '\\n'); - assert.equal(await page.evaluate(`window.term.buffer.active.length`), 6); - await writeSync(page, '\\n\\n\\n\\n'); - assert.equal(await page.evaluate(`window.term.buffer.active.length`), 10); - }); - - describe('getLine', () => { - it('invalid index', async () => { - await openTerminal(page, { rows: 5 }); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(-1)`), undefined); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(5)`), undefined); - }); - - it('isWrapped', async () => { - await openTerminal(page, { cols: 5 }); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).isWrapped`), false); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(1).isWrapped`), false); - await writeSync(page, 'abcde'); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).isWrapped`), false); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(1).isWrapped`), false); - await writeSync(page, 'f'); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).isWrapped`), false); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(1).isWrapped`), true); - }); - - it('translateToString', async () => { - await openTerminal(page, { cols: 5 }); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).translateToString()`), ' '); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).translateToString(true)`), ''); - await writeSync(page, 'foo'); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).translateToString()`), 'foo '); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).translateToString(true)`), 'foo'); - await writeSync(page, 'bar'); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).translateToString()`), 'fooba'); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).translateToString(true)`), 'fooba'); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(1).translateToString(true)`), 'r'); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).translateToString(false, 1)`), 'ooba'); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).translateToString(false, 1, 3)`), 'oo'); - }); - - it('getCell', async () => { - await openTerminal(page, { cols: 5 }); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).getCell(-1)`), undefined); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).getCell(5)`), undefined); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).getCell(0).getChars()`), ''); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).getCell(0).getWidth()`), 1); - await writeSync(page, 'a文'); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).getCell(0).getChars()`), 'a'); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).getCell(0).getWidth()`), 1); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).getCell(1).getChars()`), '文'); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).getCell(1).getWidth()`), 2); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).getCell(2).getChars()`), ''); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).getCell(2).getWidth()`), 0); - }); - - it('clearMarkers', async () => { - await openTerminal(page, { cols: 5 }); - await page.evaluate(` - window.disposeStack = []; - `); - await writeSync(page, '\\n\\n\\n\\n'); - await writeSync(page, '\\n\\n\\n\\n'); - await writeSync(page, '\\n\\n\\n\\n'); - await writeSync(page, '\\n\\n\\n\\n'); - await page.evaluate(`window.term.registerMarker(1)`); - await page.evaluate(`window.term.registerMarker(2)`); - await page.evaluate(`window.term.scrollLines(10)`); - await page.evaluate(`window.term.registerMarker(3)`); - await page.evaluate(`window.term.registerMarker(4)`); - await page.evaluate(` - for (let i = 0; i < window.term.markers.length; ++i) { - const marker = window.term.markers[i]; - marker.onDispose(() => window.disposeStack.push(marker)); - }`); - await page.evaluate(`window.term.clear()`); - assert.equal(await page.evaluate(`window.disposeStack.length`), 4); - }); - }); - - it('active, normal, alternate', async () => { - await openTerminal(page, { cols: 5 }); - assert.equal(await page.evaluate(`window.term.buffer.active.type`), 'normal'); - assert.equal(await page.evaluate(`window.term.buffer.normal.type`), 'normal'); - assert.equal(await page.evaluate(`window.term.buffer.alternate.type`), 'alternate'); - - await writeSync(page, 'norm '); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).translateToString()`), 'norm '); - assert.equal(await page.evaluate(`window.term.buffer.normal.getLine(0).translateToString()`), 'norm '); - assert.equal(await page.evaluate(`window.term.buffer.alternate.getLine(0)`), undefined); - - await writeSync(page, '\\x1b[?47h\\r'); // use alternate screen buffer - assert.equal(await page.evaluate(`window.term.buffer.active.type`), 'alternate'); - assert.equal(await page.evaluate(`window.term.buffer.normal.type`), 'normal'); - assert.equal(await page.evaluate(`window.term.buffer.alternate.type`), 'alternate'); - - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).translateToString()`), ' '); - await writeSync(page, 'alt '); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).translateToString()`), 'alt '); - assert.equal(await page.evaluate(`window.term.buffer.normal.getLine(0).translateToString()`), 'norm '); - assert.equal(await page.evaluate(`window.term.buffer.alternate.getLine(0).translateToString()`), 'alt '); - - await writeSync(page, '\\x1b[?47l\\r'); // use normal screen buffer - assert.equal(await page.evaluate(`window.term.buffer.active.type`), 'normal'); - assert.equal(await page.evaluate(`window.term.buffer.normal.type`), 'normal'); - assert.equal(await page.evaluate(`window.term.buffer.alternate.type`), 'alternate'); - - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).translateToString()`), 'norm '); - assert.equal(await page.evaluate(`window.term.buffer.normal.getLine(0).translateToString()`), 'norm '); - assert.equal(await page.evaluate(`window.term.buffer.alternate.getLine(0)`), undefined); - }); - }); - - describe('modes', () => { - it('defaults', async () => { - await openTerminal(page); - assert.deepStrictEqual(await page.evaluate(`window.term.modes`), { - applicationCursorKeysMode: false, - applicationKeypadMode: false, - bracketedPasteMode: false, - insertMode: false, - mouseTrackingMode: 'none', - originMode: false, - reverseWraparoundMode: false, - sendFocusMode: false, - wraparoundMode: true - }); - }); - it('applicationCursorKeysMode', async () => { - await openTerminal(page); - await writeSync(page, '\\x1b[?1h'); - assert.strictEqual(await page.evaluate(`window.term.modes.applicationCursorKeysMode`), true); - await writeSync(page, '\\x1b[?1l'); - assert.strictEqual(await page.evaluate(`window.term.modes.applicationCursorKeysMode`), false); - }); - it('applicationKeypadMode', async () => { - await openTerminal(page); - await writeSync(page, '\\x1b[?66h'); - assert.strictEqual(await page.evaluate(`window.term.modes.applicationKeypadMode`), true); - await writeSync(page, '\\x1b[?66l'); - assert.strictEqual(await page.evaluate(`window.term.modes.applicationKeypadMode`), false); - }); - it('bracketedPasteMode', async () => { - await openTerminal(page); - await writeSync(page, '\\x1b[?2004h'); - assert.strictEqual(await page.evaluate(`window.term.modes.bracketedPasteMode`), true); - await writeSync(page, '\\x1b[?2004l'); - assert.strictEqual(await page.evaluate(`window.term.modes.bracketedPasteMode`), false); - }); - it('insertMode', async () => { - await openTerminal(page); - await writeSync(page, '\\x1b[4h'); - assert.strictEqual(await page.evaluate(`window.term.modes.insertMode`), true); - await writeSync(page, '\\x1b[4l'); - assert.strictEqual(await page.evaluate(`window.term.modes.insertMode`), false); - }); - it('mouseTrackingMode', async () => { - await openTerminal(page); - await writeSync(page, '\\x1b[?9h'); - assert.strictEqual(await page.evaluate(`window.term.modes.mouseTrackingMode`), 'x10'); - await writeSync(page, '\\x1b[?9l'); - assert.strictEqual(await page.evaluate(`window.term.modes.mouseTrackingMode`), 'none'); - await writeSync(page, '\\x1b[?1000h'); - assert.strictEqual(await page.evaluate(`window.term.modes.mouseTrackingMode`), 'vt200'); - await writeSync(page, '\\x1b[?1000l'); - assert.strictEqual(await page.evaluate(`window.term.modes.mouseTrackingMode`), 'none'); - await writeSync(page, '\\x1b[?1002h'); - assert.strictEqual(await page.evaluate(`window.term.modes.mouseTrackingMode`), 'drag'); - await writeSync(page, '\\x1b[?1002l'); - assert.strictEqual(await page.evaluate(`window.term.modes.mouseTrackingMode`), 'none'); - await writeSync(page, '\\x1b[?1003h'); - assert.strictEqual(await page.evaluate(`window.term.modes.mouseTrackingMode`), 'any'); - await writeSync(page, '\\x1b[?1003l'); - assert.strictEqual(await page.evaluate(`window.term.modes.mouseTrackingMode`), 'none'); - }); - it('originMode', async () => { - await openTerminal(page); - await writeSync(page, '\\x1b[?6h'); - assert.strictEqual(await page.evaluate(`window.term.modes.originMode`), true); - await writeSync(page, '\\x1b[?6l'); - assert.strictEqual(await page.evaluate(`window.term.modes.originMode`), false); - }); - it('reverseWraparoundMode', async () => { - await openTerminal(page); - await writeSync(page, '\\x1b[?45h'); - assert.strictEqual(await page.evaluate(`window.term.modes.reverseWraparoundMode`), true); - await writeSync(page, '\\x1b[?45l'); - assert.strictEqual(await page.evaluate(`window.term.modes.reverseWraparoundMode`), false); - }); - it('sendFocusMode', async () => { - await openTerminal(page); - await writeSync(page, '\\x1b[?1004h'); - assert.strictEqual(await page.evaluate(`window.term.modes.sendFocusMode`), true); - await writeSync(page, '\\x1b[?1004l'); - assert.strictEqual(await page.evaluate(`window.term.modes.sendFocusMode`), false); - }); - it('wraparoundMode', async () => { - await openTerminal(page); - await writeSync(page, '\\x1b[?7h'); - assert.strictEqual(await page.evaluate(`window.term.modes.wraparoundMode`), true); - await writeSync(page, '\\x1b[?7l'); - assert.strictEqual(await page.evaluate(`window.term.modes.wraparoundMode`), false); - }); - }); - - it('dispose', async () => { - await page.evaluate(` - window.term = new Terminal(); - window.term.dispose(); - `); - assert.equal(await page.evaluate(`window.term._core._isDisposed`), true); - }); - - it('dispose (opened)', async () => { - await openTerminal(page); - await page.evaluate(`window.term.dispose()`); - assert.equal(await page.evaluate(`window.term._core._isDisposed`), true); - }); - - it('render when visible after hidden', async () => { - await page.evaluate(`document.querySelector('#terminal-container').style.display='none'`); - await page.evaluate(`window.term = new Terminal()`); - await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`); - await page.evaluate(`document.querySelector('#terminal-container').style.display=''`); - await pollFor(page, `window.term._core._renderService.dimensions.css.cell.width > 0`, true); - }); - - describe('registerDecoration', () => { - describe('bufferDecorations', () => { - it('should register decorations and render them when terminal open is called', async () => { - await openTerminal(page); - await page.evaluate(`window.marker1 = window.term.registerMarker(1)`); - await page.evaluate(`window.marker2 = window.term.registerMarker(2)`); - await page.evaluate(`window.term.registerDecoration({ marker: window.marker1 })`); - await page.evaluate(`window.term.registerDecoration({ marker: window.marker2 })`); - await openTerminal(page); - await pollFor(page, `document.querySelectorAll('.xterm-screen .xterm-decoration').length`, 2); - }); - it('should return undefined when the marker has already been disposed of', async () => { - await openTerminal(page); - await page.evaluate(`window.marker = window.term.registerMarker(1)`); - await page.evaluate(`window.marker.dispose()`); - await pollFor(page, `window.decoration = window.term.registerDecoration({ marker: window.marker });`, undefined); - }); - it('should throw when a negative x offset is provided', async () => { - await openTerminal(page); - await page.evaluate(`window.marker = window.term.registerMarker(1)`); - await page.evaluate(` - try { - window.decoration = window.term.registerDecoration({ marker: window.marker, x: -2 }); - } catch (e) { - window.throwMessage = e.message; - } - `); - await pollFor(page, 'window.throwMessage', 'This API only accepts positive integers'); - }); - }); - describe('overviewRulerDecorations', () => { - it('should not add an overview ruler when width is not set', async () => { - await openTerminal(page); - await page.evaluate(`window.marker1 = window.term.registerMarker(1)`); - await page.evaluate(`window.marker2 = window.term.registerMarker(2)`); - await page.evaluate(`window.term.registerDecoration({ marker: window.marker1, overviewRulerOptions: { color: 'red', position: 'full' } })`); - await page.evaluate(`window.term.registerDecoration({ marker: window.marker2, overviewRulerOptions: { color: 'blue', position: 'full' } })`); - await openTerminal(page); - await pollFor(page, `document.querySelectorAll('.xterm-decoration-overview-ruler').length`, 0); - }); - it('should add an overview ruler when width is set', async () => { - await openTerminal(page, { overviewRulerWidth: 15 }); - await page.evaluate(`window.marker1 = window.term.registerMarker(1)`); - await page.evaluate(`window.marker2 = window.term.registerMarker(2)`); - await page.evaluate(`window.term.registerDecoration({ marker: window.marker1, overviewRulerOptions: { color: 'red', position: 'full' } })`); - await page.evaluate(`window.term.registerDecoration({ marker: window.marker2, overviewRulerOptions: { color: 'blue', position: 'full' } })`); - await openTerminal(page); - await pollFor(page, `document.querySelectorAll('.xterm-decoration-overview-ruler').length`, 1); - }); - }); - }); - - describe('registerLinkProvider', () => { - it('should fire provideLinks when hovering cells', async () => { - await openTerminal(page); - // Focus the terminal as the cursor will show and trigger a rerender, which can clear the - // active link - await page.evaluate('window.term.focus()'); - await page.evaluate(` - window.calls = []; - window.disposable = window.term.registerLinkProvider({ - provideLinks: (position, cb) => { - calls.push(position); - cb(undefined); - } - }); - `); - const dims = await getDimensions(); - await moveMouseCell(page, dims, 1, 1); - await moveMouseCell(page, dims, 2, 2); - await moveMouseCell(page, dims, 10, 4); - await pollFor(page, `window.calls`, [1, 2, 4]); - await page.evaluate(`window.disposable.dispose()`); - }); - - it('should fire hover and leave events on the link', async () => { - await openTerminal(page); - // Focus the terminal as the cursor will show and trigger a rerender, which can clear the - // active link - await page.evaluate('window.term.focus()'); - await writeSync(page, 'foo bar baz'); - // Wait for renderer to catch up as links are cleared on render - await pollFor(page, `document.querySelector('.xterm-rows').textContent`, 'foo bar baz '); - await page.evaluate(` - window.calls = []; - window.disposable = window.term.registerLinkProvider({ - provideLinks: (position, cb) => { - window.calls.push('provide ' + position); - if (position === 1) { - window.calls.push('match'); - cb([{ - range: { start: { x: 5, y: 1 }, end: { x: 7, y: 1 } }, - text: 'bar', - activate: () => window.calls.push('activate'), - hover: () => window.calls.push('hover'), - leave: () => window.calls.push('leave') - }]); - } - } - }); - `); - const dims = await getDimensions(); - await moveMouseCell(page, dims, 5, 1); - await timeout(100); - await moveMouseCell(page, dims, 4, 1); - await pollFor(page, `window.calls`, ['provide 1', 'match', 'hover', 'leave' ]); - await moveMouseCell(page, dims, 7, 1); - await timeout(100); - await moveMouseCell(page, dims, 8, 1); - await pollFor(page, `window.calls`, ['provide 1', 'match', 'hover', 'leave', 'hover', 'leave']); - await page.evaluate(`window.disposable.dispose()`); - }); - - it('should work fine when hover and leave callbacks are not provided', async () => { - await openTerminal(page); - // Focus the terminal as the cursor will show and trigger a rerender, which can clear the - // active link - await page.evaluate('window.term.focus()'); - await writeSync(page, 'foo bar baz'); - // Wait for renderer to catch up as links are cleared on render - await pollFor(page, `document.querySelector('.xterm-rows').textContent`, 'foo bar baz '); - await page.evaluate(` - window.calls = []; - window.disposable = window.term.registerLinkProvider({ - provideLinks: (position, cb) => { - window.calls.push('provide ' + position); - if (position === 1) { - window.calls.push('match 1'); - cb([{ - range: { start: { x: 5, y: 1 }, end: { x: 7, y: 1 } }, - text: 'bar', - activate: () => window.calls.push('activate') - }]); - } else if (position === 2) { - window.calls.push('match 2'); - cb([{ - range: { start: { x: 5, y: 2 }, end: { x: 7, y: 2 } }, - text: 'bar', - activate: () => window.calls.push('activate') - }]); - } - } - }); - `); - const dims = await getDimensions(); - await moveMouseCell(page, dims, 5, 1); - await pollFor(page, `window.calls`, ['provide 1', 'match 1']); - await moveMouseCell(page, dims, 4, 2); - await pollFor(page, `window.calls`, ['provide 1', 'match 1', 'provide 2', 'match 2']); - await moveMouseCell(page, dims, 7, 1); - await pollFor(page, `window.calls`, ['provide 1', 'match 1', 'provide 2', 'match 2', 'provide 1', 'match 1']); - await moveMouseCell(page, dims, 6, 2); - await pollFor(page, `window.calls`, ['provide 1', 'match 1', 'provide 2', 'match 2', 'provide 1', 'match 1', 'provide 2', 'match 2']); - await page.evaluate(`window.disposable.dispose()`); - }); - - it('should fire activate events when clicking the link', async () => { - await openTerminal(page); - // Focus the terminal as the cursor will show and trigger a rerender, which can clear the - // active link - await page.evaluate('window.term.focus()'); - await writeSync(page, 'a b c'); - // Wait for renderer to catch up as links are cleared on render - await pollFor(page, `document.querySelector('.xterm-rows').textContent`, 'a b c '); - await page.evaluate(` - window.calls = []; - window.disposable = window.term.registerLinkProvider({ - provideLinks: (y, cb) => { - window.calls.push('provide ' + y); - cb([{ - range: { start: { x: 1, y }, end: { x: 80, y } }, - text: window.term.buffer.active.getLine(y - 1).translateToString(), - activate: (_, text) => window.calls.push('activate ' + y), - hover: () => window.calls.push('hover ' + y), - leave: () => window.calls.push('leave ' + y) - }]); - } - }); - `); - const dims = await getDimensions(); - await moveMouseCell(page, dims, 3, 1); - await pollFor(page, `window.calls`, ['provide 1', 'hover 1']); - await page.mouse.down(); - await page.mouse.up(); - await pollFor(page, `window.calls`, ['provide 1', 'hover 1', 'activate 1']); - await moveMouseCell(page, dims, 1, 2); - await pollFor(page, `window.calls`, ['provide 1', 'hover 1', 'activate 1', 'leave 1', 'provide 2', 'hover 2']); - await page.mouse.down(); - await page.mouse.up(); - await pollFor(page, `window.calls`, ['provide 1', 'hover 1', 'activate 1', 'leave 1', 'provide 2', 'hover 2', 'activate 2']); - await moveMouseCell(page, dims, 5, 1); - await pollFor(page, `window.calls`, ['provide 1', 'hover 1', 'activate 1', 'leave 1', 'provide 2', 'hover 2', 'activate 2', 'leave 2', 'provide 1', 'hover 1']); - await page.mouse.down(); - await page.mouse.up(); - await pollFor(page, `window.calls`, ['provide 1', 'hover 1', 'activate 1', 'leave 1', 'provide 2', 'hover 2', 'activate 2', 'leave 2', 'provide 1', 'hover 1', 'activate 1']); - await page.evaluate(`window.disposable.dispose()`); - }); - - it('should work when multiple links are provided on the same line', async () => { - await openTerminal(page); - // Focus the terminal as the cursor will show and trigger a rerender, which can clear the - // active link - await page.evaluate('window.term.focus()'); - await writeSync(page, 'foo bar baz'); - // Wait for renderer to catch up as links are cleared on render - await pollFor(page, `document.querySelector('.xterm-rows').textContent`, 'foo bar baz '); - await page.evaluate(` - window.calls = []; - window.disposable = window.term.registerLinkProvider({ - provideLinks: (position, cb) => { - window.calls.push('provide ' + position); - if (position === 1) { - cb([{ - range: { start: { x: 1, y: 1 }, end: { x: 3, y: 1 } }, - text: '', - activate: () => window.calls.push('activate'), - hover: () => window.calls.push('hover 1-3'), - leave: () => window.calls.push('leave 1-3') - }, { - range: { start: { x: 5, y: 1 }, end: { x: 7, y: 1 } }, - text: '', - activate: () => window.calls.push('activate'), - hover: () => window.calls.push('hover 5-7'), - leave: () => window.calls.push('leave 5-7') - }, { - range: { start: { x: 9, y: 1 }, end: { x: 11, y: 1 } }, - text: '', - activate: () => window.calls.push('activate'), - hover: () => window.calls.push('hover 9-11'), - leave: () => window.calls.push('leave 9-11') - }]); - } - } - }); - `); - const dims = await getDimensions(); - await moveMouseCell(page, dims, 2, 1); - await pollFor(page, `window.calls`, ['provide 1', 'hover 1-3']); - await moveMouseCell(page, dims, 6, 1); - await pollFor(page, `window.calls`, ['provide 1', 'hover 1-3', 'leave 1-3', 'hover 5-7']); - await moveMouseCell(page, dims, 6, 2); - await pollFor(page, `window.calls`, ['provide 1', 'hover 1-3', 'leave 1-3', 'hover 5-7', 'leave 5-7', 'provide 2']); - await moveMouseCell(page, dims, 10, 1); - await pollFor(page, `window.calls`, ['provide 1', 'hover 1-3', 'leave 1-3', 'hover 5-7', 'leave 5-7', 'provide 2', 'provide 1', 'hover 9-11']); - await page.evaluate(`window.disposable.dispose()`); - }); - - it('should dispose links when hovering away', async () => { - await openTerminal(page); - // Focus the terminal as the cursor will show and trigger a rerender, which can clear the - // active link - await page.evaluate('window.term.focus()'); - await writeSync(page, 'foo bar baz'); - // Wait for renderer to catch up as links are cleared on render - await pollFor(page, `document.querySelector('.xterm-rows').textContent`, 'foo bar baz '); - await page.evaluate(` - window.calls = []; - window.disposable = window.term.registerLinkProvider({ - provideLinks: (position, cb) => { - window.calls.push('provide ' + position); - if (position === 1) { - cb([{ - range: { start: { x: 1, y: 1 }, end: { x: 3, y: 1 } }, - text: '', - activate: () => window.calls.push('activate'), - dispose: () => window.calls.push('dispose 1-3'), - hover: () => window.calls.push('hover 1-3'), - leave: () => window.calls.push('leave 1-3') - }, { - range: { start: { x: 5, y: 1 }, end: { x: 7, y: 1 } }, - text: '', - activate: () => window.calls.push('activate'), - dispose: () => window.calls.push('dispose 5-7'), - hover: () => window.calls.push('hover 5-7'), - leave: () => window.calls.push('leave 5-7') - }, { - range: { start: { x: 9, y: 1 }, end: { x: 11, y: 1 } }, - text: '', - activate: () => window.calls.push('activate'), - dispose: () => window.calls.push('dispose 9-11'), - hover: () => window.calls.push('hover 9-11'), - leave: () => window.calls.push('leave 9-11') - }]); - } - } - }); - `); - const dims = await getDimensions(); - await moveMouseCell(page, dims, 2, 1); - await pollFor(page, `window.calls`, ['provide 1', 'hover 1-3']); - await moveMouseCell(page, dims, 6, 1); - await pollFor(page, `window.calls`, ['provide 1', 'hover 1-3', 'leave 1-3', 'hover 5-7']); - await moveMouseCell(page, dims, 6, 2); - await pollFor(page, `window.calls`, ['provide 1', 'hover 1-3', 'leave 1-3', 'hover 5-7', 'leave 5-7', 'dispose 1-3', 'dispose 5-7', 'dispose 9-11', 'provide 2']); - await moveMouseCell(page, dims, 10, 1); - await pollFor(page, `window.calls`, ['provide 1', 'hover 1-3', 'leave 1-3', 'hover 5-7', 'leave 5-7', 'dispose 1-3', 'dispose 5-7', 'dispose 9-11', 'provide 2', 'provide 1', 'hover 9-11']); - await moveMouseCell(page, dims, 10, 2); - await pollFor(page, `window.calls`, ['provide 1', 'hover 1-3', 'leave 1-3', 'hover 5-7', 'leave 5-7', 'dispose 1-3', 'dispose 5-7', 'dispose 9-11', 'provide 2', 'provide 1', 'hover 9-11', 'leave 9-11', 'dispose 1-3', 'dispose 5-7', 'dispose 9-11', 'provide 2']); - await page.evaluate(`window.disposable.dispose()`); - }); - }); -}); - -interface IDimensions { - top: number; - left: number; - renderDimensions: IRenderDimensions; -} - -async function getDimensions(): Promise { - return await page.evaluate(` - (function() { - const rect = document.querySelector('.xterm-rows').getBoundingClientRect(); - return { - top: rect.top, - left: rect.left, - renderDimensions: window.term._core._renderService.dimensions - }; - })(); - `); -} - -async function getCellCoordinates(dimensions: IDimensions, col: number, row: number): Promise<{ x: number, y: number }> { - return { - x: dimensions.left + dimensions.renderDimensions.device.cell.width * (col - 0.5), - y: dimensions.top + dimensions.renderDimensions.device.cell.height * (row - 0.5) - }; -} - -async function moveMouseCell(page: Page, dimensions: IDimensions, col: number, row: number): Promise { - const coords = await getCellCoordinates(dimensions, col, row); - await page.mouse.move(coords.x, coords.y); -} diff --git a/test/playwright/InputHandler.test.ts b/test/playwright/InputHandler.test.ts index 1b5f225a..42b2916a 100644 --- a/test/playwright/InputHandler.test.ts +++ b/test/playwright/InputHandler.test.ts @@ -3,10 +3,9 @@ * @license MIT */ import { test } from '@playwright/test'; -import { deepStrictEqual, ok, strictEqual } from 'assert'; -import type { IDisposable } from 'xterm'; -import { createTestContext, ITestContext, openTerminal, pollFor } from './TestUtils'; +import { deepStrictEqual, ok } from 'assert'; import { IRenderDimensions } from 'browser/renderer/shared/Types'; +import { ITestContext, createTestContext, openTerminal, pollFor } from './TestUtils'; let ctx: ITestContext; test.beforeAll(async ({ browser }) => { diff --git a/test/playwright/Terminal.test.ts b/test/playwright/Terminal.test.ts new file mode 100644 index 00000000..6fe2f803 --- /dev/null +++ b/test/playwright/Terminal.test.ts @@ -0,0 +1,1053 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ +import { test } from '@playwright/test'; +import { deepStrictEqual, notStrictEqual, strictEqual } from 'assert'; +import { ITestContext, createTestContext, openTerminal, pollFor, timeout } from './TestUtils'; +import { IRenderDimensions } from 'browser/renderer/shared/Types'; + +let ctx: ITestContext; +test.beforeAll(async ({ browser }) => { + ctx = await createTestContext(browser); + await openTerminal(ctx); +}); +test.afterAll(async () => await ctx.page.close()); + + +test.describe.only('API Integration Tests', () => { + test('Default options', async () => { + strictEqual(await ctx.proxy.cols, 80); + strictEqual(await ctx.proxy.rows, 24); + }); + + test('Proposed API check', async () => { + await openTerminal(ctx, { allowProposedApi: false }); + await ctx.page.evaluate(` + try { + window.term.markers; + } catch (e) { + window.throwMessage = e.message; + } + `); + await pollFor(ctx.page, 'window.throwMessage', 'You must set the allowProposedApi option to true to use proposed API'); + }); + + test('write', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + window.term.write('foo'); + window.term.write('bar'); + window.term.write('文'); + `); + await pollFor(ctx.page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foobar文'); + }); + + test('write with callback', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + window.term.write('foo', () => { window.__x = 'a'; }); + window.term.write('bar', () => { window.__x += 'b'; }); + window.term.write('文', () => { window.__x += 'c'; }); + `); + await pollFor(ctx.page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foobar文'); + await pollFor(ctx.page, `window.__x`, 'abc'); + }); + + test('write - bytes (UTF8)', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + window.term.write(new Uint8Array([102, 111, 111])); // foo + window.term.write(new Uint8Array([98, 97, 114])); // bar + window.term.write(new Uint8Array([230, 150, 135])); // 文 + `); + await pollFor(ctx.page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foobar文'); + }); + + test('write - bytes (UTF8) with callback', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + window.term.write(new Uint8Array([102, 111, 111]), () => { window.__x = 'A'; }); // foo + window.term.write(new Uint8Array([98, 97, 114]), () => { window.__x += 'B'; }); // bar + window.term.write(new Uint8Array([230, 150, 135]), () => { window.__x += 'C'; }); // 文 + `); + await pollFor(ctx.page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foobar文'); + await pollFor(ctx.page, `window.__x`, 'ABC'); + }); + + test('writeln', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + window.term.writeln('foo'); + window.term.writeln('bar'); + window.term.writeln('文'); + `); + await pollFor(ctx.page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foo'); + await pollFor(ctx.page, `window.term.buffer.active.getLine(1).translateToString(true)`, 'bar'); + await pollFor(ctx.page, `window.term.buffer.active.getLine(2).translateToString(true)`, '文'); + }); + + test('writeln with callback', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + window.term.writeln('foo', () => { window.__x = '1'; }); + window.term.writeln('bar', () => { window.__x += '2'; }); + window.term.writeln('文', () => { window.__x += '3'; }); + `); + await pollFor(ctx.page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foo'); + await pollFor(ctx.page, `window.term.buffer.active.getLine(1).translateToString(true)`, 'bar'); + await pollFor(ctx.page, `window.term.buffer.active.getLine(2).translateToString(true)`, '文'); + await pollFor(ctx.page, `window.__x`, '123'); + }); + + test('writeln - bytes (UTF8)', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + window.term.writeln(new Uint8Array([102, 111, 111])); + window.term.writeln(new Uint8Array([98, 97, 114])); + window.term.writeln(new Uint8Array([230, 150, 135])); + `); + await pollFor(ctx.page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foo'); + await pollFor(ctx.page, `window.term.buffer.active.getLine(1).translateToString(true)`, 'bar'); + await pollFor(ctx.page, `window.term.buffer.active.getLine(2).translateToString(true)`, '文'); + }); + + test('paste', async () => { + await openTerminal(ctx); + const calls: string[] = []; + ctx.proxy.onData(e => calls.push(e)); + await ctx.proxy.paste('foo'); + await ctx.proxy.paste('\r\nfoo\nbar\r'); + await ctx.proxy.write('\x1b[?2004h'); + await ctx.proxy.paste('foo'); + await ctx.page.evaluate(`window.term.options.ignoreBracketedPasteMode = true;`); + await ctx.proxy.paste('check_mode'); + deepStrictEqual(calls, ['foo', '\rfoo\rbar\r', '\x1b[200~foo\x1b[201~', 'check_mode']); + }); + + test('clear', async () => { + await openTerminal(ctx, { rows: 5 }); + await ctx.page.evaluate(` + window.term.write('test0'); + window.parsed = 0; + for (let i = 1; i < 10; i++) { + window.term.write('\\n\\rtest' + i, () => window.parsed++); + } + `); + await pollFor(ctx.page, `window.parsed`, 9); + await ctx.page.evaluate(`window.term.clear()`); + await pollFor(ctx.page, `window.term.buffer.active.length`, 5); + await pollFor(ctx.page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'test9'); + for (let i = 1; i < 5; i++) { + await pollFor(ctx.page, `window.term.buffer.active.getLine(${i}).translateToString(true)`, ''); + } + }); + + test.describe('options', () => { + test('getter', async () => { + await openTerminal(ctx); + strictEqual(await ctx.page.evaluate(`window.term.options.cols`), 80); + strictEqual(await ctx.page.evaluate(`window.term.options.rows`), 24); + }); + test('setter', async () => { + await openTerminal(ctx); + try { + await ctx.page.evaluate('window.term.options.cols = 40'); + test.fail(); + } catch {} + try { + await ctx.page.evaluate('window.term.options.rows = 20'); + test.fail(); + } catch {} + await ctx.page.evaluate('window.term.options.scrollback = 1'); + strictEqual(await ctx.page.evaluate(`window.term.options.scrollback`), 1); + await ctx.page.evaluate(` + window.term.options = { + fontSize: 30, + fontFamily: 'Arial' + }; + `); + strictEqual(await ctx.page.evaluate(`window.term.options.fontSize`), 30); + strictEqual(await ctx.page.evaluate(`window.term.options.fontFamily`), 'Arial'); + }); + test('object.keys return the correct number of options', async () => { + await openTerminal(ctx); + notStrictEqual(await ctx.page.evaluate(`Object.keys(window.term.options).length`), 0); + }); + }); + + test.describe('renderer', () => { + test('foreground', async () => { + await openTerminal(ctx); + await ctx.proxy.write('\x1b[30m0\x1b[31m1\x1b[32m2\x1b[33m3\x1b[34m4\x1b[35m5\x1b[36m6\x1b[37m7'); + await pollFor(ctx.page, `document.querySelectorAll('.xterm-rows > :nth-child(1) > *').length`, 9); + deepStrictEqual(await ctx.page.evaluate(` + [ + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(1)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(2)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(3)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(4)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(5)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(6)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(7)').className + ] + `), [ + 'xterm-fg-0', + 'xterm-fg-1', + 'xterm-fg-2', + 'xterm-fg-3', + 'xterm-fg-4', + 'xterm-fg-5', + 'xterm-fg-6' + ]); + }); + + test('background', async () => { + await openTerminal(ctx); + await ctx.proxy.write('\x1b[40m0\x1b[41m1\x1b[42m2\x1b[43m3\x1b[44m4\x1b[45m5\x1b[46m6\x1b[47m7'); + await pollFor(ctx.page, `document.querySelectorAll('.xterm-rows > :nth-child(1) > *').length`, 9); + deepStrictEqual(await ctx.page.evaluate(` + [ + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(1)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(2)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(3)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(4)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(5)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(6)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(7)').className + ] + `), [ + 'xterm-bg-0', + 'xterm-bg-1', + 'xterm-bg-2', + 'xterm-bg-3', + 'xterm-bg-4', + 'xterm-bg-5', + 'xterm-bg-6' + ]); + }); + }); + + test('selection', async () => { + await openTerminal(ctx, { rows: 5, cols: 5 }); + await ctx.proxy.write(`\n\nfoo\n\n\rbar\n\n\rbaz`); + strictEqual(await ctx.page.evaluate(`window.term.hasSelection()`), false); + strictEqual(await ctx.page.evaluate(`window.term.getSelection()`), ''); + deepStrictEqual(await ctx.page.evaluate(`window.term.getSelectionPosition()`), undefined); + await ctx.page.evaluate(`window.term.selectAll()`); + strictEqual(await ctx.page.evaluate(`window.term.hasSelection()`), true); + if (process.platform === 'win32') { + strictEqual(await ctx.page.evaluate(`window.term.getSelection()`), '\r\n\r\nfoo\r\n\r\nbar\r\n\r\nbaz'); + } else { + strictEqual(await ctx.page.evaluate(`window.term.getSelection()`), '\n\nfoo\n\nbar\n\nbaz'); + } + deepStrictEqual(await ctx.page.evaluate(`window.term.getSelectionPosition()`), { start: { x: 0, y: 0 }, end: { x: 5, y: 6 } }); + await ctx.page.evaluate(`window.term.clearSelection()`); + strictEqual(await ctx.page.evaluate(`window.term.hasSelection()`), false); + strictEqual(await ctx.page.evaluate(`window.term.getSelection()`), ''); + deepStrictEqual(await ctx.page.evaluate(`window.term.getSelectionPosition()`), undefined); + await ctx.page.evaluate(`window.term.select(1, 2, 2)`); + strictEqual(await ctx.page.evaluate(`window.term.hasSelection()`), true); + strictEqual(await ctx.page.evaluate(`window.term.getSelection()`), 'oo'); + deepStrictEqual(await ctx.page.evaluate(`window.term.getSelectionPosition()`), { start: { x: 1, y: 2 }, end: { x: 3, y: 2 } }); + }); + + test('focus, blur', async () => { + await openTerminal(ctx); + strictEqual(await ctx.page.evaluate(`document.activeElement.className`), ''); + await ctx.page.evaluate(`window.term.focus()`); + strictEqual(await ctx.page.evaluate(`document.activeElement.className`), 'xterm-helper-textarea'); + await ctx.page.evaluate(`window.term.blur()`); + strictEqual(await ctx.page.evaluate(`document.activeElement.className`), ''); + }); + + test.describe('loadAddon', () => { + test('constructor', async () => { + await openTerminal(ctx, { cols: 5 }); + await ctx.page.evaluate(` + window.cols = 0; + window.term.loadAddon({ + activate: (t) => window.cols = t.cols, + dispose: () => {} + }); + `); + strictEqual(await ctx.page.evaluate(`window.cols`), 5); + }); + + test('dispose (addon)', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + window.disposeCalled = false + window.addon = { + activate: () => {}, + dispose: () => window.disposeCalled = true + }; + window.term.loadAddon(window.addon); + `); + strictEqual(await ctx.page.evaluate(`window.disposeCalled`), false); + await ctx.page.evaluate(`window.addon.dispose()`); + strictEqual(await ctx.page.evaluate(`window.disposeCalled`), true); + }); + + test('dispose (terminal)', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + window.disposeCalled = false + window.term.loadAddon({ + activate: () => {}, + dispose: () => window.disposeCalled = true + }); + `); + strictEqual(await ctx.page.evaluate(`window.disposeCalled`), false); + await ctx.page.evaluate(`window.term.dispose()`); + strictEqual(await ctx.page.evaluate(`window.disposeCalled`), true); + }); + }); + + test.describe('Events', () => { + test('onCursorMove', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + window.callCount = 0; + window.term.onCursorMove(e => window.callCount++); + window.term.write('foo'); + `); + await pollFor(ctx.page, `window.callCount`, 1); + await ctx.page.evaluate(`window.term.write('bar')`); + await pollFor(ctx.page, `window.callCount`, 2); + }); + + test('onData', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + window.calls = []; + window.term.onData(e => calls.push(e)); + `); + await ctx.page.type('.xterm-helper-textarea', 'foo'); + deepStrictEqual(await ctx.page.evaluate(`window.calls`), ['f', 'o', 'o']); + }); + + test('onKey', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + window.calls = []; + window.term.onKey(e => calls.push(e.key)); + `); + await ctx.page.type('.xterm-helper-textarea', 'foo'); + deepStrictEqual(await ctx.page.evaluate(`window.calls`), ['f', 'o', 'o']); + }); + + test('onLineFeed', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + window.callCount = 0; + window.term.onLineFeed(() => callCount++); + window.term.writeln('foo'); + `); + await pollFor(ctx.page, `window.callCount`, 1); + await ctx.page.evaluate(`window.term.writeln('bar')`); + await pollFor(ctx.page, `window.callCount`, 2); + }); + + test('onScroll', async () => { + await openTerminal(ctx, { rows: 5 }); + await ctx.page.evaluate(` + window.calls = []; + window.term.onScroll(e => window.calls.push(e)); + for (let i = 0; i < 4; i++) { + window.term.writeln('foo'); + } + `); + await pollFor(ctx.page, `window.calls`, []); + await ctx.page.evaluate(`window.term.writeln('bar')`); + await pollFor(ctx.page, `window.calls`, [1]); + await ctx.page.evaluate(`window.term.writeln('baz')`); + await pollFor(ctx.page, `window.calls`, [1, 2]); + }); + + test('onSelectionChange', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + window.callCount = 0; + window.term.onSelectionChange(() => window.callCount++); + `); + await pollFor(ctx.page, `window.callCount`, 0); + await ctx.page.evaluate(`window.term.selectAll()`); + await pollFor(ctx.page, `window.callCount`, 1); + await ctx.page.evaluate(`window.term.clearSelection()`); + await pollFor(ctx.page, `window.callCount`, 2); + }); + + test('onRender', async function(): Promise { + await openTerminal(ctx); + await timeout(20); // Ensure all init events are fired + await ctx.page.evaluate(` + window.calls = []; + window.term.onRender(e => window.calls.push([e.start, e.end])); + `); + await pollFor(ctx.page, `window.calls`, []); + await ctx.page.evaluate(`window.term.write('foo')`); + await pollFor(ctx.page, `window.calls`, [[0, 0]]); + await ctx.page.evaluate(`window.term.write('bar\\n\\nbaz')`); + await pollFor(ctx.page, `window.calls`, [[0, 0], [0, 2]]); + }); + + test('onResize', async () => { + await openTerminal(ctx); + await timeout(20); // Ensure all init events are fired + await ctx.page.evaluate(` + window.calls = []; + window.term.onResize(e => window.calls.push([e.cols, e.rows])); + `); + await pollFor(ctx.page, `window.calls`, []); + await ctx.page.evaluate(`window.term.resize(10, 5)`); + await pollFor(ctx.page, `window.calls`, [[10, 5]]); + await ctx.page.evaluate(`window.term.resize(20, 15)`); + await pollFor(ctx.page, `window.calls`, [[10, 5], [20, 15]]); + }); + + test('onTitleChange', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + window.calls = []; + window.term.onTitleChange(e => window.calls.push(e)); + `); + await pollFor(ctx.page, `window.calls`, []); + await ctx.page.evaluate(`window.term.write('\x1b]2;foo\x9c')`); + await pollFor(ctx.page, `window.calls`, ['foo']); + }); + test('onBell', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + window.calls = []; + window.term.onBell(() => window.calls.push(true)); + `); + await pollFor(ctx.page, `window.calls`, []); + await ctx.page.evaluate(`window.term.write('\x07')`); + await pollFor(ctx.page, `window.calls`, [true]); + }); + }); + + test.describe('buffer', () => { + test('cursorX, cursorY', async () => { + await openTerminal(ctx, { rows: 5, cols: 5 }); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.cursorX`), 0); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.cursorY`), 0); + await ctx.proxy.write('foo'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.cursorX`), 3); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.cursorY`), 0); + await ctx.proxy.write('\n'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.cursorX`), 3); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.cursorY`), 1); + await ctx.proxy.write('\r'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.cursorX`), 0); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.cursorY`), 1); + await ctx.proxy.write('abcde'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.cursorX`), 5); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.cursorY`), 1); + await ctx.proxy.write('\n\r\n\n\n\n\n'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.cursorX`), 0); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.cursorY`), 4); + }); + + test('viewportY', async () => { + await openTerminal(ctx, { rows: 5 }); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.viewportY`), 0); + await ctx.proxy.write('\n\n\n\n'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.viewportY`), 0); + await ctx.proxy.write('\n'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.viewportY`), 1); + await ctx.proxy.write('\n\n\n\n'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.viewportY`), 5); + await ctx.page.evaluate(`window.term.scrollLines(-1)`); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.viewportY`), 4); + await ctx.page.evaluate(`window.term.scrollToTop()`); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.viewportY`), 0); + }); + + test('baseY', async () => { + await openTerminal(ctx, { rows: 5 }); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.baseY`), 0); + await ctx.proxy.write('\n\n\n\n'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.baseY`), 0); + await ctx.proxy.write('\n'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.baseY`), 1); + await ctx.proxy.write('\n\n\n\n'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.baseY`), 5); + await ctx.page.evaluate(`window.term.scrollLines(-1)`); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.baseY`), 5); + await ctx.page.evaluate(`window.term.scrollToTop()`); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.baseY`), 5); + }); + + test('length', async () => { + await openTerminal(ctx, { rows: 5 }); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.length`), 5); + await ctx.proxy.write('\n\n\n\n'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.length`), 5); + await ctx.proxy.write('\n'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.length`), 6); + await ctx.proxy.write('\n\n\n\n'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.length`), 10); + }); + + test.describe('getLine', () => { + test('invalid index', async () => { + await openTerminal(ctx, { rows: 5 }); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(-1)`), undefined); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(5)`), undefined); + }); + + test('isWrapped', async () => { + await openTerminal(ctx, { cols: 5 }); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).isWrapped`), false); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(1).isWrapped`), false); + await ctx.proxy.write('abcde'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).isWrapped`), false); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(1).isWrapped`), false); + await ctx.proxy.write('f'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).isWrapped`), false); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(1).isWrapped`), true); + }); + + test('translateToString', async () => { + await openTerminal(ctx, { cols: 5 }); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).translateToString()`), ' '); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).translateToString(true)`), ''); + await ctx.proxy.write('foo'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).translateToString()`), 'foo '); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).translateToString(true)`), 'foo'); + await ctx.proxy.write('bar'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).translateToString()`), 'fooba'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).translateToString(true)`), 'fooba'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(1).translateToString(true)`), 'r'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).translateToString(false, 1)`), 'ooba'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).translateToString(false, 1, 3)`), 'oo'); + }); + + test('getCell', async () => { + await openTerminal(ctx, { cols: 5 }); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).getCell(-1)`), undefined); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).getCell(5)`), undefined); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).getCell(0).getChars()`), ''); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).getCell(0).getWidth()`), 1); + await ctx.proxy.write('a文'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).getCell(0).getChars()`), 'a'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).getCell(0).getWidth()`), 1); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).getCell(1).getChars()`), '文'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).getCell(1).getWidth()`), 2); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).getCell(2).getChars()`), ''); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).getCell(2).getWidth()`), 0); + }); + + test('clearMarkers', async () => { + await openTerminal(ctx, { cols: 5 }); + await ctx.page.evaluate(` + window.disposeStack = []; + `); + await ctx.proxy.write('\n\n\n\n'); + await ctx.proxy.write('\n\n\n\n'); + await ctx.proxy.write('\n\n\n\n'); + await ctx.proxy.write('\n\n\n\n'); + await ctx.page.evaluate(`window.term.registerMarker(1)`); + await ctx.page.evaluate(`window.term.registerMarker(2)`); + await ctx.page.evaluate(`window.term.scrollLines(10)`); + await ctx.page.evaluate(`window.term.registerMarker(3)`); + await ctx.page.evaluate(`window.term.registerMarker(4)`); + await ctx.page.evaluate(` + for (let i = 0; i < window.term.markers.length; ++i) { + const marker = window.term.markers[i]; + marker.onDispose(() => window.disposeStack.push(marker)); + }`); + await ctx.page.evaluate(`window.term.clear()`); + strictEqual(await ctx.page.evaluate(`window.disposeStack.length`), 4); + }); + }); + + test('active, normal, alternate', async () => { + await openTerminal(ctx, { cols: 5 }); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.type`), 'normal'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.normal.type`), 'normal'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.alternate.type`), 'alternate'); + + await ctx.proxy.write('norm '); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).translateToString()`), 'norm '); + strictEqual(await ctx.page.evaluate(`window.term.buffer.normal.getLine(0).translateToString()`), 'norm '); + strictEqual(await ctx.page.evaluate(`window.term.buffer.alternate.getLine(0)`), undefined); + + await ctx.proxy.write('\x1b[?47h\r'); // use alternate screen buffer + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.type`), 'alternate'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.normal.type`), 'normal'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.alternate.type`), 'alternate'); + + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).translateToString()`), ' '); + await ctx.proxy.write('alt '); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).translateToString()`), 'alt '); + strictEqual(await ctx.page.evaluate(`window.term.buffer.normal.getLine(0).translateToString()`), 'norm '); + strictEqual(await ctx.page.evaluate(`window.term.buffer.alternate.getLine(0).translateToString()`), 'alt '); + + await ctx.proxy.write('\x1b[?47l\r'); // use normal screen buffer + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.type`), 'normal'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.normal.type`), 'normal'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.alternate.type`), 'alternate'); + + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).translateToString()`), 'norm '); + strictEqual(await ctx.page.evaluate(`window.term.buffer.normal.getLine(0).translateToString()`), 'norm '); + strictEqual(await ctx.page.evaluate(`window.term.buffer.alternate.getLine(0)`), undefined); + }); + }); + + test.describe('modes', () => { + test.beforeEach(() => openTerminal(ctx)); + test('defaults', async () => { + deepStrictEqual(await ctx.page.evaluate(`window.term.modes`), { + applicationCursorKeysMode: false, + applicationKeypadMode: false, + bracketedPasteMode: false, + insertMode: false, + mouseTrackingMode: 'none', + originMode: false, + reverseWraparoundMode: false, + sendFocusMode: false, + wraparoundMode: true + }); + }); + test('applicationCursorKeysMode', async () => { + await ctx.proxy.write('\x1b[?1h'); + strictEqual(await ctx.page.evaluate(`window.term.modes.applicationCursorKeysMode`), true); + await ctx.proxy.write('\x1b[?1l'); + strictEqual(await ctx.page.evaluate(`window.term.modes.applicationCursorKeysMode`), false); + }); + test('applicationKeypadMode', async () => { + await ctx.proxy.write('\x1b[?66h'); + strictEqual(await ctx.page.evaluate(`window.term.modes.applicationKeypadMode`), true); + await ctx.proxy.write('\x1b[?66l'); + strictEqual(await ctx.page.evaluate(`window.term.modes.applicationKeypadMode`), false); + }); + test('bracketedPasteMode', async () => { + await ctx.proxy.write('\x1b[?2004h'); + strictEqual(await ctx.page.evaluate(`window.term.modes.bracketedPasteMode`), true); + await ctx.proxy.write('\x1b[?2004l'); + strictEqual(await ctx.page.evaluate(`window.term.modes.bracketedPasteMode`), false); + }); + test('insertMode', async () => { + await ctx.proxy.write('\x1b[4h'); + strictEqual(await ctx.page.evaluate(`window.term.modes.insertMode`), true); + await ctx.proxy.write('\x1b[4l'); + strictEqual(await ctx.page.evaluate(`window.term.modes.insertMode`), false); + }); + test('mouseTrackingMode', async () => { + await ctx.proxy.write('\x1b[?9h'); + strictEqual(await ctx.page.evaluate(`window.term.modes.mouseTrackingMode`), 'x10'); + await ctx.proxy.write('\x1b[?9l'); + strictEqual(await ctx.page.evaluate(`window.term.modes.mouseTrackingMode`), 'none'); + await ctx.proxy.write('\x1b[?1000h'); + strictEqual(await ctx.page.evaluate(`window.term.modes.mouseTrackingMode`), 'vt200'); + await ctx.proxy.write('\x1b[?1000l'); + strictEqual(await ctx.page.evaluate(`window.term.modes.mouseTrackingMode`), 'none'); + await ctx.proxy.write('\x1b[?1002h'); + strictEqual(await ctx.page.evaluate(`window.term.modes.mouseTrackingMode`), 'drag'); + await ctx.proxy.write('\x1b[?1002l'); + strictEqual(await ctx.page.evaluate(`window.term.modes.mouseTrackingMode`), 'none'); + await ctx.proxy.write('\x1b[?1003h'); + strictEqual(await ctx.page.evaluate(`window.term.modes.mouseTrackingMode`), 'any'); + await ctx.proxy.write('\x1b[?1003l'); + strictEqual(await ctx.page.evaluate(`window.term.modes.mouseTrackingMode`), 'none'); + }); + test('originMode', async () => { + await ctx.proxy.write('\x1b[?6h'); + strictEqual(await ctx.page.evaluate(`window.term.modes.originMode`), true); + await ctx.proxy.write('\x1b[?6l'); + strictEqual(await ctx.page.evaluate(`window.term.modes.originMode`), false); + }); + test('reverseWraparoundMode', async () => { + await ctx.proxy.write('\x1b[?45h'); + strictEqual(await ctx.page.evaluate(`window.term.modes.reverseWraparoundMode`), true); + await ctx.proxy.write('\x1b[?45l'); + strictEqual(await ctx.page.evaluate(`window.term.modes.reverseWraparoundMode`), false); + }); + test('sendFocusMode', async () => { + await ctx.proxy.write('\x1b[?1004h'); + strictEqual(await ctx.page.evaluate(`window.term.modes.sendFocusMode`), true); + await ctx.proxy.write('\x1b[?1004l'); + strictEqual(await ctx.page.evaluate(`window.term.modes.sendFocusMode`), false); + }); + test('wraparoundMode', async () => { + await ctx.proxy.write('\x1b[?7h'); + strictEqual(await ctx.page.evaluate(`window.term.modes.wraparoundMode`), true); + await ctx.proxy.write('\x1b[?7l'); + strictEqual(await ctx.page.evaluate(`window.term.modes.wraparoundMode`), false); + }); + }); + + test('dispose', async () => { + await ctx.page.evaluate(` + if ('term' in window) { + try { + window.term.dispose(); + } catch {} + } + window.term = new Terminal(); + window.term.dispose(); + `); + strictEqual(await ctx.page.evaluate(`window.term._core._isDisposed`), true); + }); + + test('dispose (opened)', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + if ('term' in window) { + try { + window.term.dispose(); + } catch {} + } + `); + strictEqual(await ctx.page.evaluate(`window.term._core._isDisposed`), true); + }); + + test('render when visible after hidden', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + if ('term' in window) { + try { + window.term.dispose(); + } catch {} + } + `); + await ctx.page.evaluate(`document.querySelector('#terminal-container').style.display='none'`); + await ctx.page.evaluate(`window.term = new Terminal()`); + await ctx.page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`); + await ctx.page.evaluate(`document.querySelector('#terminal-container').style.display=''`); + await pollFor(ctx.page, `window.term._core._renderService.dimensions.css.cell.width > 0`, true); + }); + + test.describe('registerDecoration', () => { + test.describe('bufferDecorations', () => { + test('should register decorations and render them when terminal open is called', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(`window.marker1 = window.term.registerMarker(1)`); + await ctx.page.evaluate(`window.marker2 = window.term.registerMarker(2)`); + await ctx.page.evaluate(`window.term.registerDecoration({ marker: window.marker1 })`); + await ctx.page.evaluate(`window.term.registerDecoration({ marker: window.marker2 })`); + await pollFor(ctx.page, `document.querySelectorAll('.xterm-screen .xterm-decoration').length`, 2); + }); + test('should return undefined when the marker has already been disposed of', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(`window.marker = window.term.registerMarker(1)`); + await ctx.page.evaluate(`window.marker.dispose()`); + await pollFor(ctx.page, `window.decoration = window.term.registerDecoration({ marker: window.marker });`, undefined); + }); + test('should throw when a negative x offset is provided', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(`window.marker = window.term.registerMarker(1)`); + await ctx.page.evaluate(` + try { + window.decoration = window.term.registerDecoration({ marker: window.marker, x: -2 }); + } catch (e) { + window.throwMessage = e.message; + } + `); + await pollFor(ctx.page, 'window.throwMessage', 'This API only accepts positive integers'); + }); + }); + test.describe('overviewRulerDecorations', () => { + test('should not add an overview ruler when width is not set', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(`window.marker1 = window.term.registerMarker(1)`); + await ctx.page.evaluate(`window.marker2 = window.term.registerMarker(2)`); + await ctx.page.evaluate(`window.term.registerDecoration({ marker: window.marker1, overviewRulerOptions: { color: 'red', position: 'full' } })`); + await ctx.page.evaluate(`window.term.registerDecoration({ marker: window.marker2, overviewRulerOptions: { color: 'blue', position: 'full' } })`); + await pollFor(ctx.page, `document.querySelectorAll('.xterm-decoration-overview-ruler').length`, 0); + }); + test('should add an overview ruler when width is set', async () => { + await openTerminal(ctx, { overviewRulerWidth: 15 }); + await ctx.page.evaluate(`window.marker1 = window.term.registerMarker(1)`); + await ctx.page.evaluate(`window.marker2 = window.term.registerMarker(2)`); + await ctx.page.evaluate(`window.term.registerDecoration({ marker: window.marker1, overviewRulerOptions: { color: 'red', position: 'full' } })`); + await ctx.page.evaluate(`window.term.registerDecoration({ marker: window.marker2, overviewRulerOptions: { color: 'blue', position: 'full' } })`); + await pollFor(ctx.page, `document.querySelectorAll('.xterm-decoration-overview-ruler').length`, 1); + }); + }); + }); + + test.describe('registerLinkProvider', () => { + test('should fire provideLinks when hovering cells', async () => { + await openTerminal(ctx); + // Focus the terminal as the cursor will show and trigger a rerender, which can clear the + // active link + await ctx.proxy.focus(); + await ctx.page.evaluate(` + window.calls = []; + window.disposable = window.term.registerLinkProvider({ + provideLinks: (position, cb) => { + calls.push(position); + cb(undefined); + } + }); + `); + const dims = await getDimensions(); + await moveMouseCell(dims, 1, 1); + await moveMouseCell(dims, 2, 2); + await moveMouseCell(dims, 10, 4); + await pollFor(ctx.page, `window.calls`, [1, 2, 4]); + await ctx.page.evaluate(`window.disposable.dispose()`); + }); + + test('should fire hover and leave events on the link', async () => { + await openTerminal(ctx); + // Focus the terminal as the cursor will show and trigger a rerender, which can clear the + // active link + await ctx.page.evaluate('window.term.focus()'); + await ctx.proxy.write('foo bar baz'); + // Wait for renderer to catch up as links are cleared on render + await pollFor(ctx.page, `document.querySelector('.xterm-rows').textContent`, 'foo bar baz '); + await ctx.page.evaluate(` + window.calls = []; + window.disposable = window.term.registerLinkProvider({ + provideLinks: (position, cb) => { + window.calls.push('provide ' + position); + if (position === 1) { + window.calls.push('match'); + cb([{ + range: { start: { x: 5, y: 1 }, end: { x: 7, y: 1 } }, + text: 'bar', + activate: () => window.calls.push('activate'), + hover: () => window.calls.push('hover'), + leave: () => window.calls.push('leave') + }]); + } + } + }); + `); + const dims = await getDimensions(); + await moveMouseCell(dims, 5, 1); + await timeout(100); + await moveMouseCell(dims, 4, 1); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'match', 'hover', 'leave' ]); + await moveMouseCell(dims, 7, 1); + await timeout(100); + await moveMouseCell(dims, 8, 1); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'match', 'hover', 'leave', 'hover', 'leave']); + await ctx.page.evaluate(`window.disposable.dispose()`); + }); + + test('should work fine when hover and leave callbacks are not provided', async () => { + await openTerminal(ctx); + // Focus the terminal as the cursor will show and trigger a rerender, which can clear the + // active link + await ctx.page.evaluate('window.term.focus()'); + await ctx.proxy.write('foo bar baz'); + // Wait for renderer to catch up as links are cleared on render + await pollFor(ctx.page, `document.querySelector('.xterm-rows').textContent`, 'foo bar baz '); + await ctx.page.evaluate(` + window.calls = []; + window.disposable = window.term.registerLinkProvider({ + provideLinks: (position, cb) => { + window.calls.push('provide ' + position); + if (position === 1) { + window.calls.push('match 1'); + cb([{ + range: { start: { x: 5, y: 1 }, end: { x: 7, y: 1 } }, + text: 'bar', + activate: () => window.calls.push('activate') + }]); + } else if (position === 2) { + window.calls.push('match 2'); + cb([{ + range: { start: { x: 5, y: 2 }, end: { x: 7, y: 2 } }, + text: 'bar', + activate: () => window.calls.push('activate') + }]); + } + } + }); + `); + const dims = await getDimensions(); + await moveMouseCell(dims, 5, 1); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'match 1']); + await moveMouseCell(dims, 4, 2); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'match 1', 'provide 2', 'match 2']); + await moveMouseCell(dims, 7, 1); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'match 1', 'provide 2', 'match 2', 'provide 1', 'match 1']); + await moveMouseCell(dims, 6, 2); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'match 1', 'provide 2', 'match 2', 'provide 1', 'match 1', 'provide 2', 'match 2']); + await ctx.page.evaluate(`window.disposable.dispose()`); + }); + + test('should fire activate events when clicking the link', async () => { + await openTerminal(ctx); + // Focus the terminal as the cursor will show and trigger a rerender, which can clear the + // active link + await ctx.page.evaluate('window.term.focus()'); + await ctx.proxy.write('a b c'); + // Wait for renderer to catch up as links are cleared on render + await pollFor(ctx.page, `document.querySelector('.xterm-rows').textContent`, 'a b c '); + await ctx.page.evaluate(` + window.calls = []; + window.disposable = window.term.registerLinkProvider({ + provideLinks: (y, cb) => { + window.calls.push('provide ' + y); + cb([{ + range: { start: { x: 1, y }, end: { x: 80, y } }, + text: window.term.buffer.active.getLine(y - 1).translateToString(), + activate: (_, text) => window.calls.push('activate ' + y), + hover: () => window.calls.push('hover ' + y), + leave: () => window.calls.push('leave ' + y) + }]); + } + }); + `); + const dims = await getDimensions(); + await moveMouseCell(dims, 3, 1); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'hover 1']); + await ctx.page.mouse.down(); + await ctx.page.mouse.up(); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'hover 1', 'activate 1']); + await moveMouseCell(dims, 1, 2); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'hover 1', 'activate 1', 'leave 1', 'provide 2', 'hover 2']); + await ctx.page.mouse.down(); + await ctx.page.mouse.up(); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'hover 1', 'activate 1', 'leave 1', 'provide 2', 'hover 2', 'activate 2']); + await moveMouseCell(dims, 5, 1); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'hover 1', 'activate 1', 'leave 1', 'provide 2', 'hover 2', 'activate 2', 'leave 2', 'provide 1', 'hover 1']); + await ctx.page.mouse.down(); + await ctx.page.mouse.up(); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'hover 1', 'activate 1', 'leave 1', 'provide 2', 'hover 2', 'activate 2', 'leave 2', 'provide 1', 'hover 1', 'activate 1']); + await ctx.page.evaluate(`window.disposable.dispose()`); + }); + + test('should work when multiple links are provided on the same line', async () => { + await openTerminal(ctx); + // Focus the terminal as the cursor will show and trigger a rerender, which can clear the + // active link + await ctx.page.evaluate('window.term.focus()'); + await ctx.proxy.write('foo bar baz'); + // Wait for renderer to catch up as links are cleared on render + await pollFor(ctx.page, `document.querySelector('.xterm-rows').textContent`, 'foo bar baz '); + await ctx.page.evaluate(` + window.calls = []; + window.disposable = window.term.registerLinkProvider({ + provideLinks: (position, cb) => { + window.calls.push('provide ' + position); + if (position === 1) { + cb([{ + range: { start: { x: 1, y: 1 }, end: { x: 3, y: 1 } }, + text: '', + activate: () => window.calls.push('activate'), + hover: () => window.calls.push('hover 1-3'), + leave: () => window.calls.push('leave 1-3') + }, { + range: { start: { x: 5, y: 1 }, end: { x: 7, y: 1 } }, + text: '', + activate: () => window.calls.push('activate'), + hover: () => window.calls.push('hover 5-7'), + leave: () => window.calls.push('leave 5-7') + }, { + range: { start: { x: 9, y: 1 }, end: { x: 11, y: 1 } }, + text: '', + activate: () => window.calls.push('activate'), + hover: () => window.calls.push('hover 9-11'), + leave: () => window.calls.push('leave 9-11') + }]); + } + } + }); + `); + const dims = await getDimensions(); + await moveMouseCell(dims, 2, 1); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'hover 1-3']); + await moveMouseCell(dims, 6, 1); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'hover 1-3', 'leave 1-3', 'hover 5-7']); + await moveMouseCell(dims, 6, 2); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'hover 1-3', 'leave 1-3', 'hover 5-7', 'leave 5-7', 'provide 2']); + await moveMouseCell(dims, 10, 1); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'hover 1-3', 'leave 1-3', 'hover 5-7', 'leave 5-7', 'provide 2', 'provide 1', 'hover 9-11']); + await ctx.page.evaluate(`window.disposable.dispose()`); + }); + + test('should dispose links when hovering away', async () => { + await openTerminal(ctx); + // Focus the terminal as the cursor will show and trigger a rerender, which can clear the + // active link + await ctx.page.evaluate('window.term.focus()'); + await ctx.proxy.write('foo bar baz'); + // Wait for renderer to catch up as links are cleared on render + await pollFor(ctx.page, `document.querySelector('.xterm-rows').textContent`, 'foo bar baz '); + await ctx.page.evaluate(` + window.calls = []; + window.disposable = window.term.registerLinkProvider({ + provideLinks: (position, cb) => { + window.calls.push('provide ' + position); + if (position === 1) { + cb([{ + range: { start: { x: 1, y: 1 }, end: { x: 3, y: 1 } }, + text: '', + activate: () => window.calls.push('activate'), + dispose: () => window.calls.push('dispose 1-3'), + hover: () => window.calls.push('hover 1-3'), + leave: () => window.calls.push('leave 1-3') + }, { + range: { start: { x: 5, y: 1 }, end: { x: 7, y: 1 } }, + text: '', + activate: () => window.calls.push('activate'), + dispose: () => window.calls.push('dispose 5-7'), + hover: () => window.calls.push('hover 5-7'), + leave: () => window.calls.push('leave 5-7') + }, { + range: { start: { x: 9, y: 1 }, end: { x: 11, y: 1 } }, + text: '', + activate: () => window.calls.push('activate'), + dispose: () => window.calls.push('dispose 9-11'), + hover: () => window.calls.push('hover 9-11'), + leave: () => window.calls.push('leave 9-11') + }]); + } + } + }); + `); + const dims = await getDimensions(); + await moveMouseCell(dims, 2, 1); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'hover 1-3']); + await moveMouseCell(dims, 6, 1); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'hover 1-3', 'leave 1-3', 'hover 5-7']); + await moveMouseCell(dims, 6, 2); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'hover 1-3', 'leave 1-3', 'hover 5-7', 'leave 5-7', 'dispose 1-3', 'dispose 5-7', 'dispose 9-11', 'provide 2']); + await moveMouseCell(dims, 10, 1); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'hover 1-3', 'leave 1-3', 'hover 5-7', 'leave 5-7', 'dispose 1-3', 'dispose 5-7', 'dispose 9-11', 'provide 2', 'provide 1', 'hover 9-11']); + await moveMouseCell(dims, 10, 2); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'hover 1-3', 'leave 1-3', 'hover 5-7', 'leave 5-7', 'dispose 1-3', 'dispose 5-7', 'dispose 9-11', 'provide 2', 'provide 1', 'hover 9-11', 'leave 9-11', 'dispose 1-3', 'dispose 5-7', 'dispose 9-11', 'provide 2']); + await ctx.page.evaluate(`window.disposable.dispose()`); + }); + }); +}); + +interface IDimensions { + top: number; + left: number; + renderDimensions: IRenderDimensions; +} + +async function getDimensions(): Promise { + return await ctx.page.evaluate(` + (function() { + const rect = document.querySelector('.xterm-rows').getBoundingClientRect(); + return { + top: rect.top, + left: rect.left, + renderDimensions: window.term._core._renderService.dimensions + }; + })(); + `); +} + +async function getCellCoordinates(dimensions: IDimensions, col: number, row: number): Promise<{ x: number, y: number }> { + return { + x: dimensions.left + dimensions.renderDimensions.device.cell.width * (col - 0.5), + y: dimensions.top + dimensions.renderDimensions.device.cell.height * (row - 0.5) + }; +} + +async function moveMouseCell(dimensions: IDimensions, col: number, row: number): Promise { + const coords = await getCellCoordinates(dimensions, col, row); + await ctx.page.mouse.move(coords.x, coords.y); +} diff --git a/test/playwright/TestUtils.ts b/test/playwright/TestUtils.ts index 5111612e..0ab95b76 100644 --- a/test/playwright/TestUtils.ts +++ b/test/playwright/TestUtils.ts @@ -4,13 +4,13 @@ */ import { Browser, JSHandle, Page } from '@playwright/test'; -import { deepStrictEqual } from 'assert'; +import { deepStrictEqual, strictEqual } 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, IBufferNamespace, IBufferRange, IDecoration, IDecorationOptions, IModes, ITerminalOptions, Terminal } from 'xterm'; +import { IBuffer, IBufferCell, IBufferLine, IBufferNamespace, IBufferRange, IDecoration, IDecorationOptions, IModes, ITerminalInitOnlyOptions, ITerminalOptions, Terminal } from 'xterm'; import { EventEmitter } from '../../out/common/EventEmitter'; // TODO: We could avoid needing this import deepEqual = require('deep-equal'); @@ -353,12 +353,19 @@ class TerminalCoreProxy { } } -export async function openTerminal(ctx: ITestContext, options: ITerminalOptions = {}): Promise { +export async function openTerminal(ctx: ITestContext, options: ITerminalOptions | ITerminalInitOnlyOptions = {}): Promise { await ctx.page.evaluate(` - if ('term' in window) { + if ('term' in window) { + try { window.term.dispose(); - } - window.term = new Terminal(${JSON.stringify(options)}); + } catch {} + } + `); + // HACK: Tests may have side effects that could cause the terminal not to be removed. This + // assertion catches this case early. + strictEqual(await ctx.page.evaluate(`document.querySelector('#terminal-container').children.length`), 0, 'there must be no terminals on the page'); + await ctx.page.evaluate(` + window.term = new Terminal(${JSON.stringify({ allowProposedApi: true, ...options })}); window.term.open(document.querySelector('#terminal-container')); `); await ctx.page.waitForSelector('.xterm-rows'); From 4cd459708221f3ed76a99e3f28366fef02a95348 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 2 Sep 2023 08:19:59 -0700 Subject: [PATCH 27/40] Browser-specific playwright job --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bbbb413f..37210bba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -221,6 +221,7 @@ jobs: matrix: node-version: [18] # just one as integration tests are about testing in browser runs-on: [ubuntu] # macos is flaky + browser: [chromium, firefox] runs-on: ${{ matrix.runs-on }}-latest steps: - uses: actions/checkout@v3 @@ -234,7 +235,7 @@ jobs: yarn --frozen-lockfile yarn install-addons - name: Install playwright - run: npx playwright install --with-deps + run: npx playwright install --with-deps ${{ matrix.browser }} - name: Wait for build job uses: NathanFirmo/wait-for-other-job@v1.1.1 with: @@ -254,7 +255,7 @@ jobs: - name: Build demo run: yarn build-demo - name: Integration tests - run: yarn test-playwright --forbid-only + run: yarn test-playwright-${{ matrix.browser }} --forbid-only test-api: needs: build From 8d0fde2d7ff393f959d98e6943d0db7df020dcf1 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 2 Sep 2023 08:22:56 -0700 Subject: [PATCH 28/40] Remove only --- test/playwright/Terminal.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/playwright/Terminal.test.ts b/test/playwright/Terminal.test.ts index 6fe2f803..9bb0148f 100644 --- a/test/playwright/Terminal.test.ts +++ b/test/playwright/Terminal.test.ts @@ -15,7 +15,7 @@ test.beforeAll(async ({ browser }) => { test.afterAll(async () => await ctx.page.close()); -test.describe.only('API Integration Tests', () => { +test.describe('API Integration Tests', () => { test('Default options', async () => { strictEqual(await ctx.proxy.cols, 80); strictEqual(await ctx.proxy.rows, 24); From 158f7a17a6b4e3d51e7ed5513edda9f80007a7a4 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 2 Sep 2023 08:26:42 -0700 Subject: [PATCH 29/40] Use list reporter --- test/playwright/playwright.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/playwright/playwright.config.ts b/test/playwright/playwright.config.ts index 57b2f1e2..3d1fe4fb 100644 --- a/test/playwright/playwright.config.ts +++ b/test/playwright/playwright.config.ts @@ -24,6 +24,7 @@ const config: PlaywrightTestConfig = { } } ], + reporter: 'list', webServer: { command: 'npm start', port: 3000, From 31bbeca2d868ff9d68e958b56caefa92278f59f6 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 2 Sep 2023 08:26:46 -0700 Subject: [PATCH 30/40] Add webkit --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37210bba..1bfe456b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -221,7 +221,7 @@ jobs: matrix: node-version: [18] # just one as integration tests are about testing in browser runs-on: [ubuntu] # macos is flaky - browser: [chromium, firefox] + browser: [chromium, firefox, webkit] runs-on: ${{ matrix.runs-on }}-latest steps: - uses: actions/checkout@v3 From 7c434a0566a4d89f2f1ce148e7a528f96f90db51 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 2 Sep 2023 08:30:14 -0700 Subject: [PATCH 31/40] Merge unzip steps --- .github/workflows/ci.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1bfe456b..bded412c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,12 +90,14 @@ jobs: - uses: actions/download-artifact@v3 with: name: build-artifacts - - name: Unzip artifacts (Linux, macOS) + - name: Unzip artifacts if: runner.os != 'Windows' - run: unzip -o compressed-build.zip - - name: Unzip artifacts (Windows) - if: runner.os == 'Windows' - run: 7z x compressed-build.zip -aoa -o${{ github.workspace }} + run: + if [ "$RUNNER_OS" == "Windows" ]; then + 7z x compressed-build.zip -aoa -o${{ github.workspace }} + else + unzip -o compressed-build.zip + fi - name: Print directory structure run: ls -R - name: Unit test coverage From 2269e92c88e36364beacf1caf1fbbba9b576fe0e Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 2 Sep 2023 08:30:51 -0700 Subject: [PATCH 32/40] Fix syntax --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bded412c..b5eda4dc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,7 +92,7 @@ jobs: name: build-artifacts - name: Unzip artifacts if: runner.os != 'Windows' - run: + run: | if [ "$RUNNER_OS" == "Windows" ]; then 7z x compressed-build.zip -aoa -o${{ github.workspace }} else From 2b5ebb2b7bb804cd401901bb86ab9920d5e300ca Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 2 Sep 2023 08:33:01 -0700 Subject: [PATCH 33/40] Use unified unzip on unit tests --- .github/workflows/ci.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5eda4dc..2c46b9b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -133,12 +133,14 @@ jobs: - uses: actions/download-artifact@v3 with: name: build-artifacts - - name: Unzip artifacts (Linux, macOS) + - name: Unzip artifacts if: runner.os != 'Windows' - run: unzip -o compressed-build.zip - - name: Unzip artifacts (Windows) - if: runner.os == 'Windows' - run: 7z x compressed-build.zip -aoa -o${{ github.workspace }} + run: | + if [ "$RUNNER_OS" == "Windows" ]; then + 7z x compressed-build.zip -aoa -o${{ github.workspace }} + else + unzip -o compressed-build.zip + fi - name: Print directory structure run: ls -R - name: Unit tests From 525d498e9f8b20beff66062e170bc654c3c11e79 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 2 Sep 2023 08:40:57 -0700 Subject: [PATCH 34/40] Fix unzip on Windows --- .github/workflows/ci.yml | 54 +++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c46b9b5..72af5e05 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -91,7 +91,6 @@ jobs: with: name: build-artifacts - name: Unzip artifacts - if: runner.os != 'Windows' run: | if [ "$RUNNER_OS" == "Windows" ]; then 7z x compressed-build.zip -aoa -o${{ github.workspace }} @@ -134,7 +133,6 @@ jobs: with: name: build-artifacts - name: Unzip artifacts - if: runner.os != 'Windows' run: | if [ "$RUNNER_OS" == "Windows" ]; then 7z x compressed-build.zip -aoa -o${{ github.workspace }} @@ -168,12 +166,13 @@ jobs: - uses: actions/download-artifact@v3 with: name: build-artifacts - - name: Unzip artifacts (Linux, macOS) - if: runner.os != 'Windows' - run: unzip -o compressed-build.zip - - name: Unzip artifacts (Windows) - if: runner.os == 'Windows' - run: 7z x compressed-build.zip -aoa -o${{ github.workspace }} + - name: Unzip artifacts + run: | + if [ "$RUNNER_OS" == "Windows" ]; then + 7z x compressed-build.zip -aoa -o${{ github.workspace }} + else + unzip -o compressed-build.zip + fi - name: Print directory structure run: ls -R - name: Unit tests @@ -208,12 +207,13 @@ jobs: - uses: actions/download-artifact@v3 with: name: build-artifacts - - name: Unzip artifacts (Linux, macOS) - if: runner.os != 'Windows' - run: unzip -o compressed-build.zip - - name: Unzip artifacts (Windows) - if: runner.os == 'Windows' - run: 7z x compressed-build.zip -aoa -o${{ github.workspace }} + - name: Unzip artifacts + run: | + if [ "$RUNNER_OS" == "Windows" ]; then + 7z x compressed-build.zip -aoa -o${{ github.workspace }} + else + unzip -o compressed-build.zip + fi - name: Print directory structure run: ls -R - name: Integration tests (${{ matrix.browser }}) @@ -248,12 +248,13 @@ jobs: - uses: actions/download-artifact@v3 with: name: build-artifacts - - name: Unzip artifacts (Linux, macOS) - if: runner.os != 'Windows' - run: unzip -o compressed-build.zip - - name: Unzip artifacts (Windows) - if: runner.os == 'Windows' - run: 7z x compressed-build.zip -aoa -o${{ github.workspace }} + - name: Unzip artifacts + run: | + if [ "$RUNNER_OS" == "Windows" ]; then + 7z x compressed-build.zip -aoa -o${{ github.workspace }} + else + unzip -o compressed-build.zip + fi - name: Print directory structure run: ls -R - name: Build demo @@ -286,12 +287,13 @@ jobs: - uses: actions/download-artifact@v3 with: name: build-artifacts - - name: Unzip artifacts (Linux, macOS) - if: runner.os != 'Windows' - run: unzip -o compressed-build.zip - - name: Unzip artifacts (Windows) - if: runner.os == 'Windows' - run: 7z x compressed-build.zip -aoa -o${{ github.workspace }} + - name: Unzip artifacts + run: | + if [ "$RUNNER_OS" == "Windows" ]; then + 7z x compressed-build.zip -aoa -o${{ github.workspace }} + else + unzip -o compressed-build.zip + fi - name: Print directory structure run: ls -R - name: Integration tests (${{ matrix.browser }}) From 5fc1c04bd6f79170f35c6739e2f4aad9443eb8ae Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 2 Sep 2023 08:41:59 -0700 Subject: [PATCH 35/40] Use bash on all OS' --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72af5e05..814e2d84 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -91,6 +91,7 @@ jobs: with: name: build-artifacts - name: Unzip artifacts + shell: bash run: | if [ "$RUNNER_OS" == "Windows" ]; then 7z x compressed-build.zip -aoa -o${{ github.workspace }} @@ -133,6 +134,7 @@ jobs: with: name: build-artifacts - name: Unzip artifacts + shell: bash run: | if [ "$RUNNER_OS" == "Windows" ]; then 7z x compressed-build.zip -aoa -o${{ github.workspace }} @@ -167,6 +169,7 @@ jobs: with: name: build-artifacts - name: Unzip artifacts + shell: bash run: | if [ "$RUNNER_OS" == "Windows" ]; then 7z x compressed-build.zip -aoa -o${{ github.workspace }} @@ -208,6 +211,7 @@ jobs: with: name: build-artifacts - name: Unzip artifacts + shell: bash run: | if [ "$RUNNER_OS" == "Windows" ]; then 7z x compressed-build.zip -aoa -o${{ github.workspace }} @@ -249,6 +253,7 @@ jobs: with: name: build-artifacts - name: Unzip artifacts + shell: bash run: | if [ "$RUNNER_OS" == "Windows" ]; then 7z x compressed-build.zip -aoa -o${{ github.workspace }} @@ -288,6 +293,7 @@ jobs: with: name: build-artifacts - name: Unzip artifacts + shell: bash run: | if [ "$RUNNER_OS" == "Windows" ]; then 7z x compressed-build.zip -aoa -o${{ github.workspace }} From 076ab0f49ba2cb90d4aee870449b9d663800302a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 2 Sep 2023 08:44:32 -0700 Subject: [PATCH 36/40] Combine unzip and ls -R --- .github/workflows/ci.yml | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 814e2d84..0edc9bf4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -98,8 +98,7 @@ jobs: else unzip -o compressed-build.zip fi - - name: Print directory structure - run: ls -R + ls -R - name: Unit test coverage run: | yarn test-unit-coverage --forbid-only @@ -141,8 +140,7 @@ jobs: else unzip -o compressed-build.zip fi - - name: Print directory structure - run: ls -R + ls -R - name: Unit tests run: yarn test-unit --forbid-only @@ -176,8 +174,7 @@ jobs: else unzip -o compressed-build.zip fi - - name: Print directory structure - run: ls -R + ls -R - name: Unit tests run: yarn test-unit --forbid-only @@ -218,8 +215,7 @@ jobs: else unzip -o compressed-build.zip fi - - name: Print directory structure - run: ls -R + ls -R - name: Integration tests (${{ matrix.browser }}) run: yarn test-api-${{ matrix.browser }} --headless --forbid-only @@ -260,8 +256,7 @@ jobs: else unzip -o compressed-build.zip fi - - name: Print directory structure - run: ls -R + ls -R - name: Build demo run: yarn build-demo - name: Integration tests @@ -300,7 +295,6 @@ jobs: else unzip -o compressed-build.zip fi - - name: Print directory structure - run: ls -R + ls -R - name: Integration tests (${{ matrix.browser }}) run: yarn test-api-${{ matrix.browser }} --headless --forbid-only From 7c7ac866818cb8acf50884387e6729c4cdf85d66 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 2 Sep 2023 08:53:20 -0700 Subject: [PATCH 37/40] Run inner pwsh to unzip on Winodws --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0edc9bf4..21c11fc4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,7 +94,7 @@ jobs: shell: bash run: | if [ "$RUNNER_OS" == "Windows" ]; then - 7z x compressed-build.zip -aoa -o${{ github.workspace }} + pwsh -Command "7z x compressed-build.zip -aoa -o${{ github.workspace }}" else unzip -o compressed-build.zip fi @@ -136,7 +136,7 @@ jobs: shell: bash run: | if [ "$RUNNER_OS" == "Windows" ]; then - 7z x compressed-build.zip -aoa -o${{ github.workspace }} + pwsh -Command "7z x compressed-build.zip -aoa -o${{ github.workspace }}" else unzip -o compressed-build.zip fi @@ -170,7 +170,7 @@ jobs: shell: bash run: | if [ "$RUNNER_OS" == "Windows" ]; then - 7z x compressed-build.zip -aoa -o${{ github.workspace }} + pwsh -Command "7z x compressed-build.zip -aoa -o${{ github.workspace }}" else unzip -o compressed-build.zip fi @@ -211,7 +211,7 @@ jobs: shell: bash run: | if [ "$RUNNER_OS" == "Windows" ]; then - 7z x compressed-build.zip -aoa -o${{ github.workspace }} + pwsh -Command "7z x compressed-build.zip -aoa -o${{ github.workspace }}" else unzip -o compressed-build.zip fi @@ -252,7 +252,7 @@ jobs: shell: bash run: | if [ "$RUNNER_OS" == "Windows" ]; then - 7z x compressed-build.zip -aoa -o${{ github.workspace }} + pwsh -Command "7z x compressed-build.zip -aoa -o${{ github.workspace }}" else unzip -o compressed-build.zip fi @@ -291,7 +291,7 @@ jobs: shell: bash run: | if [ "$RUNNER_OS" == "Windows" ]; then - 7z x compressed-build.zip -aoa -o${{ github.workspace }} + pwsh -Command "7z x compressed-build.zip -aoa -o${{ github.workspace }}" else unzip -o compressed-build.zip fi From 93aae670baa22f351b2a9385a46061f51eb1ff0c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 2 Sep 2023 08:56:59 -0700 Subject: [PATCH 38/40] Readme explaining test/api --- test/api/README.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 test/api/README.md diff --git a/test/api/README.md b/test/api/README.md new file mode 100644 index 00000000..1c86815c --- /dev/null +++ b/test/api/README.md @@ -0,0 +1 @@ +This project contains helpers for the legacy playwright tests. Currently addons use this as a dependency, when all addons have moved over to use `@playright/test` this can be removed. From 5e628d7fc60d0a1a1ea9edd27f3a12f92bd9bea9 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 2 Sep 2023 09:11:15 -0700 Subject: [PATCH 39/40] Use arrow functions --- test/playwright/CharWidth.test.ts | 2 +- test/playwright/InputHandler.test.ts | 2 +- test/playwright/MouseTracking.test.ts | 4 ++-- test/playwright/Parser.test.ts | 2 +- test/playwright/Terminal.test.ts | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/playwright/CharWidth.test.ts b/test/playwright/CharWidth.test.ts index fbc06697..5325f4ef 100644 --- a/test/playwright/CharWidth.test.ts +++ b/test/playwright/CharWidth.test.ts @@ -15,7 +15,7 @@ test.afterAll(async () => await ctx.page.close()); test.beforeEach(async () => await ctx.proxy.reset()); -test.describe('CharWidth Integration Tests', function(): void { +test.describe('CharWidth Integration Tests', () => { test.describe('getStringCellWidth', () => { test('ASCII chars', async () => { await ctx.proxy.write('This is just ASCII text.#'); diff --git a/test/playwright/InputHandler.test.ts b/test/playwright/InputHandler.test.ts index 42b2916a..e422bd1c 100644 --- a/test/playwright/InputHandler.test.ts +++ b/test/playwright/InputHandler.test.ts @@ -15,7 +15,7 @@ test.beforeAll(async ({ browser }) => { test.afterAll(async () => await ctx.page.close()); -test.describe('InputHandler Integration Tests', function (): void { +test.describe('InputHandler Integration Tests', () => { test.describe('CSI', () => { test.beforeEach(async () => await ctx.proxy.reset()); diff --git a/test/playwright/MouseTracking.test.ts b/test/playwright/MouseTracking.test.ts index ba96a4e6..dbce55cd 100644 --- a/test/playwright/MouseTracking.test.ts +++ b/test/playwright/MouseTracking.test.ts @@ -164,8 +164,8 @@ function parseReport(encoding: string, msg: number[]): { state: any, row: number } } -test.describe('Mouse Tracking Tests', function (): void { - test.beforeAll(async function(): Promise { +test.describe('Mouse Tracking Tests', () => { + test.beforeAll(async () => { await ctx.page.setViewportSize({ width, height }); // patch terminal to get the onData calls // we encode the msg here to an array of codes to not lose bytes diff --git a/test/playwright/Parser.test.ts b/test/playwright/Parser.test.ts index 02e0c4b4..42406270 100644 --- a/test/playwright/Parser.test.ts +++ b/test/playwright/Parser.test.ts @@ -26,7 +26,7 @@ declare global { } } -test.describe('Parser Integration Tests', function (): void { +test.describe('Parser Integration Tests', () => { test.beforeEach(async () => await ctx.proxy.reset()); test.afterEach(async () => { await ctx.page.evaluate(() => { diff --git a/test/playwright/Terminal.test.ts b/test/playwright/Terminal.test.ts index 9bb0148f..3a4e00f0 100644 --- a/test/playwright/Terminal.test.ts +++ b/test/playwright/Terminal.test.ts @@ -378,7 +378,7 @@ test.describe('API Integration Tests', () => { await pollFor(ctx.page, `window.callCount`, 2); }); - test('onRender', async function(): Promise { + test('onRender', async () => { await openTerminal(ctx); await timeout(20); // Ensure all init events are fired await ctx.page.evaluate(` From 68c572345b1a0391ef4f31a9b6df33e8903334f9 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 2 Sep 2023 09:19:17 -0700 Subject: [PATCH 40/40] Remove debugger statement --- src/browser/Viewport.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index 1611f54b..c6e8732f 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -118,7 +118,6 @@ export class Viewport extends Disposable implements IViewport { } private _innerRefresh(): void { - debugger; if (this._charSizeService.height > 0) { this._currentRowHeight = this._renderService.dimensions.device.cell.height / this._coreBrowserService.dpr; this._currentDeviceCellHeight = this._renderService.dimensions.device.cell.height;