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 01/99] 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 02/99] 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 03/99] 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 04/99] 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 05/99] 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 06/99] 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 07/99] 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 08/99] 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 09/99] 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 10/99] 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 11/99] 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 12/99] 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 13/99] 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 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 14/99] 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 ceef536b7919cd6761777baa8c66ef03c64e6a53 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 1 Sep 2023 07:36:48 -0700 Subject: [PATCH 15/99] Upgrade es6 -> es2021 This uses the recommended target for node 16 in tsconfig/bases. Fixes #4706 --- .eslintrc.json | 2 +- .eslintrc.json.typings | 2 +- addons/xterm-addon-attach/src/tsconfig.json | 2 +- addons/xterm-addon-attach/test/tsconfig.json | 2 +- addons/xterm-addon-canvas/src/tsconfig.json | 4 ++-- addons/xterm-addon-fit/src/tsconfig.json | 2 +- addons/xterm-addon-fit/test/tsconfig.json | 2 +- addons/xterm-addon-search/src/tsconfig.json | 4 ++-- addons/xterm-addon-search/test/tsconfig.json | 4 ++-- addons/xterm-addon-serialize/benchmark/tsconfig.json | 7 +++++-- addons/xterm-addon-serialize/src/tsconfig.json | 2 +- addons/xterm-addon-serialize/test/tsconfig.json | 2 +- addons/xterm-addon-unicode11/src/tsconfig.json | 2 +- addons/xterm-addon-unicode11/test/tsconfig.json | 2 +- addons/xterm-addon-web-links/src/tsconfig.json | 2 +- addons/xterm-addon-web-links/test/tsconfig.json | 2 +- addons/xterm-addon-webgl/src/tsconfig.json | 4 ++-- addons/xterm-addon-webgl/test/tsconfig.json | 4 ++-- demo/tsconfig.json | 2 +- src/browser/tsconfig.json | 3 +-- src/tsconfig-base.json | 2 +- test/api/tsconfig.json | 2 +- test/benchmark/tsconfig.json | 4 ++-- 23 files changed, 33 insertions(+), 31 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 9287da5c..015776d3 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,7 +1,7 @@ { "env": { "browser": true, - "es6": true, + "es2021": true, "node": true }, "parser": "@typescript-eslint/parser", diff --git a/.eslintrc.json.typings b/.eslintrc.json.typings index 5af028d3..0d234ee7 100644 --- a/.eslintrc.json.typings +++ b/.eslintrc.json.typings @@ -1,7 +1,7 @@ { "env": { "browser": true, - "es6": true, + "es2021": true, "node": true }, "parser": "@typescript-eslint/parser", diff --git a/addons/xterm-addon-attach/src/tsconfig.json b/addons/xterm-addon-attach/src/tsconfig.json index f3e409d1..7e68574d 100644 --- a/addons/xterm-addon-attach/src/tsconfig.json +++ b/addons/xterm-addon-attach/src/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es2015", + "target": "es2021", "lib": [ "dom", "es2015" diff --git a/addons/xterm-addon-attach/test/tsconfig.json b/addons/xterm-addon-attach/test/tsconfig.json index fa67b169..67ad42b7 100644 --- a/addons/xterm-addon-attach/test/tsconfig.json +++ b/addons/xterm-addon-attach/test/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es2015", + "target": "es2021", "lib": [ "es2015" ], diff --git a/addons/xterm-addon-canvas/src/tsconfig.json b/addons/xterm-addon-canvas/src/tsconfig.json index f752dc5b..f6fb08a7 100644 --- a/addons/xterm-addon-canvas/src/tsconfig.json +++ b/addons/xterm-addon-canvas/src/tsconfig.json @@ -1,10 +1,10 @@ { "compilerOptions": { "module": "commonjs", - "target": "es2015", + "target": "es2021", "lib": [ "dom", - "es6" + "es2021" ], "rootDir": ".", "outDir": "../out", diff --git a/addons/xterm-addon-fit/src/tsconfig.json b/addons/xterm-addon-fit/src/tsconfig.json index 3bfbea67..668f910b 100644 --- a/addons/xterm-addon-fit/src/tsconfig.json +++ b/addons/xterm-addon-fit/src/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es2015", + "target": "es2021", "lib": [ "dom", "es2015" diff --git a/addons/xterm-addon-fit/test/tsconfig.json b/addons/xterm-addon-fit/test/tsconfig.json index fa67b169..67ad42b7 100644 --- a/addons/xterm-addon-fit/test/tsconfig.json +++ b/addons/xterm-addon-fit/test/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es2015", + "target": "es2021", "lib": [ "es2015" ], diff --git a/addons/xterm-addon-search/src/tsconfig.json b/addons/xterm-addon-search/src/tsconfig.json index 9ef27b5c..1f08b12d 100644 --- a/addons/xterm-addon-search/src/tsconfig.json +++ b/addons/xterm-addon-search/src/tsconfig.json @@ -1,10 +1,10 @@ { "compilerOptions": { "module": "commonjs", - "target": "es2015", + "target": "es2021", "lib": [ "dom", - "es6", + "es2021", ], "rootDir": ".", "outDir": "../out", diff --git a/addons/xterm-addon-search/test/tsconfig.json b/addons/xterm-addon-search/test/tsconfig.json index 052fac43..ee45e725 100644 --- a/addons/xterm-addon-search/test/tsconfig.json +++ b/addons/xterm-addon-search/test/tsconfig.json @@ -1,9 +1,9 @@ { "compilerOptions": { "module": "commonjs", - "target": "es2015", + "target": "es2021", "lib": [ - "es6", + "es2021", ], "rootDir": ".", "outDir": "../out-test", diff --git a/addons/xterm-addon-serialize/benchmark/tsconfig.json b/addons/xterm-addon-serialize/benchmark/tsconfig.json index bf5e335c..6bbb2a4b 100644 --- a/addons/xterm-addon-serialize/benchmark/tsconfig.json +++ b/addons/xterm-addon-serialize/benchmark/tsconfig.json @@ -1,11 +1,14 @@ { "compilerOptions": { - "lib": ["dom", "es6"], + "lib": [ + "dom", + "es2021" + ], "outDir": "../out-benchmark", "types": ["../../../node_modules/@types/node"], "moduleResolution": "node", "strict": false, - "target": "es2015", + "target": "es2021", "module": "commonjs", "baseUrl": ".", "paths": { diff --git a/addons/xterm-addon-serialize/src/tsconfig.json b/addons/xterm-addon-serialize/src/tsconfig.json index ba26f22e..22ffe53e 100644 --- a/addons/xterm-addon-serialize/src/tsconfig.json +++ b/addons/xterm-addon-serialize/src/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es2015", + "target": "es2021", "lib": [ "dom", "es2015" diff --git a/addons/xterm-addon-serialize/test/tsconfig.json b/addons/xterm-addon-serialize/test/tsconfig.json index 971f3e92..6eeb5cde 100644 --- a/addons/xterm-addon-serialize/test/tsconfig.json +++ b/addons/xterm-addon-serialize/test/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es2015", + "target": "es2021", "lib": [ "es2015" ], diff --git a/addons/xterm-addon-unicode11/src/tsconfig.json b/addons/xterm-addon-unicode11/src/tsconfig.json index f5489fcc..e2c1464e 100644 --- a/addons/xterm-addon-unicode11/src/tsconfig.json +++ b/addons/xterm-addon-unicode11/src/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es2015", + "target": "es2021", "lib": [ "dom", "es2015" diff --git a/addons/xterm-addon-unicode11/test/tsconfig.json b/addons/xterm-addon-unicode11/test/tsconfig.json index 4b3cb31c..0406d3a4 100644 --- a/addons/xterm-addon-unicode11/test/tsconfig.json +++ b/addons/xterm-addon-unicode11/test/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es2015", + "target": "es2021", "lib": [ "dom", "es2015" diff --git a/addons/xterm-addon-web-links/src/tsconfig.json b/addons/xterm-addon-web-links/src/tsconfig.json index f3e409d1..7e68574d 100644 --- a/addons/xterm-addon-web-links/src/tsconfig.json +++ b/addons/xterm-addon-web-links/src/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es2015", + "target": "es2021", "lib": [ "dom", "es2015" diff --git a/addons/xterm-addon-web-links/test/tsconfig.json b/addons/xterm-addon-web-links/test/tsconfig.json index 48975764..2b4ec58f 100644 --- a/addons/xterm-addon-web-links/test/tsconfig.json +++ b/addons/xterm-addon-web-links/test/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es2015", + "target": "es2021", "lib": [ "es2015" ], diff --git a/addons/xterm-addon-webgl/src/tsconfig.json b/addons/xterm-addon-webgl/src/tsconfig.json index 74989973..4d4194db 100644 --- a/addons/xterm-addon-webgl/src/tsconfig.json +++ b/addons/xterm-addon-webgl/src/tsconfig.json @@ -1,10 +1,10 @@ { "compilerOptions": { "module": "commonjs", - "target": "es2015", + "target": "es2021", "lib": [ "dom", - "es6" + "es2021" ], "rootDir": ".", "outDir": "../out", diff --git a/addons/xterm-addon-webgl/test/tsconfig.json b/addons/xterm-addon-webgl/test/tsconfig.json index 1ff9217e..6479cec9 100644 --- a/addons/xterm-addon-webgl/test/tsconfig.json +++ b/addons/xterm-addon-webgl/test/tsconfig.json @@ -1,9 +1,9 @@ { "compilerOptions": { "module": "commonjs", - "target": "es2015", + "target": "es2021", "lib": [ - "es6", + "es2021", ], "rootDir": ".", "outDir": "../out-test", diff --git a/demo/tsconfig.json b/demo/tsconfig.json index 018ddb42..f728f20e 100644 --- a/demo/tsconfig.json +++ b/demo/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es2015", + "target": "es2021", "rootDir": ".", "sourceMap": true, "baseUrl": ".", diff --git a/src/browser/tsconfig.json b/src/browser/tsconfig.json index 212aeab8..673a55a6 100644 --- a/src/browser/tsconfig.json +++ b/src/browser/tsconfig.json @@ -3,8 +3,7 @@ "compilerOptions": { "lib": [ "dom", - "es2015", - "es2016.Array.Include" + "es2021" ], "outDir": "../../out", "types": [ diff --git a/src/tsconfig-base.json b/src/tsconfig-base.json index cf4454a4..5ea04ea2 100644 --- a/src/tsconfig-base.json +++ b/src/tsconfig-base.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "target": "es2015", + "target": "es2021", "rootDir": ".", "sourceMap": true, diff --git a/test/api/tsconfig.json b/test/api/tsconfig.json index 60eb33cd..bc050db4 100644 --- a/test/api/tsconfig.json +++ b/test/api/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "lib": [ "dom", - "es6" + "es2021" ], "rootDir": ".", "outDir": "../../out-test/api", diff --git a/test/benchmark/tsconfig.json b/test/benchmark/tsconfig.json index 2ebe2ebe..8d1736df 100644 --- a/test/benchmark/tsconfig.json +++ b/test/benchmark/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "lib": [ "dom", - "es6", + "es2021", ], "outDir": "../../out-test/benchmark", "types": [ @@ -10,7 +10,7 @@ ], "moduleResolution": "node", "strict": false, - "target": "es2015", + "target": "es2021", "module": "commonjs", "baseUrl": ".", "paths": { 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 16/99] 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 17/99] 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 18/99] 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 19/99] 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 20/99] 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 21/99] 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 22/99] 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 23/99] 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 24/99] 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 25/99] 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 26/99] 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 27/99] 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 28/99] 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 29/99] 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 30/99] 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 31/99] 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 32/99] 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 33/99] 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 34/99] 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 35/99] 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 36/99] 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 37/99] 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 38/99] 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; From c03cb2b016b493df1cfb6865295a16c3dfea6306 Mon Sep 17 00:00:00 2001 From: tisilent Date: Thu, 7 Sep 2023 23:44:36 +0800 Subject: [PATCH 39/99] When DomRenderer focus,render the cursor line. --- demo/client.ts | 6 ++++++ demo/index.html | 4 ++++ src/browser/renderer/dom/DomRenderer.ts | 1 + 3 files changed, 11 insertions(+) diff --git a/demo/client.ts b/demo/client.ts index f9591ce4..bde5ca72 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -247,6 +247,7 @@ if (document.location.pathname === '/test') { document.getElementById('bce').addEventListener('click', coloredErase); addVtButtons(); initImageAddonExposed(); + testEvents(); } function createTerminal(): void { @@ -1335,3 +1336,8 @@ function initImageAddonExposed(): void { canvas?.toBlob(data => window.open(URL.createObjectURL(data), '_blank')); }); } + +function testEvents(): void { + document.getElementById('event-focus').addEventListener('click', ()=> term.focus()); + document.getElementById('event-blur').addEventListener('click', ()=> term.blur()); +} diff --git a/demo/index.html b/demo/index.html index a3e28200..6d9be706 100644 --- a/demo/index.html +++ b/demo/index.html @@ -109,6 +109,10 @@
+ +
Events Test
+
+
diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index 8126de6e..7413776c 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -303,6 +303,7 @@ export class DomRenderer extends Disposable implements IRenderer { public handleFocus(): void { this._rowContainer.classList.add(FOCUS_CLASS); + this.renderRows(this._bufferService.buffer.y, this._bufferService.buffer.y); } public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void { From fb4b662756657c9c1c1fd683dfcff968712bd75f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 7 Sep 2023 10:46:35 -0700 Subject: [PATCH 40/99] v5.3.0 and bump all addons --- addons/xterm-addon-attach/package.json | 2 +- addons/xterm-addon-canvas/package.json | 2 +- addons/xterm-addon-fit/package.json | 2 +- addons/xterm-addon-image/package.json | 2 +- addons/xterm-addon-ligatures/package.json | 2 +- addons/xterm-addon-search/package.json | 2 +- addons/xterm-addon-serialize/package.json | 2 +- addons/xterm-addon-unicode11/package.json | 2 +- addons/xterm-addon-web-links/package.json | 2 +- addons/xterm-addon-webgl/package.json | 2 +- package.json | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/addons/xterm-addon-attach/package.json b/addons/xterm-addon-attach/package.json index 222d9a41..f93cc612 100644 --- a/addons/xterm-addon-attach/package.json +++ b/addons/xterm-addon-attach/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-attach", - "version": "0.8.0", + "version": "0.9.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-canvas/package.json b/addons/xterm-addon-canvas/package.json index d747795a..701baafe 100644 --- a/addons/xterm-addon-canvas/package.json +++ b/addons/xterm-addon-canvas/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-canvas", - "version": "0.4.0", + "version": "0.5.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-fit/package.json b/addons/xterm-addon-fit/package.json index a066f66d..df9ea857 100644 --- a/addons/xterm-addon-fit/package.json +++ b/addons/xterm-addon-fit/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-fit", - "version": "0.7.0", + "version": "0.8.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-image/package.json b/addons/xterm-addon-image/package.json index 8de858b5..4669fea2 100644 --- a/addons/xterm-addon-image/package.json +++ b/addons/xterm-addon-image/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-image", - "version": "0.5.0", + "version": "0.6.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-ligatures/package.json b/addons/xterm-addon-ligatures/package.json index e62be196..1ca896ca 100644 --- a/addons/xterm-addon-ligatures/package.json +++ b/addons/xterm-addon-ligatures/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-ligatures", - "version": "0.6.0", + "version": "0.7.0", "description": "Add support for programming ligatures to xterm.js", "author": { "name": "The xterm.js authors", diff --git a/addons/xterm-addon-search/package.json b/addons/xterm-addon-search/package.json index 603a72d1..d2986140 100644 --- a/addons/xterm-addon-search/package.json +++ b/addons/xterm-addon-search/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-search", - "version": "0.12.0", + "version": "0.13.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-serialize/package.json b/addons/xterm-addon-serialize/package.json index ef0c0a9f..c8203229 100644 --- a/addons/xterm-addon-serialize/package.json +++ b/addons/xterm-addon-serialize/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-serialize", - "version": "0.10.0", + "version": "0.11.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-unicode11/package.json b/addons/xterm-addon-unicode11/package.json index d86d8f28..e1651961 100644 --- a/addons/xterm-addon-unicode11/package.json +++ b/addons/xterm-addon-unicode11/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-unicode11", - "version": "0.5.0", + "version": "0.6.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-web-links/package.json b/addons/xterm-addon-web-links/package.json index 5fc8bb57..25f4ce83 100644 --- a/addons/xterm-addon-web-links/package.json +++ b/addons/xterm-addon-web-links/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-web-links", - "version": "0.8.0", + "version": "0.9.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-webgl/package.json b/addons/xterm-addon-webgl/package.json index a6f00ef2..e8398c92 100644 --- a/addons/xterm-addon-webgl/package.json +++ b/addons/xterm-addon-webgl/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-webgl", - "version": "0.15.0", + "version": "0.16.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/package.json b/package.json index e297cb28..8ffd1385 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xterm", "description": "Full xterm terminal, in your browser", - "version": "5.2.1", + "version": "5.3.0", "main": "lib/xterm.js", "style": "css/xterm.css", "types": "typings/xterm.d.ts", From 71ee3c7a33542f2f744cf48d48abf2bd964d51e1 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 7 Sep 2023 10:57:21 -0700 Subject: [PATCH 41/99] Disable image addon publishing --- bin/publish.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/publish.js b/bin/publish.js index ec089e7b..e9c7c3e8 100644 --- a/bin/publish.js +++ b/bin/publish.js @@ -30,7 +30,7 @@ const addonPackageDirs = [ path.resolve(__dirname, '../addons/xterm-addon-attach'), path.resolve(__dirname, '../addons/xterm-addon-canvas'), path.resolve(__dirname, '../addons/xterm-addon-fit'), - path.resolve(__dirname, '../addons/xterm-addon-image'), + // path.resolve(__dirname, '../addons/xterm-addon-image'), path.resolve(__dirname, '../addons/xterm-addon-ligatures'), path.resolve(__dirname, '../addons/xterm-addon-search'), path.resolve(__dirname, '../addons/xterm-addon-serialize'), From c64115e0a0850939ba13f2df78166a731c0b94be Mon Sep 17 00:00:00 2001 From: tisilent Date: Fri, 8 Sep 2023 17:34:03 +0800 Subject: [PATCH 42/99] Add handling of invisible --- src/browser/renderer/dom/DomRendererRowFactory.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index e6412b80..2d21a647 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -180,7 +180,11 @@ export class DomRendererRowFactory { && !isDecorated ) { // no span alterations, thus only account chars skipping all code below - text += chars; + if (cell.isInvisible()) { + text += WHITESPACE_CELL_CHAR; + } else { + text += chars; + } cellAmount++; continue; } else { From 393a8b3a864c933b28ae6458639ba5ebef20f233 Mon Sep 17 00:00:00 2001 From: tisilent Date: Fri, 8 Sep 2023 18:08:14 +0800 Subject: [PATCH 43/99] When DomRenderer blue,perform rendering. --- demo/client.ts | 16 ++++++++++++++++ demo/index.html | 8 ++++++++ src/browser/renderer/dom/DomRenderer.ts | 1 + 3 files changed, 25 insertions(+) diff --git a/demo/client.ts b/demo/client.ts index bde5ca72..85a1104e 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -119,6 +119,8 @@ const actionElements = { findResults: document.querySelector('#find-results') }; const paddingElement = document.getElementById('padding') as HTMLInputElement; +const selectionBackgroundElement = document.getElementById('selectionBackground') as HTMLInputElement; +const selectionInactiveBackgroundElement = document.getElementById('selectionInactiveBackground') as HTMLInputElement; const xtermjsTheme = { foreground: '#F8F8F8', @@ -146,6 +148,18 @@ function setPadding(): void { addons.fit.instance.fit(); } +function setSelectionInactiveBackground(): void { + const theme = { ...term.options.theme }; + theme.selectionInactiveBackground = selectionInactiveBackgroundElement.value; + term.options.theme = theme; +} + +function setSelectionBackground(): void { + const theme = { ...term.options.theme }; + theme.selectionBackground = selectionBackgroundElement.value; + term.options.theme = theme; +} + function getSearchOptions(): ISearchOptions { return { regex: (document.getElementById('regex') as HTMLInputElement).checked, @@ -333,6 +347,8 @@ function createTerminal(): void { resizeObserver.observe(terminalContainer); addDomListener(paddingElement, 'change', setPadding); + addDomListener(selectionInactiveBackgroundElement, 'change', setSelectionInactiveBackground); + addDomListener(selectionBackgroundElement, 'change', setSelectionBackground); addDomListener(actionElements.findNext, 'keydown', (e) => { if (e.key === 'Enter') { diff --git a/demo/index.html b/demo/index.html index 6d9be706..594f0ee7 100644 --- a/demo/index.html +++ b/demo/index.html @@ -75,6 +75,14 @@ +
+ + +
+
+ + +

Test

diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index 7413776c..98213e08 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -299,6 +299,7 @@ export class DomRenderer extends Disposable implements IRenderer { public handleBlur(): void { this._rowContainer.classList.remove(FOCUS_CLASS); + this.renderRows(0, this._bufferService.rows - 1); } public handleFocus(): void { From 2f233f2389ae05758aa1afd43e1111d963978165 Mon Sep 17 00:00:00 2001 From: tisilent Date: Fri, 8 Sep 2023 18:52:11 +0800 Subject: [PATCH 44/99] =?UTF-8?q?=F0=9F=92=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- demo/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/index.html b/demo/index.html index 594f0ee7..f56af820 100644 --- a/demo/index.html +++ b/demo/index.html @@ -76,7 +76,7 @@
- +
From f23af3661bc25e38c1df49dad0d493022c563882 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 07:28:41 -0700 Subject: [PATCH 45/99] wip --- package.json | 1 + test/playwright/Renderer.test.ts | 82 ++++++++++++++++++++++++++++++++ test/playwright/tsconfig.json | 3 +- yarn.lock | 12 +++++ 4 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 test/playwright/Renderer.test.ts diff --git a/package.json b/package.json index 8ffd1385..77f223aa 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "vtfeatures": "node bin/extract_vtfeatures.js src/**/*.ts src/*.ts" }, "devDependencies": { + "@lunapaint/png-codec": "^0.2.0", "@playwright/test": "^1.37.1", "@types/chai": "^4.2.22", "@types/debug": "^4.1.7", diff --git a/test/playwright/Renderer.test.ts b/test/playwright/Renderer.test.ts new file mode 100644 index 00000000..90cca908 --- /dev/null +++ b/test/playwright/Renderer.test.ts @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { decodePng } from '@lunapaint/png-codec'; +import { test } from '@playwright/test'; +import { ITheme } 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()); + +test.describe('WebGL Renderer Integration Tests', async () => { + // itWebgl('dispose removes renderer canvases', async function(): Promise { + // await setupBrowser(); + // assert.equal(await page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 2); + // await page.evaluate(`addon.dispose()`); + // assert.equal(await page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 0); + // await browser.close(); + // }); + + test.describe.only('colors', () => { + test('foreground 0-15', async () => { + const theme: ITheme = { + black: '#010203', + red: '#040506', + green: '#070809', + yellow: '#0a0b0c', + blue: '#0d0e0f', + magenta: '#101112', + cyan: '#131415', + white: '#161718' + }; + await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.proxy.write(`\x1b[30m█\x1b[31m█\x1b[32m█\x1b[33m█\x1b[34m█\x1b[35m█\x1b[36m█\x1b[37m█`); + await pollFor(ctx.page, () => getCellColor(1, 1), [1, 2, 3, 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [4, 5, 6, 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [7, 8, 9, 255]); + await pollFor(ctx.page, () => getCellColor(4, 1), [10, 11, 12, 255]); + await pollFor(ctx.page, () => getCellColor(5, 1), [13, 14, 15, 255]); + await pollFor(ctx.page, () => getCellColor(6, 1), [16, 17, 18, 255]); + await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); + await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); + }); + }); +}); + +async function getCellColor(col: number, row: number): Promise { + const buffer = await ctx.page.locator('#terminal-container .xterm-rows').screenshot({ path: 'ss.png' }); + const decoded = (await decodePng(buffer)).image; + const cellSize = { + width: decoded.width / await ctx.proxy.cols, + height: decoded.height / await ctx.proxy.rows, + }; + console.log('buffer', buffer); + console.log('decoded', decoded); + console.log('cellSize', cellSize); + const x = 1; //Math.floor((col - 1) + 0.5 * cellSize.width); + const y = 1; //Math.floor((row - 1) + 0.5 * cellSize.height); + const i = y * decoded.width + x; + console.log({ x, y, i }); + console.log('result', Array.from(decoded.data.slice(i, i + 4))); + return Array.from(decoded.data.slice(i, i + 4)); + + await ctx.page.evaluate(` + window.gl = window.term._core._renderService._renderer.value._gl; + window.result = new Uint8Array(4); + window.d = window.term._core._renderService.dimensions; + window.gl.readPixels( + Math.floor((${col - 0.5}) * window.d.device.cell.width), + Math.floor(window.gl.drawingBufferHeight - 1 - (${row - 0.5}) * window.d.device.cell.height), + 1, 1, window.gl.RGBA, window.gl.UNSIGNED_BYTE, window.result + ); + `); + // await page.locator('.header').screenshot({ path: 'screenshot.png' }); + return await ctx.page.evaluate(`Array.from(window.result)`); +} diff --git a/test/playwright/tsconfig.json b/test/playwright/tsconfig.json index 2b5e55fa..aeb31e32 100644 --- a/test/playwright/tsconfig.json +++ b/test/playwright/tsconfig.json @@ -7,7 +7,8 @@ "rootDir": ".", "outDir": "../../out-test/playwright", "types": [ - "../../node_modules/@types/node" + "../../node_modules/@types/node", + "../../node_modules/@lunapaint/png-codec" ], "sourceMap": true, "removeComments": true, diff --git a/yarn.lock b/yarn.lock index 2945d97b..50a4fb84 100644 --- a/yarn.lock +++ b/yarn.lock @@ -326,6 +326,13 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@lunapaint/png-codec@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@lunapaint/png-codec/-/png-codec-0.2.0.tgz#b9fe0a0728889af280c31239b061eb3f8c848816" + integrity sha512-S2Fk8+I27j8ZL585PlEK9hhljZcp6j+JWB5ZHAeePdufJMYHxXD2zlatLRaiy5riXRFqkCi/gong7yP9kSsEZg== + dependencies: + pako "^2.0.4" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -3002,6 +3009,11 @@ package-hash@^4.0.0: lodash.flattendeep "^4.4.0" release-zalgo "^1.0.0" +pako@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" + integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" From 66a6ecea1483dbf9755a407b9ea9822067ee3a62 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 07:46:45 -0700 Subject: [PATCH 46/99] Improve playwright debug logging --- test/playwright/TestUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/playwright/TestUtils.ts b/test/playwright/TestUtils.ts index 0ab95b76..e91b8310 100644 --- a/test/playwright/TestUtils.ts +++ b/test/playwright/TestUtils.ts @@ -383,7 +383,7 @@ export async function pollFor(page: playwright.Page, evalOrFn: string | (() = const result = typeof evalOrFn === 'string' ? await page.evaluate(evalOrFn) : await evalOrFn(); if (process.env.DEBUG) { - console.log('pollFor result: ', JSON.stringify(result)); + console.log('pollFor\n actual: ', JSON.stringify(result), ' expected: ', JSON.stringify(val)); } if (!deepEqual(result, val)) { From ea45b04105eead07db4ecdf8f8f298fc2b66a466 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 07:47:14 -0700 Subject: [PATCH 47/99] First working dom renderer api tests Part of #4767 --- test/playwright/Renderer.test.ts | 78 ++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 35 deletions(-) diff --git a/test/playwright/Renderer.test.ts b/test/playwright/Renderer.test.ts index 90cca908..0721b678 100644 --- a/test/playwright/Renderer.test.ts +++ b/test/playwright/Renderer.test.ts @@ -4,7 +4,7 @@ */ import { decodePng } from '@lunapaint/png-codec'; -import { test } from '@playwright/test'; +import { LocatorScreenshotOptions, test } from '@playwright/test'; import { ITheme } from 'xterm'; import { createTestContext, ITestContext, openTerminal, pollFor } from './TestUtils'; @@ -15,16 +15,10 @@ test.beforeAll(async ({ browser }) => { }); test.afterAll(async () => await ctx.page.close()); -test.describe('WebGL Renderer Integration Tests', async () => { - // itWebgl('dispose removes renderer canvases', async function(): Promise { - // await setupBrowser(); - // assert.equal(await page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 2); - // await page.evaluate(`addon.dispose()`); - // assert.equal(await page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 0); - // await browser.close(); - // }); +test.describe.only('WebGL Renderer Integration Tests', async () => { + test.beforeEach(() => ctx.proxy.reset()); - test.describe.only('colors', () => { + test.describe('colors', () => { test('foreground 0-15', async () => { const theme: ITheme = { black: '#010203', @@ -48,35 +42,49 @@ test.describe('WebGL Renderer Integration Tests', async () => { await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); }); }); + + test('foreground 0-7 drawBoldTextInBrightColors', async () => { + const theme: ITheme = { + brightBlack: '#010203', + brightRed: '#040506', + brightGreen: '#070809', + brightYellow: '#0a0b0c', + brightBlue: '#0d0e0f', + brightMagenta: '#101112', + brightCyan: '#131415', + brightWhite: '#161718' + }; + await ctx.page.evaluate(` + window.term.options.theme = ${JSON.stringify(theme)}; + window.term.options.drawBoldTextInBrightColors = true; + `); + await ctx.proxy.write(`\x1b[1;30m█\x1b[1;31m█\x1b[1;32m█\x1b[1;33m█\x1b[1;34m█\x1b[1;35m█\x1b[1;36m█\x1b[1;37m█`); + await pollFor(ctx.page, () => getCellColor(1, 1), [1, 2, 3, 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [4, 5, 6, 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [7, 8, 9, 255]); + await pollFor(ctx.page, () => getCellColor(4, 1), [10, 11, 12, 255]); + await pollFor(ctx.page, () => getCellColor(5, 1), [13, 14, 15, 255]); + await pollFor(ctx.page, () => getCellColor(6, 1), [16, 17, 18, 255]); + await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); + await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); + }); }); -async function getCellColor(col: number, row: number): Promise { - const buffer = await ctx.page.locator('#terminal-container .xterm-rows').screenshot({ path: 'ss.png' }); +/** + * Gets the color of the pixel in the center of a cell. + * @param col The 1-based column index to get the color for. + * @param row The 1-based row index to get the color for. + */ +async function getCellColor(col: number, row: number): Promise<[red: number, green: number, blue: number, alpha: number]> { + const screenshotOptions: LocatorScreenshotOptions | undefined = process.env.DEBUG ? { path: 'out-test/playwright/screenshot.png' } : undefined; + const buffer = await ctx.page.locator('#terminal-container .xterm-rows').screenshot(screenshotOptions); const decoded = (await decodePng(buffer)).image; const cellSize = { width: decoded.width / await ctx.proxy.cols, - height: decoded.height / await ctx.proxy.rows, + height: decoded.height / await ctx.proxy.rows }; - console.log('buffer', buffer); - console.log('decoded', decoded); - console.log('cellSize', cellSize); - const x = 1; //Math.floor((col - 1) + 0.5 * cellSize.width); - const y = 1; //Math.floor((row - 1) + 0.5 * cellSize.height); - const i = y * decoded.width + x; - console.log({ x, y, i }); - console.log('result', Array.from(decoded.data.slice(i, i + 4))); - return Array.from(decoded.data.slice(i, i + 4)); - - await ctx.page.evaluate(` - window.gl = window.term._core._renderService._renderer.value._gl; - window.result = new Uint8Array(4); - window.d = window.term._core._renderService.dimensions; - window.gl.readPixels( - Math.floor((${col - 0.5}) * window.d.device.cell.width), - Math.floor(window.gl.drawingBufferHeight - 1 - (${row - 0.5}) * window.d.device.cell.height), - 1, 1, window.gl.RGBA, window.gl.UNSIGNED_BYTE, window.result - ); - `); - // await page.locator('.header').screenshot({ path: 'screenshot.png' }); - return await ctx.page.evaluate(`Array.from(window.result)`); + const x = Math.floor((col - 1/* 1- to 0-based index */ + 0.5/* middle of cell */) * cellSize.width); + const y = Math.floor((row - 1/* 1- to 0-based index */ + 0.5/* middle of cell */) * cellSize.height); + const i = (y * decoded.width + x) * 4/* 4 channels per pixel */; + return Array.from(decoded.data.slice(i, i + 4)) as [number, number, number, number]; } From a656409bb5717e831c001f4e996a07b6ce98da91 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 08:04:36 -0700 Subject: [PATCH 48/99] All color dom renderer tests --- test/playwright/Renderer.test.ts | 708 ++++++++++++++++++++++++++++++- test/playwright/TestUtils.ts | 3 +- 2 files changed, 701 insertions(+), 10 deletions(-) diff --git a/test/playwright/Renderer.test.ts b/test/playwright/Renderer.test.ts index 0721b678..45705bb1 100644 --- a/test/playwright/Renderer.test.ts +++ b/test/playwright/Renderer.test.ts @@ -3,10 +3,10 @@ * @license MIT */ -import { decodePng } from '@lunapaint/png-codec'; +import { IImage32, decodePng } from '@lunapaint/png-codec'; import { LocatorScreenshotOptions, test } from '@playwright/test'; import { ITheme } from 'xterm'; -import { createTestContext, ITestContext, openTerminal, pollFor } from './TestUtils'; +import { createTestContext, ITestContext, MaybeAsync, openTerminal, pollFor } from './TestUtils'; let ctx: ITestContext; test.beforeAll(async ({ browser }) => { @@ -16,7 +16,14 @@ test.beforeAll(async ({ browser }) => { test.afterAll(async () => await ctx.page.close()); test.describe.only('WebGL Renderer Integration Tests', async () => { - test.beforeEach(() => ctx.proxy.reset()); + // TODO: Speed up tests + test.setTimeout(30000); + + test.beforeEach(async () => { + await ctx.proxy.reset(); + // Clear the cached screenshot before each test + frameDetails = undefined; + }); test.describe('colors', () => { test('foreground 0-15', async () => { @@ -68,6 +75,655 @@ test.describe.only('WebGL Renderer Integration Tests', async () => { await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); }); + + test('background 0-15', async () => { + const theme: ITheme = { + black: '#010203', + red: '#040506', + green: '#070809', + yellow: '#0a0b0c', + blue: '#0d0e0f', + magenta: '#101112', + cyan: '#131415', + white: '#161718' + }; + await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.proxy.write(`\x1b[40m \x1b[41m \x1b[42m \x1b[43m \x1b[44m \x1b[45m \x1b[46m \x1b[47m `); + await pollFor(ctx.page, () => getCellColor(1, 1), [1, 2, 3, 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [4, 5, 6, 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [7, 8, 9, 255]); + await pollFor(ctx.page, () => getCellColor(4, 1), [10, 11, 12, 255]); + await pollFor(ctx.page, () => getCellColor(5, 1), [13, 14, 15, 255]); + await pollFor(ctx.page, () => getCellColor(6, 1), [16, 17, 18, 255]); + await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); + await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); + }); + + test('foreground 0-15 inverse', async () => { + const theme: ITheme = { + black: '#010203', + red: '#040506', + green: '#070809', + yellow: '#0a0b0c', + blue: '#0d0e0f', + magenta: '#101112', + cyan: '#131415', + white: '#161718' + }; + await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.proxy.write(`\x1b[7;30m \x1b[7;31m \x1b[7;32m \x1b[7;33m \x1b[7;34m \x1b[7;35m \x1b[7;36m \x1b[7;37m `); + await pollFor(ctx.page, () => getCellColor(1, 1), [1, 2, 3, 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [4, 5, 6, 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [7, 8, 9, 255]); + await pollFor(ctx.page, () => getCellColor(4, 1), [10, 11, 12, 255]); + await pollFor(ctx.page, () => getCellColor(5, 1), [13, 14, 15, 255]); + await pollFor(ctx.page, () => getCellColor(6, 1), [16, 17, 18, 255]); + await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); + await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); + }); + + test('background 0-15 inverse', async () => { + const theme: ITheme = { + black: '#010203', + red: '#040506', + green: '#070809', + yellow: '#0a0b0c', + blue: '#0d0e0f', + magenta: '#101112', + cyan: '#131415', + white: '#161718' + }; + await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.proxy.write(`\x1b[7;40m█\x1b[7;41m█\x1b[7;42m█\x1b[7;43m█\x1b[7;44m█\x1b[7;45m█\x1b[7;46m█\x1b[7;47m█`); + await pollFor(ctx.page, () => getCellColor(1, 1), [1, 2, 3, 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [4, 5, 6, 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [7, 8, 9, 255]); + await pollFor(ctx.page, () => getCellColor(4, 1), [10, 11, 12, 255]); + await pollFor(ctx.page, () => getCellColor(5, 1), [13, 14, 15, 255]); + await pollFor(ctx.page, () => getCellColor(6, 1), [16, 17, 18, 255]); + await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); + await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); + }); + + test('foreground 0-15 inivisible', async () => { + const theme: ITheme = { + black: '#010203', + red: '#040506', + green: '#070809', + yellow: '#0a0b0c', + blue: '#0d0e0f', + magenta: '#101112', + cyan: '#131415', + white: '#161718' + }; + await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.proxy.write(`\x1b[8;30m \x1b[8;31m \x1b[8;32m \x1b[8;33m \x1b[8;34m \x1b[8;35m \x1b[8;36m \x1b[8;37m `); + await pollFor(ctx.page, () => getCellColor(1, 1), [0, 0, 0, 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [0, 0, 0, 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [0, 0, 0, 255]); + await pollFor(ctx.page, () => getCellColor(4, 1), [0, 0, 0, 255]); + await pollFor(ctx.page, () => getCellColor(5, 1), [0, 0, 0, 255]); + await pollFor(ctx.page, () => getCellColor(6, 1), [0, 0, 0, 255]); + await pollFor(ctx.page, () => getCellColor(7, 1), [0, 0, 0, 255]); + await pollFor(ctx.page, () => getCellColor(8, 1), [0, 0, 0, 255]); + }); + + test('background 0-15 inivisible', async () => { + const theme: ITheme = { + black: '#010203', + red: '#040506', + green: '#070809', + yellow: '#0a0b0c', + blue: '#0d0e0f', + magenta: '#101112', + cyan: '#131415', + white: '#161718' + }; + await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.proxy.write(`\x1b[8;40m█\x1b[8;41m█\x1b[8;42m█\x1b[8;43m█\x1b[8;44m█\x1b[8;45m█\x1b[8;46m█\x1b[8;47m█`); + await pollFor(ctx.page, () => getCellColor(1, 1), [1, 2, 3, 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [4, 5, 6, 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [7, 8, 9, 255]); + await pollFor(ctx.page, () => getCellColor(4, 1), [10, 11, 12, 255]); + await pollFor(ctx.page, () => getCellColor(5, 1), [13, 14, 15, 255]); + await pollFor(ctx.page, () => getCellColor(6, 1), [16, 17, 18, 255]); + await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); + await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); + }); + + test('foreground 0-15 bright', async () => { + const theme: ITheme = { + brightBlack: '#010203', + brightRed: '#040506', + brightGreen: '#070809', + brightYellow: '#0a0b0c', + brightBlue: '#0d0e0f', + brightMagenta: '#101112', + brightCyan: '#131415', + brightWhite: '#161718' + }; + await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.proxy.write(`\x1b[90m█\x1b[91m█\x1b[92m█\x1b[93m█\x1b[94m█\x1b[95m█\x1b[96m█\x1b[97m█`); + await pollFor(ctx.page, () => getCellColor(1, 1), [1, 2, 3, 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [4, 5, 6, 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [7, 8, 9, 255]); + await pollFor(ctx.page, () => getCellColor(4, 1), [10, 11, 12, 255]); + await pollFor(ctx.page, () => getCellColor(5, 1), [13, 14, 15, 255]); + await pollFor(ctx.page, () => getCellColor(6, 1), [16, 17, 18, 255]); + await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); + await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); + }); + + test('background 0-15 bright', async () => { + const theme: ITheme = { + brightBlack: '#010203', + brightRed: '#040506', + brightGreen: '#070809', + brightYellow: '#0a0b0c', + brightBlue: '#0d0e0f', + brightMagenta: '#101112', + brightCyan: '#131415', + brightWhite: '#161718' + }; + await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.proxy.write(`\x1b[100m \x1b[101m \x1b[102m \x1b[103m \x1b[104m \x1b[105m \x1b[106m \x1b[107m `); + await pollFor(ctx.page, () => getCellColor(1, 1), [1, 2, 3, 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [4, 5, 6, 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [7, 8, 9, 255]); + await pollFor(ctx.page, () => getCellColor(4, 1), [10, 11, 12, 255]); + await pollFor(ctx.page, () => getCellColor(5, 1), [13, 14, 15, 255]); + await pollFor(ctx.page, () => getCellColor(6, 1), [16, 17, 18, 255]); + await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); + await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); + }); + + test('foreground 16-255', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\x1b[38;5;${16 + y * 16 + x}m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.slice(1, 3), 16); + const g = parseInt(cssColor.slice(3, 5), 16); + const b = parseInt(cssColor.slice(5, 7), 16); + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); + } + } + }); + + test('background 16-255', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\x1b[48;5;${16 + y * 16 + x}m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.slice(1, 3), 16); + const g = parseInt(cssColor.slice(3, 5), 16); + const b = parseInt(cssColor.slice(5, 7), 16); + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); + } + } + }); + + test('foreground 16-255 inverse', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\x1b[7;38;5;${16 + y * 16 + x}m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.slice(1, 3), 16); + const g = parseInt(cssColor.slice(3, 5), 16); + const b = parseInt(cssColor.slice(5, 7), 16); + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); + } + } + }); + + test('background 16-255 inverse', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\x1b[7;48;5;${16 + y * 16 + x}m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.slice(1, 3), 16); + const g = parseInt(cssColor.slice(3, 5), 16); + const b = parseInt(cssColor.slice(5, 7), 16); + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); + } + } + }); + + test('foreground 16-255 invisible', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\x1b[8;38;5;${16 + y * 16 + x}m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, 0, 0, 255]); + } + } + }); + + test('background 16-255 invisible', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\x1b[8;48;5;${16 + y * 16 + x}m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.slice(1, 3), 16); + const g = parseInt(cssColor.slice(3, 5), 16); + const b = parseInt(cssColor.slice(5, 7), 16); + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); + } + } + }); + + test('foreground 16-255 dim', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\x1b[2;38;5;${16 + y * 16 + x}m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.slice(1, 3), 16); + const g = parseInt(cssColor.slice(3, 5), 16); + const b = parseInt(cssColor.slice(5, 7), 16); + // It's difficult to assert the exact color due to rounding, just ensure the color differs + // to the regular color + await pollFor(ctx.page, async () => { + const c = await getCellColor(x + 1, y + 1); + return ( + (c[0] === 0 || c[0] !== r) && + (c[1] === 0 || c[1] !== g) && + (c[2] === 0 || c[2] !== b) + ); + }, true); + } + } + }); + + test('background 16-255 dim', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\x1b[2;48;5;${16 + y * 16 + x}m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.slice(1, 3), 16); + const g = parseInt(cssColor.slice(3, 5), 16); + const b = parseInt(cssColor.slice(5, 7), 16); + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); + } + } + }); + + test('foreground true color red', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[38;2;${i};0;0m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]); + } + } + }); + + test('background true color red', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[48;2;${i};0;0m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]); + } + } + }); + + test('foreground true color green', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[38;2;0;${i};0m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]); + } + } + }); + + test('background true color green', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[48;2;0;${i};0m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]); + } + } + }); + + test('foreground true color blue', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[38;2;0;0;${i}m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]); + } + } + }); + + test('background true color blue', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[48;2;0;0;${i}m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]); + } + } + }); + + test('foreground true color grey', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[38;2;${i};${i};${i}m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); + } + } + }); + + test('background true color grey', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[48;2;${i};${i};${i}m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); + } + } + }); + + test('foreground true color red inverse', async function(): Promise { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[7;38;2;${i};0;0m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]); + } + } + }); + + test('background true color red inverse', async function(): Promise { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[7;48;2;${i};0;0m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]); + } + } + }); + + test('foreground true color green inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[7;38;2;0;${i};0m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]); + } + } + }); + + test('background true color green inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[7;48;2;0;${i};0m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]); + } + } + }); + + test('foreground true color blue inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[7;38;2;0;0;${i}m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]); + } + } + }); + + test('background true color blue inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[7;48;2;0;0;${i}m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]); + } + } + }); + + test('foreground true color grey inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[7;38;2;${i};${i};${i}m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); + } + } + }); + + test('background true color grey inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[7;48;2;${i};${i};${i}m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); + } + } + }); + + test('foreground true color grey invisible', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[8;38;2;${i};${i};${i}m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, 0, 0, 255]); + } + } + }); + + test('background true color grey invisible', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[8;48;2;${i};${i};${i}m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); + } + } + }); }); /** @@ -75,16 +731,50 @@ test.describe.only('WebGL Renderer Integration Tests', async () => { * @param col The 1-based column index to get the color for. * @param row The 1-based row index to get the color for. */ -async function getCellColor(col: number, row: number): Promise<[red: number, green: number, blue: number, alpha: number]> { +function getCellColor(col: number, row: number): MaybeAsync<[red: number, green: number, blue: number, alpha: number]> { + if (!frameDetails) { + return getFrameDetails().then(frameDetails => getCellColorInner(frameDetails, col, row)); + } + return getCellColorInner(frameDetails, col, row); +} + +let frameDetails: { cols: number, rows: number, decoded: IImage32 } | undefined = undefined; +async function getFrameDetails(): Promise<{ cols: number, rows: number, decoded: IImage32 }> { const screenshotOptions: LocatorScreenshotOptions | undefined = process.env.DEBUG ? { path: 'out-test/playwright/screenshot.png' } : undefined; const buffer = await ctx.page.locator('#terminal-container .xterm-rows').screenshot(screenshotOptions); - const decoded = (await decodePng(buffer)).image; + frameDetails = { + cols: await ctx.proxy.cols, + rows: await ctx.proxy.rows, + decoded: (await decodePng(buffer, { force32: true })).image + }; + return frameDetails; +} + +function getCellColorInner(frameDetails: { cols: number, rows: number, decoded: IImage32 }, col: number, row: number): [red: number, green: number, blue: number, alpha: number] { const cellSize = { - width: decoded.width / await ctx.proxy.cols, - height: decoded.height / await ctx.proxy.rows + width: frameDetails.decoded.width / frameDetails.cols, + height: frameDetails.decoded.height / frameDetails.rows }; const x = Math.floor((col - 1/* 1- to 0-based index */ + 0.5/* middle of cell */) * cellSize.width); const y = Math.floor((row - 1/* 1- to 0-based index */ + 0.5/* middle of cell */) * cellSize.height); - const i = (y * decoded.width + x) * 4/* 4 channels per pixel */; - return Array.from(decoded.data.slice(i, i + 4)) as [number, number, number, number]; + const i = (y * frameDetails.decoded.width + x) * 4/* 4 channels per pixel */; + return Array.from(frameDetails.decoded.data.slice(i, i + 4)) as [number, number, number, number]; } + +const COLORS_16_TO_255 = [ + '#000000', '#00005f', '#000087', '#0000af', '#0000d7', '#0000ff', '#005f00', '#005f5f', '#005f87', '#005faf', '#005fd7', '#005fff', '#008700', '#00875f', '#008787', '#0087af', + '#0087d7', '#0087ff', '#00af00', '#00af5f', '#00af87', '#00afaf', '#00afd7', '#00afff', '#00d700', '#00d75f', '#00d787', '#00d7af', '#00d7d7', '#00d7ff', '#00ff00', '#00ff5f', + '#00ff87', '#00ffaf', '#00ffd7', '#00ffff', '#5f0000', '#5f005f', '#5f0087', '#5f00af', '#5f00d7', '#5f00ff', '#5f5f00', '#5f5f5f', '#5f5f87', '#5f5faf', '#5f5fd7', '#5f5fff', + '#5f8700', '#5f875f', '#5f8787', '#5f87af', '#5f87d7', '#5f87ff', '#5faf00', '#5faf5f', '#5faf87', '#5fafaf', '#5fafd7', '#5fafff', '#5fd700', '#5fd75f', '#5fd787', '#5fd7af', + '#5fd7d7', '#5fd7ff', '#5fff00', '#5fff5f', '#5fff87', '#5fffaf', '#5fffd7', '#5fffff', '#870000', '#87005f', '#870087', '#8700af', '#8700d7', '#8700ff', '#875f00', '#875f5f', + '#875f87', '#875faf', '#875fd7', '#875fff', '#878700', '#87875f', '#878787', '#8787af', '#8787d7', '#8787ff', '#87af00', '#87af5f', '#87af87', '#87afaf', '#87afd7', '#87afff', + '#87d700', '#87d75f', '#87d787', '#87d7af', '#87d7d7', '#87d7ff', '#87ff00', '#87ff5f', '#87ff87', '#87ffaf', '#87ffd7', '#87ffff', '#af0000', '#af005f', '#af0087', '#af00af', + '#af00d7', '#af00ff', '#af5f00', '#af5f5f', '#af5f87', '#af5faf', '#af5fd7', '#af5fff', '#af8700', '#af875f', '#af8787', '#af87af', '#af87d7', '#af87ff', '#afaf00', '#afaf5f', + '#afaf87', '#afafaf', '#afafd7', '#afafff', '#afd700', '#afd75f', '#afd787', '#afd7af', '#afd7d7', '#afd7ff', '#afff00', '#afff5f', '#afff87', '#afffaf', '#afffd7', '#afffff', + '#d70000', '#d7005f', '#d70087', '#d700af', '#d700d7', '#d700ff', '#d75f00', '#d75f5f', '#d75f87', '#d75faf', '#d75fd7', '#d75fff', '#d78700', '#d7875f', '#d78787', '#d787af', + '#d787d7', '#d787ff', '#d7af00', '#d7af5f', '#d7af87', '#d7afaf', '#d7afd7', '#d7afff', '#d7d700', '#d7d75f', '#d7d787', '#d7d7af', '#d7d7d7', '#d7d7ff', '#d7ff00', '#d7ff5f', + '#d7ff87', '#d7ffaf', '#d7ffd7', '#d7ffff', '#ff0000', '#ff005f', '#ff0087', '#ff00af', '#ff00d7', '#ff00ff', '#ff5f00', '#ff5f5f', '#ff5f87', '#ff5faf', '#ff5fd7', '#ff5fff', + '#ff8700', '#ff875f', '#ff8787', '#ff87af', '#ff87d7', '#ff87ff', '#ffaf00', '#ffaf5f', '#ffaf87', '#ffafaf', '#ffafd7', '#ffafff', '#ffd700', '#ffd75f', '#ffd787', '#ffd7af', + '#ffd7d7', '#ffd7ff', '#ffff00', '#ffff5f', '#ffff87', '#ffffaf', '#ffffd7', '#ffffff', '#080808', '#121212', '#1c1c1c', '#262626', '#303030', '#3a3a3a', '#444444', '#4e4e4e', + '#585858', '#626262', '#6c6c6c', '#767676', '#808080', '#8a8a8a', '#949494', '#9e9e9e', '#a8a8a8', '#b2b2b2', '#bcbcbc', '#c6c6c6', '#d0d0d0', '#dadada', '#e4e4e4', '#eeeeee' +]; diff --git a/test/playwright/TestUtils.ts b/test/playwright/TestUtils.ts index e91b8310..f556e41b 100644 --- a/test/playwright/TestUtils.ts +++ b/test/playwright/TestUtils.ts @@ -374,8 +374,9 @@ export async function openTerminal(ctx: ITestContext, options: ITerminalOptions } +export type MaybeAsync = Promise | T; -export async function pollFor(page: playwright.Page, evalOrFn: string | (() => Promise), val: T, preFn?: () => Promise, maxDuration?: number, stack?: string): Promise { +export async function pollFor(page: playwright.Page, evalOrFn: string | (() => MaybeAsync), val: T, preFn?: () => Promise, maxDuration?: number, stack?: string): Promise { stack ??= new Error().stack; if (preFn) { await preFn(); From 97be015234dea2f3aa775e8ba2aaa9aca0f49c00 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 08:08:45 -0700 Subject: [PATCH 49/99] All tests copied over, some failing --- test/playwright/Renderer.test.ts | 338 +++++++++++++++++++++++++++++++ 1 file changed, 338 insertions(+) diff --git a/test/playwright/Renderer.test.ts b/test/playwright/Renderer.test.ts index 45705bb1..e1f40213 100644 --- a/test/playwright/Renderer.test.ts +++ b/test/playwright/Renderer.test.ts @@ -724,6 +724,344 @@ test.describe.only('WebGL Renderer Integration Tests', async () => { } } }); + + test.describe.skip('minimumContrastRatio', async () => { + test('should adjust 0-15 colors on black background', async () => { + const theme: ITheme = { + black: '#2e3436', + red: '#cc0000', + green: '#4e9a06', + yellow: '#c4a000', + blue: '#3465a4', + magenta: '#75507b', + cyan: '#06989a', + white: '#d3d7cf', + brightBlack: '#555753', + brightRed: '#ef2929', + brightGreen: '#8ae234', + brightYellow: '#fce94f', + brightBlue: '#729fcf', + brightMagenta: '#ad7fa8', + brightCyan: '#34e2e2', + brightWhite: '#eeeeec' + }; + await ctx.page.evaluate(` + window.term.options.theme = ${JSON.stringify(theme)}; + window.term.options.minimumContrastRatio = 1; + `); + // Block characters ignore block elements so a different char is used here + await ctx.proxy.write( + `\x1b[30m■\x1b[31m■\x1b[32m■\x1b[33m■\x1b[34m■\x1b[35m■\x1b[36m■\x1b[37m■\r\n` + + `\x1b[90m■\x1b[91m■\x1b[92m■\x1b[93m■\x1b[94m■\x1b[95m■\x1b[96m■\x1b[97m■` + ); + // Validate before minimumContrastRatio is applied + await pollFor(ctx.page, () => getCellColor(1, 1), [0x2e, 0x34, 0x36, 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [0xcc, 0x00, 0x00, 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [0x4e, 0x9a, 0x06, 255]); + await pollFor(ctx.page, () => getCellColor(4, 1), [0xc4, 0xa0, 0x00, 255]); + await pollFor(ctx.page, () => getCellColor(5, 1), [0x34, 0x65, 0xa4, 255]); + await pollFor(ctx.page, () => getCellColor(6, 1), [0x75, 0x50, 0x7b, 255]); + await pollFor(ctx.page, () => getCellColor(7, 1), [0x06, 0x98, 0x9a, 255]); + await pollFor(ctx.page, () => getCellColor(8, 1), [0xd3, 0xd7, 0xcf, 255]); + await pollFor(ctx.page, () => getCellColor(1, 2), [0x55, 0x57, 0x53, 255]); + await pollFor(ctx.page, () => getCellColor(2, 2), [0xef, 0x29, 0x29, 255]); + await pollFor(ctx.page, () => getCellColor(3, 2), [0x8a, 0xe2, 0x34, 255]); + await pollFor(ctx.page, () => getCellColor(4, 2), [0xfc, 0xe9, 0x4f, 255]); + await pollFor(ctx.page, () => getCellColor(5, 2), [0x72, 0x9f, 0xcf, 255]); + await pollFor(ctx.page, () => getCellColor(6, 2), [0xad, 0x7f, 0xa8, 255]); + await pollFor(ctx.page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]); + await pollFor(ctx.page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]); + // Setting and check for minimum contrast values, note that these are not + // exact to the contrast ratio, if the increase luminance algorithm + // changes then these will probably fail + await ctx.page.evaluate(`window.term.options.minimumContrastRatio = 10;`); + await pollFor(ctx.page, () => getCellColor(1, 1), [176, 180, 180, 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [238, 158, 158, 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [152, 198, 110, 255]); + await pollFor(ctx.page, () => getCellColor(4, 1), [208, 179, 49, 255]); + await pollFor(ctx.page, () => getCellColor(5, 1), [161, 183, 215, 255]); + await pollFor(ctx.page, () => getCellColor(6, 1), [191, 174, 194, 255]); + await pollFor(ctx.page, () => getCellColor(7, 1), [110, 197, 198, 255]); + await pollFor(ctx.page, () => getCellColor(8, 1), [211, 215, 207, 255]); + await pollFor(ctx.page, () => getCellColor(1, 2), [183, 185, 183, 255]); + await pollFor(ctx.page, () => getCellColor(2, 2), [249, 156, 156, 255]); + await pollFor(ctx.page, () => getCellColor(3, 2), [138, 226, 52, 255]); + await pollFor(ctx.page, () => getCellColor(4, 2), [252, 233, 79, 255]); + await pollFor(ctx.page, () => getCellColor(5, 2), [154, 186, 221, 255]); + await pollFor(ctx.page, () => getCellColor(6, 2), [203, 173, 199, 255]); + // Unchanged + await pollFor(ctx.page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]); + await pollFor(ctx.page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]); + }); + + test('should adjust 0-15 colors on white background', async () => { + const theme: ITheme = { + background: '#ffffff', + black: '#2e3436', + red: '#cc0000', + green: '#4e9a06', + yellow: '#c4a000', + blue: '#3465a4', + magenta: '#75507b', + cyan: '#06989a', + white: '#d3d7cf', + brightBlack: '#555753', + brightRed: '#ef2929', + brightGreen: '#8ae234', + brightYellow: '#fce94f', + brightBlue: '#729fcf', + brightMagenta: '#ad7fa8', + brightCyan: '#34e2e2', + brightWhite: '#eeeeec' + }; + await ctx.page.evaluate(` + window.term.options.theme = ${JSON.stringify(theme)}; + window.term.options.minimumContrastRatio = 1; + `); + // Block characters ignore block elements so a different char is used here + await ctx.proxy.write( + `\x1b[30m■\x1b[31m■\x1b[32m■\x1b[33m■\x1b[34m■\x1b[35m■\x1b[36m■\x1b[37m■\r\n` + + `\x1b[90m■\x1b[91m■\x1b[92m■\x1b[93m■\x1b[94m■\x1b[95m■\x1b[96m■\x1b[97m■` + ); + // Validate before minimumContrastRatio is applied + await pollFor(ctx.page, () => getCellColor(1, 1), [0x2e, 0x34, 0x36, 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [0xcc, 0x00, 0x00, 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [0x4e, 0x9a, 0x06, 255]); + await pollFor(ctx.page, () => getCellColor(4, 1), [0xc4, 0xa0, 0x00, 255]); + await pollFor(ctx.page, () => getCellColor(5, 1), [0x34, 0x65, 0xa4, 255]); + await pollFor(ctx.page, () => getCellColor(6, 1), [0x75, 0x50, 0x7b, 255]); + await pollFor(ctx.page, () => getCellColor(7, 1), [0x06, 0x98, 0x9a, 255]); + await pollFor(ctx.page, () => getCellColor(8, 1), [0xd3, 0xd7, 0xcf, 255]); + await pollFor(ctx.page, () => getCellColor(1, 2), [0x55, 0x57, 0x53, 255]); + await pollFor(ctx.page, () => getCellColor(2, 2), [0xef, 0x29, 0x29, 255]); + await pollFor(ctx.page, () => getCellColor(3, 2), [0x8a, 0xe2, 0x34, 255]); + await pollFor(ctx.page, () => getCellColor(4, 2), [0xfc, 0xe9, 0x4f, 255]); + await pollFor(ctx.page, () => getCellColor(5, 2), [0x72, 0x9f, 0xcf, 255]); + await pollFor(ctx.page, () => getCellColor(6, 2), [0xad, 0x7f, 0xa8, 255]); + await pollFor(ctx.page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]); + await pollFor(ctx.page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]); + // Setting and check for minimum contrast values, note that these are not + // exact to the contrast ratio, if the increase luminance algorithm + // changes then these will probably fail + await ctx.page.evaluate(`window.term.options.minimumContrastRatio = 10;`); + await pollFor(ctx.page, () => getCellColor(1, 1), [46, 52, 54, 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [132, 0, 0, 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [36, 72, 0, 255]); + await pollFor(ctx.page, () => getCellColor(4, 1), [72, 59, 0, 255]); + await pollFor(ctx.page, () => getCellColor(5, 1), [32, 64, 106, 255]); + await pollFor(ctx.page, () => getCellColor(6, 1), [75, 51, 80, 255]); + await pollFor(ctx.page, () => getCellColor(7, 1), [0, 71, 72, 255]); + await pollFor(ctx.page, () => getCellColor(8, 1), [64, 64, 63, 255]); + await pollFor(ctx.page, () => getCellColor(1, 2), [61, 63, 59, 255]); + await pollFor(ctx.page, () => getCellColor(2, 2), [125, 19, 19, 255]); + await pollFor(ctx.page, () => getCellColor(3, 2), [40, 67, 13, 255]); + await pollFor(ctx.page, () => getCellColor(4, 2), [67, 63, 19, 255]); + await pollFor(ctx.page, () => getCellColor(5, 2), [45, 65, 87, 255]); + await pollFor(ctx.page, () => getCellColor(6, 2), [81, 57, 78, 255]); + await pollFor(ctx.page, () => getCellColor(7, 2), [13, 67, 67, 255]); + await pollFor(ctx.page, () => getCellColor(8, 2), [64, 64, 64, 255]); + }); + + test('should enforce half the contrast for dim cells', async () => { + const theme: ITheme = { + background: '#ffffff', + black: '#2e3436', + red: '#cc0000', + green: '#4e9a06', + yellow: '#c4a000', + blue: '#3465a4', + magenta: '#75507b', + cyan: '#06989a', + white: '#d3d7cf', + brightBlack: '#555753', + brightRed: '#ef2929', + brightGreen: '#8ae234', + brightYellow: '#fce94f', + brightBlue: '#729fcf', + brightMagenta: '#ad7fa8', + brightCyan: '#34e2e2', + brightWhite: '#eeeeec' + }; + await ctx.page.evaluate(` + window.term.options.theme = ${JSON.stringify(theme)}; + window.term.options.minimumContrastRatio = 1; + `); + // Block characters ignore block elements so a different char is used here + await ctx.proxy.write( + '\x1b[2m' + + `\x1b[30m■\x1b[31m■\x1b[32m■\x1b[33m■\x1b[34m■\x1b[35m■\x1b[36m■\x1b[37m■\r\n` + + `\x1b[90m■\x1b[91m■\x1b[92m■\x1b[93m■\x1b[94m■\x1b[95m■\x1b[96m■\x1b[97m■` + ); + // Validate before minimumContrastRatio is applied + await pollFor(ctx.page, () => getCellColor(1, 1), [Math.floor((255 + 0x2e) / 2), Math.floor((255 + 0x34) / 2), Math.floor((255 + 0x36) / 2), 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [Math.floor((255 + 0xcc) / 2), Math.floor((255 + 0x00) / 2), Math.floor((255 + 0x00) / 2), 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [Math.floor((255 + 0x4e) / 2), Math.floor((255 + 0x9a) / 2), Math.floor((255 + 0x06) / 2), 255]); + await pollFor(ctx.page, () => getCellColor(4, 1), [Math.floor((255 + 0xc4) / 2), Math.floor((255 + 0xa0) / 2), Math.floor((255 + 0x00) / 2), 255]); + await pollFor(ctx.page, () => getCellColor(5, 1), [Math.floor((255 + 0x34) / 2), Math.floor((255 + 0x65) / 2), Math.floor((255 + 0xa4) / 2), 255]); + await pollFor(ctx.page, () => getCellColor(6, 1), [Math.floor((255 + 0x75) / 2), Math.floor((255 + 0x50) / 2), Math.floor((255 + 0x7b) / 2), 255]); + await pollFor(ctx.page, () => getCellColor(7, 1), [Math.floor((255 + 0x06) / 2), Math.floor((255 + 0x98) / 2), Math.floor((255 + 0x9a) / 2), 255]); + await pollFor(ctx.page, () => getCellColor(8, 1), [Math.floor((255 + 0xd3) / 2), Math.floor((255 + 0xd7) / 2), Math.floor((255 + 0xcf) / 2), 255]); + await pollFor(ctx.page, () => getCellColor(1, 2), [Math.floor((255 + 0x55) / 2), Math.floor((255 + 0x57) / 2), Math.floor((255 + 0x53) / 2), 255]); + await pollFor(ctx.page, () => getCellColor(2, 2), [Math.floor((255 + 0xef) / 2), Math.floor((255 + 0x29) / 2), Math.floor((255 + 0x29) / 2), 255]); + await pollFor(ctx.page, () => getCellColor(3, 2), [Math.floor((255 + 0x8a) / 2), Math.floor((255 + 0xe2) / 2), Math.floor((255 + 0x34) / 2), 255]); + await pollFor(ctx.page, () => getCellColor(4, 2), [Math.floor((255 + 0xfc) / 2), Math.floor((255 + 0xe9) / 2), Math.floor((255 + 0x4f) / 2), 255]); + await pollFor(ctx.page, () => getCellColor(5, 2), [Math.floor((255 + 0x72) / 2), Math.floor((255 + 0x9f) / 2), Math.floor((255 + 0xcf) / 2), 255]); + await pollFor(ctx.page, () => getCellColor(6, 2), [Math.floor((255 + 0xad) / 2), Math.floor((255 + 0x7f) / 2), Math.floor((255 + 0xa8) / 2), 255]); + await pollFor(ctx.page, () => getCellColor(7, 2), [Math.floor((255 + 0x34) / 2), Math.floor((255 + 0xe2) / 2), Math.floor((255 + 0xe2) / 2), 255]); + await pollFor(ctx.page, () => getCellColor(8, 2), [Math.floor((255 + 0xee) / 2), Math.floor((255 + 0xee) / 2), Math.floor((255 + 0xec) / 2), 255]); + // Setting and check for minimum contrast values, note that these are not + // exact to the contrast ratio, if the increase luminance algorithm + // changes then these will probably fail + await ctx.page.evaluate(`window.term.options.minimumContrastRatio = 10;`); + await pollFor(ctx.page, () => getCellColor(1, 1), [150, 153, 154, 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [229, 127, 127, 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [63, 124, 4, 255]); + await pollFor(ctx.page, () => getCellColor(4, 1), [127, 104, 0, 255]); + await pollFor(ctx.page, () => getCellColor(5, 1), [153, 178, 209, 255]); + await pollFor(ctx.page, () => getCellColor(6, 1), [186, 167, 189, 255]); + await pollFor(ctx.page, () => getCellColor(7, 1), [4, 122, 124, 255]); + await pollFor(ctx.page, () => getCellColor(8, 1), [110, 112, 108, 255]); + await pollFor(ctx.page, () => getCellColor(1, 2), [170, 171, 169, 255]); + await pollFor(ctx.page, () => getCellColor(2, 2), [215, 36, 36, 255]); + await pollFor(ctx.page, () => getCellColor(3, 2), [72, 117, 25, 255]); + await pollFor(ctx.page, () => getCellColor(4, 2), [117, 109, 36, 255]); + await pollFor(ctx.page, () => getCellColor(5, 2), [72, 103, 135, 255]); + await pollFor(ctx.page, () => getCellColor(6, 2), [125, 91, 121, 255]); + await pollFor(ctx.page, () => getCellColor(7, 2), [25, 117, 117, 255]); + await pollFor(ctx.page, () => getCellColor(8, 2), [111, 111, 110, 255]); + }); + }); + + test.describe.skip('selectionBackground', async () => { + test('should resolve the inverse foreground color based on the original background color, not the selection', async () => { + const theme: ITheme = { + foreground: '#FF0000', + background: '#00FF00', + selectionBackground: '#0000FF' + }; + await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.proxy.write( ` █\x1b[7m█\x1b[0m`); + await pollFor(ctx.page, () => getCellColor(1, 1), [0, 255, 0, 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [255, 0, 0, 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [0, 255, 0, 255]); + await ctx.page.evaluate(`window.term.selectAll()`); + // Selection only cell needs to be first to ensure renderer has kicked in + await pollFor(ctx.page, () => getCellColor(1, 1), [0, 0, 255, 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [255, 0, 0, 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [0, 255, 0, 255]); + }); + }); + + test.describe('allowTransparency', async () => { + test.beforeEach(() => ctx.page.evaluate(`term.options.allowTransparency = true`)); + + test('transparent background inverse', async () => { + const theme: ITheme = { + background: '#ff000080' + }; + await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + const data = `\x1b[7m█\x1b[0m`; + await ctx.proxy.write( data); + // Inverse background should be opaque + await pollFor(ctx.page, () => getCellColor(1, 1), [255, 0, 0, 255]); + }); + }); + + test.describe('selectionForeground', () => { + test('transparent background inverse', async () => { + const theme: ITheme = { + selectionForeground: '#ff0000' + }; + await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + const data = `\x1b[7m█\x1b[0m`; + await ctx.proxy.write( data); + await ctx.page.evaluate(`window.term.selectAll()`); + await pollFor(ctx.page, () => getCellColor(1, 1), [255, 0, 0, 255]); + }); + }); + + test.describe('decoration color overrides', async () => { + test('foregroundColor', async () => { + await ctx.page.evaluate(` + const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); + window.term.registerDecoration({ + marker, + foregroundColor: '#ff0000', + backgroundColor: '#0000ff' + }); + `); + const data = `█`; + await ctx.proxy.write( data); + await pollFor(ctx.page, () => getCellColor(1, 1), [255, 0, 0, 255]); + }); + test('foregroundColor should ignore inverse', async () => { + await ctx.page.evaluate(` + const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); + window.term.registerDecoration({ + marker, + foregroundColor: '#ff0000', + backgroundColor: '#0000ff' + }); + `); + const data = `\x1b[7m█\x1b[0m`; + await ctx.proxy.write( data); + await pollFor(ctx.page, () => getCellColor(1, 1), [255, 0, 0, 255]); + }); + test('foregroundColor should ignore inverse (only fg on decoration)', async () => { + await ctx.page.evaluate(` + const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); + window.term.registerDecoration({ + marker, + width: 2, + foregroundColor: '#ff0000' + }); + `); + const data = `\x1b[7m█ \x1b[0m`; + await ctx.proxy.write( data); + await pollFor(ctx.page, () => getCellColor(1, 1), [255, 0, 0, 255]); // inverse foreground of '█' should be decoration fg override + await pollFor(ctx.page, () => getCellColor(2, 1), [255, 255, 255, 255]); // inverse background of ' ' should be default foreground + }); + test('backgroundColor', async () => { + await ctx.page.evaluate(` + const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); + window.term.registerDecoration({ + marker, + foregroundColor: '#ff0000', + backgroundColor: '#0000ff' + }); + `); + const data = ` `; + await ctx.proxy.write( data); + await pollFor(ctx.page, () => getCellColor(1, 1), [0, 0, 255, 255]); + }); + test('backgroundColor should ignore inverse', async () => { + await ctx.page.evaluate(` + const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); + window.term.registerDecoration({ + marker, + foregroundColor: '#ff0000', + backgroundColor: '#0000ff' + }); + `); + const data = `\x1b[7m \x1b[0m`; + await ctx.proxy.write( data); + await pollFor(ctx.page, () => getCellColor(1, 1), [0, 0, 255, 255]); + }); + test('backgroundColor should ignore inverse (only bg on decoration)', async () => { + const data = `\x1b[7m█ \x1b[0m`; + await ctx.proxy.write( data); + await ctx.page.evaluate(` + const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); + window.term.registerDecoration({ + marker, + width: 2, + backgroundColor: '#0000ff' + }); + `); + await pollFor(ctx.page, () => getCellColor(1, 1), [0, 0, 0, 255]); // inverse foreground of '█' should be default + await pollFor(ctx.page, () => getCellColor(2, 1), [0, 0, 255, 255]); // inverse background of ' ' should be decoration bg override + }); + }); }); /** From b543842fc01eb5a4cb3e0b6b60b6f6a4a3bad427 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 08:23:20 -0700 Subject: [PATCH 50/99] Fix dom MCR tests --- .../xterm-addon-webgl/test/WebglRenderer.api.ts | 6 +++--- test/playwright/Renderer.test.ts | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts index 69fd4ffd..c41538cd 100644 --- a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts @@ -767,7 +767,7 @@ describe('WebGL Renderer Integration Tests', async () => { window.term.options.theme = ${JSON.stringify(theme)}; window.term.options.minimumContrastRatio = 1; `); - // Block characters ignore block elements so a different char is used here + // Block elements ignore minimum contrast ratio so a different char is used here await writeSync(page, `\\x1b[30m■\\x1b[31m■\\x1b[32m■\\x1b[33m■\\x1b[34m■\\x1b[35m■\\x1b[36m■\\x1b[37m■\\r\\n` + `\\x1b[90m■\\x1b[91m■\\x1b[92m■\\x1b[93m■\\x1b[94m■\\x1b[95m■\\x1b[96m■\\x1b[97m■` @@ -836,7 +836,7 @@ describe('WebGL Renderer Integration Tests', async () => { window.term.options.theme = ${JSON.stringify(theme)}; window.term.options.minimumContrastRatio = 1; `); - // Block characters ignore block elements so a different char is used here + // Block elements ignore minimum contrast ratio so a different char is used here await writeSync(page, `\\x1b[30m■\\x1b[31m■\\x1b[32m■\\x1b[33m■\\x1b[34m■\\x1b[35m■\\x1b[36m■\\x1b[37m■\\r\\n` + `\\x1b[90m■\\x1b[91m■\\x1b[92m■\\x1b[93m■\\x1b[94m■\\x1b[95m■\\x1b[96m■\\x1b[97m■` @@ -904,7 +904,7 @@ describe('WebGL Renderer Integration Tests', async () => { window.term.options.theme = ${JSON.stringify(theme)}; window.term.options.minimumContrastRatio = 1; `); - // Block characters ignore block elements so a different char is used here + // Block elements ignore minimum contrast ratio so a different char is used here await writeSync(page, '\\x1b[2m' + `\\x1b[30m■\\x1b[31m■\\x1b[32m■\\x1b[33m■\\x1b[34m■\\x1b[35m■\\x1b[36m■\\x1b[37m■\\r\\n` + diff --git a/test/playwright/Renderer.test.ts b/test/playwright/Renderer.test.ts index e1f40213..e5ae45a3 100644 --- a/test/playwright/Renderer.test.ts +++ b/test/playwright/Renderer.test.ts @@ -6,7 +6,7 @@ import { IImage32, decodePng } from '@lunapaint/png-codec'; import { LocatorScreenshotOptions, test } from '@playwright/test'; import { ITheme } from 'xterm'; -import { createTestContext, ITestContext, MaybeAsync, openTerminal, pollFor } from './TestUtils'; +import { createTestContext, ITestContext, MaybeAsync, openTerminal, pollFor, timeout } from './TestUtils'; let ctx: ITestContext; test.beforeAll(async ({ browser }) => { @@ -16,9 +16,6 @@ test.beforeAll(async ({ browser }) => { test.afterAll(async () => await ctx.page.close()); test.describe.only('WebGL Renderer Integration Tests', async () => { - // TODO: Speed up tests - test.setTimeout(30000); - test.beforeEach(async () => { await ctx.proxy.reset(); // Clear the cached screenshot before each test @@ -725,7 +722,7 @@ test.describe.only('WebGL Renderer Integration Tests', async () => { } }); - test.describe.skip('minimumContrastRatio', async () => { + test.describe.only('minimumContrastRatio', async () => { test('should adjust 0-15 colors on black background', async () => { const theme: ITheme = { black: '#2e3436', @@ -749,7 +746,7 @@ test.describe.only('WebGL Renderer Integration Tests', async () => { window.term.options.theme = ${JSON.stringify(theme)}; window.term.options.minimumContrastRatio = 1; `); - // Block characters ignore block elements so a different char is used here + // Block elements ignore minimum contrast ratio so a different char is used here await ctx.proxy.write( `\x1b[30m■\x1b[31m■\x1b[32m■\x1b[33m■\x1b[34m■\x1b[35m■\x1b[36m■\x1b[37m■\r\n` + `\x1b[90m■\x1b[91m■\x1b[92m■\x1b[93m■\x1b[94m■\x1b[95m■\x1b[96m■\x1b[97m■` @@ -775,6 +772,7 @@ test.describe.only('WebGL Renderer Integration Tests', async () => { // exact to the contrast ratio, if the increase luminance algorithm // changes then these will probably fail await ctx.page.evaluate(`window.term.options.minimumContrastRatio = 10;`); + frameDetails = undefined; await pollFor(ctx.page, () => getCellColor(1, 1), [176, 180, 180, 255]); await pollFor(ctx.page, () => getCellColor(2, 1), [238, 158, 158, 255]); await pollFor(ctx.page, () => getCellColor(3, 1), [152, 198, 110, 255]); @@ -818,7 +816,7 @@ test.describe.only('WebGL Renderer Integration Tests', async () => { window.term.options.theme = ${JSON.stringify(theme)}; window.term.options.minimumContrastRatio = 1; `); - // Block characters ignore block elements so a different char is used here + // Block elements ignore minimum contrast ratio so a different char is used here await ctx.proxy.write( `\x1b[30m■\x1b[31m■\x1b[32m■\x1b[33m■\x1b[34m■\x1b[35m■\x1b[36m■\x1b[37m■\r\n` + `\x1b[90m■\x1b[91m■\x1b[92m■\x1b[93m■\x1b[94m■\x1b[95m■\x1b[96m■\x1b[97m■` @@ -844,6 +842,7 @@ test.describe.only('WebGL Renderer Integration Tests', async () => { // exact to the contrast ratio, if the increase luminance algorithm // changes then these will probably fail await ctx.page.evaluate(`window.term.options.minimumContrastRatio = 10;`); + frameDetails = undefined; await pollFor(ctx.page, () => getCellColor(1, 1), [46, 52, 54, 255]); await pollFor(ctx.page, () => getCellColor(2, 1), [132, 0, 0, 255]); await pollFor(ctx.page, () => getCellColor(3, 1), [36, 72, 0, 255]); @@ -886,7 +885,7 @@ test.describe.only('WebGL Renderer Integration Tests', async () => { window.term.options.theme = ${JSON.stringify(theme)}; window.term.options.minimumContrastRatio = 1; `); - // Block characters ignore block elements so a different char is used here + // Block elements ignore minimum contrast ratio so a different char is used here await ctx.proxy.write( '\x1b[2m' + `\x1b[30m■\x1b[31m■\x1b[32m■\x1b[33m■\x1b[34m■\x1b[35m■\x1b[36m■\x1b[37m■\r\n` + @@ -913,6 +912,7 @@ test.describe.only('WebGL Renderer Integration Tests', async () => { // exact to the contrast ratio, if the increase luminance algorithm // changes then these will probably fail await ctx.page.evaluate(`window.term.options.minimumContrastRatio = 10;`); + frameDetails = undefined; await pollFor(ctx.page, () => getCellColor(1, 1), [150, 153, 154, 255]); await pollFor(ctx.page, () => getCellColor(2, 1), [229, 127, 127, 255]); await pollFor(ctx.page, () => getCellColor(3, 1), [63, 124, 4, 255]); From db608e33fb8cd90bb66b93eadca6985166bb6aa8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 08:26:49 -0700 Subject: [PATCH 51/99] Fix remaining dom renderer tests --- test/playwright/Renderer.test.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/playwright/Renderer.test.ts b/test/playwright/Renderer.test.ts index e5ae45a3..692f0c32 100644 --- a/test/playwright/Renderer.test.ts +++ b/test/playwright/Renderer.test.ts @@ -15,9 +15,13 @@ test.beforeAll(async ({ browser }) => { }); test.afterAll(async () => await ctx.page.close()); -test.describe.only('WebGL Renderer Integration Tests', async () => { +test.describe.only('DOM Renderer Integration Tests', async () => { test.beforeEach(async () => { await ctx.proxy.reset(); + ctx.page.evaluate([ + `window.term.options.minimumContrastRatio = 1`, + `window.term.options.allowTransparency = false` + ].join('\n')); // Clear the cached screenshot before each test frameDetails = undefined; }); @@ -722,7 +726,7 @@ test.describe.only('WebGL Renderer Integration Tests', async () => { } }); - test.describe.only('minimumContrastRatio', async () => { + test.describe('minimumContrastRatio', async () => { test('should adjust 0-15 colors on black background', async () => { const theme: ITheme = { black: '#2e3436', @@ -932,7 +936,7 @@ test.describe.only('WebGL Renderer Integration Tests', async () => { }); }); - test.describe.skip('selectionBackground', async () => { + test.describe('selectionBackground', async () => { test('should resolve the inverse foreground color based on the original background color, not the selection', async () => { const theme: ITheme = { foreground: '#FF0000', @@ -945,6 +949,7 @@ test.describe.only('WebGL Renderer Integration Tests', async () => { await pollFor(ctx.page, () => getCellColor(2, 1), [255, 0, 0, 255]); await pollFor(ctx.page, () => getCellColor(3, 1), [0, 255, 0, 255]); await ctx.page.evaluate(`window.term.selectAll()`); + frameDetails = undefined; // Selection only cell needs to be first to ensure renderer has kicked in await pollFor(ctx.page, () => getCellColor(1, 1), [0, 0, 255, 255]); await pollFor(ctx.page, () => getCellColor(2, 1), [255, 0, 0, 255]); From e54755b074524a69a1f7fb3aca49948272c4ba85 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 08:37:06 -0700 Subject: [PATCH 52/99] Skip failure on WebKit --- test/playwright/Renderer.test.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/test/playwright/Renderer.test.ts b/test/playwright/Renderer.test.ts index 692f0c32..f407dc5d 100644 --- a/test/playwright/Renderer.test.ts +++ b/test/playwright/Renderer.test.ts @@ -6,7 +6,7 @@ import { IImage32, decodePng } from '@lunapaint/png-codec'; import { LocatorScreenshotOptions, test } from '@playwright/test'; import { ITheme } from 'xterm'; -import { createTestContext, ITestContext, MaybeAsync, openTerminal, pollFor, timeout } from './TestUtils'; +import { createTestContext, ITestContext, MaybeAsync, openTerminal, pollFor } from './TestUtils'; let ctx: ITestContext; test.beforeAll(async ({ browser }) => { @@ -15,13 +15,13 @@ test.beforeAll(async ({ browser }) => { }); test.afterAll(async () => await ctx.page.close()); -test.describe.only('DOM Renderer Integration Tests', async () => { +test.describe('DOM Renderer Integration Tests', async () => { test.beforeEach(async () => { await ctx.proxy.reset(); - ctx.page.evaluate([ - `window.term.options.minimumContrastRatio = 1`, - `window.term.options.allowTransparency = false` - ].join('\n')); + ctx.page.evaluate(` + window.term.options.minimumContrastRatio = 1; + window.term.options.allowTransparency = false; + `); // Clear the cached screenshot before each test frameDetails = undefined; }); @@ -866,6 +866,11 @@ test.describe.only('DOM Renderer Integration Tests', async () => { }); test('should enforce half the contrast for dim cells', async () => { + // TODO: This test fails on webkit + if (ctx.browser.browserType().name() === 'webkit') { + test.skip(); + return; + } const theme: ITheme = { background: '#ffffff', black: '#2e3436', From 070d194c6c144cf234d82401c169d3c0e87ada5d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 10:04:25 -0700 Subject: [PATCH 53/99] Migrate webgl to playwright/test, add unified runner script --- .../test/WebglRenderer.api.ts | 1163 ----------------- .../test/WebglRenderer.test.ts | 1133 ++++++++++++++++ .../test/playwright.config.ts | 35 + addons/xterm-addon-webgl/test/tsconfig.json | 3 +- bin/test_playwright.js | 34 + package.json | 10 +- test/playwright/TestUtils.ts | 11 +- 7 files changed, 1216 insertions(+), 1173 deletions(-) delete mode 100644 addons/xterm-addon-webgl/test/WebglRenderer.api.ts create mode 100644 addons/xterm-addon-webgl/test/WebglRenderer.test.ts create mode 100644 addons/xterm-addon-webgl/test/playwright.config.ts create mode 100644 bin/test_playwright.js diff --git a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts deleted file mode 100644 index c41538cd..00000000 --- a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts +++ /dev/null @@ -1,1163 +0,0 @@ -/** - * Copyright (c) 2019 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { assert } from 'chai'; -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'; - -const APP = 'http://127.0.0.1:3001/test'; - -let browser: Browser; -let page: Page; -const width = 800; -const height = 600; - -describe('WebGL Renderer Integration Tests', async () => { - const browserType = getBrowserType(); - const isHeadless = process.argv.includes('--headless'); - // Firefox works only in non-headless mode https://github.com/microsoft/playwright/issues/1032 - const areTestsEnabled = browserType.name() === 'chromium' || (browserType.name() === 'firefox' && !isHeadless); - const itWebgl = areTestsEnabled ? it : it.skip; - - itWebgl('dispose removes renderer canvases', async function(): Promise { - await setupBrowser(); - assert.equal(await page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 2); - await page.evaluate(`addon.dispose()`); - assert.equal(await page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 0); - await browser.close(); - }); - - describe('colors', () => { - if (areTestsEnabled) { - before(async () => setupBrowser()); - after(async () => browser.close()); - beforeEach(async () => page.evaluate(`window.term.reset()`)); - } - - itWebgl('foreground 0-15', async () => { - const theme: ITheme = { - black: '#010203', - red: '#040506', - green: '#070809', - yellow: '#0a0b0c', - blue: '#0d0e0f', - magenta: '#101112', - cyan: '#131415', - white: '#161718' - }; - await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await writeSync(page, `\\x1b[30m█\\x1b[31m█\\x1b[32m█\\x1b[33m█\\x1b[34m█\\x1b[35m█\\x1b[36m█\\x1b[37m█`); - await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]); - await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]); - await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]); - await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]); - await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]); - await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]); - await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]); - await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]); - }); - - itWebgl('foreground 0-7 drawBoldTextInBrightColors', async () => { - const theme: ITheme = { - brightBlack: '#010203', - brightRed: '#040506', - brightGreen: '#070809', - brightYellow: '#0a0b0c', - brightBlue: '#0d0e0f', - brightMagenta: '#101112', - brightCyan: '#131415', - brightWhite: '#161718' - }; - await page.evaluate(` - window.term.options.theme = ${JSON.stringify(theme)}; - window.term.options.drawBoldTextInBrightColors = true; - `); - await writeSync(page, `\\x1b[1;30m█\\x1b[1;31m█\\x1b[1;32m█\\x1b[1;33m█\\x1b[1;34m█\\x1b[1;35m█\\x1b[1;36m█\\x1b[1;37m█`); - await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]); - await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]); - await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]); - await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]); - await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]); - await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]); - await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]); - await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]); - }); - - itWebgl('background 0-15', async () => { - const theme: ITheme = { - black: '#010203', - red: '#040506', - green: '#070809', - yellow: '#0a0b0c', - blue: '#0d0e0f', - magenta: '#101112', - cyan: '#131415', - white: '#161718' - }; - await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await writeSync(page, `\\x1b[40m \\x1b[41m \\x1b[42m \\x1b[43m \\x1b[44m \\x1b[45m \\x1b[46m \\x1b[47m `); - await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]); - await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]); - await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]); - await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]); - await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]); - await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]); - await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]); - await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]); - }); - - itWebgl('foreground 0-15 inverse', async () => { - const theme: ITheme = { - black: '#010203', - red: '#040506', - green: '#070809', - yellow: '#0a0b0c', - blue: '#0d0e0f', - magenta: '#101112', - cyan: '#131415', - white: '#161718' - }; - await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await writeSync(page, `\\x1b[7;30m \\x1b[7;31m \\x1b[7;32m \\x1b[7;33m \\x1b[7;34m \\x1b[7;35m \\x1b[7;36m \\x1b[7;37m `); - await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]); - await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]); - await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]); - await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]); - await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]); - await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]); - await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]); - await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]); - }); - - itWebgl('background 0-15 inverse', async () => { - const theme: ITheme = { - black: '#010203', - red: '#040506', - green: '#070809', - yellow: '#0a0b0c', - blue: '#0d0e0f', - magenta: '#101112', - cyan: '#131415', - white: '#161718' - }; - await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await writeSync(page, `\\x1b[7;40m█\\x1b[7;41m█\\x1b[7;42m█\\x1b[7;43m█\\x1b[7;44m█\\x1b[7;45m█\\x1b[7;46m█\\x1b[7;47m█`); - await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]); - await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]); - await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]); - await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]); - await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]); - await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]); - await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]); - await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]); - }); - - itWebgl('foreground 0-15 inivisible', async () => { - const theme: ITheme = { - black: '#010203', - red: '#040506', - green: '#070809', - yellow: '#0a0b0c', - blue: '#0d0e0f', - magenta: '#101112', - cyan: '#131415', - white: '#161718' - }; - await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await writeSync(page, `\\x1b[8;30m \\x1b[8;31m \\x1b[8;32m \\x1b[8;33m \\x1b[8;34m \\x1b[8;35m \\x1b[8;36m \\x1b[8;37m `); - await pollFor(page, () => getCellColor(1, 1), [0, 0, 0, 255]); - await pollFor(page, () => getCellColor(2, 1), [0, 0, 0, 255]); - await pollFor(page, () => getCellColor(3, 1), [0, 0, 0, 255]); - await pollFor(page, () => getCellColor(4, 1), [0, 0, 0, 255]); - await pollFor(page, () => getCellColor(5, 1), [0, 0, 0, 255]); - await pollFor(page, () => getCellColor(6, 1), [0, 0, 0, 255]); - await pollFor(page, () => getCellColor(7, 1), [0, 0, 0, 255]); - await pollFor(page, () => getCellColor(8, 1), [0, 0, 0, 255]); - }); - - itWebgl('background 0-15 inivisible', async () => { - const theme: ITheme = { - black: '#010203', - red: '#040506', - green: '#070809', - yellow: '#0a0b0c', - blue: '#0d0e0f', - magenta: '#101112', - cyan: '#131415', - white: '#161718' - }; - await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await writeSync(page, `\\x1b[8;40m█\\x1b[8;41m█\\x1b[8;42m█\\x1b[8;43m█\\x1b[8;44m█\\x1b[8;45m█\\x1b[8;46m█\\x1b[8;47m█`); - await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]); - await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]); - await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]); - await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]); - await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]); - await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]); - await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]); - await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]); - }); - - itWebgl('foreground 0-15 bright', async () => { - const theme: ITheme = { - brightBlack: '#010203', - brightRed: '#040506', - brightGreen: '#070809', - brightYellow: '#0a0b0c', - brightBlue: '#0d0e0f', - brightMagenta: '#101112', - brightCyan: '#131415', - brightWhite: '#161718' - }; - await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await writeSync(page, `\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█`); - await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]); - await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]); - await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]); - await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]); - await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]); - await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]); - await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]); - await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]); - }); - - itWebgl('background 0-15 bright', async () => { - const theme: ITheme = { - brightBlack: '#010203', - brightRed: '#040506', - brightGreen: '#070809', - brightYellow: '#0a0b0c', - brightBlue: '#0d0e0f', - brightMagenta: '#101112', - brightCyan: '#131415', - brightWhite: '#161718' - }; - await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await writeSync(page, `\\x1b[100m \\x1b[101m \\x1b[102m \\x1b[103m \\x1b[104m \\x1b[105m \\x1b[106m \\x1b[107m `); - await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]); - await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]); - await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]); - await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]); - await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]); - await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]); - await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]); - await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]); - }); - - itWebgl('foreground 16-255', async () => { - let data = ''; - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - data += `\\x1b[38;5;${16 + y * 16 + x}m█\\x1b[0m`; - } - data += '\\r\\n'; - } - await writeSync(page, data); - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - const cssColor = COLORS_16_TO_255[y * 16 + x]; - const r = parseInt(cssColor.slice(1, 3), 16); - const g = parseInt(cssColor.slice(3, 5), 16); - const b = parseInt(cssColor.slice(5, 7), 16); - await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); - } - } - }); - - itWebgl('background 16-255', async () => { - let data = ''; - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - data += `\\x1b[48;5;${16 + y * 16 + x}m \x1b[0m`; - } - data += '\\r\\n'; - } - await writeSync(page, data); - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - const cssColor = COLORS_16_TO_255[y * 16 + x]; - const r = parseInt(cssColor.slice(1, 3), 16); - const g = parseInt(cssColor.slice(3, 5), 16); - const b = parseInt(cssColor.slice(5, 7), 16); - await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); - } - } - }); - - itWebgl('foreground 16-255 inverse', async () => { - let data = ''; - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - data += `\\x1b[7;38;5;${16 + y * 16 + x}m \x1b[0m`; - } - data += '\\r\\n'; - } - await writeSync(page, data); - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - const cssColor = COLORS_16_TO_255[y * 16 + x]; - const r = parseInt(cssColor.slice(1, 3), 16); - const g = parseInt(cssColor.slice(3, 5), 16); - const b = parseInt(cssColor.slice(5, 7), 16); - await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); - } - } - }); - - itWebgl('background 16-255 inverse', async () => { - let data = ''; - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - data += `\\x1b[7;48;5;${16 + y * 16 + x}m█\\x1b[0m`; - } - data += '\\r\\n'; - } - await writeSync(page, data); - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - const cssColor = COLORS_16_TO_255[y * 16 + x]; - const r = parseInt(cssColor.slice(1, 3), 16); - const g = parseInt(cssColor.slice(3, 5), 16); - const b = parseInt(cssColor.slice(5, 7), 16); - await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); - } - } - }); - - itWebgl('foreground 16-255 invisible', async () => { - let data = ''; - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - data += `\\x1b[8;38;5;${16 + y * 16 + x}m \x1b[0m`; - } - data += '\\r\\n'; - } - await writeSync(page, data); - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - await pollFor(page, () => getCellColor(x + 1, y + 1), [0, 0, 0, 255]); - } - } - }); - - itWebgl('background 16-255 invisible', async () => { - let data = ''; - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - data += `\\x1b[8;48;5;${16 + y * 16 + x}m█\\x1b[0m`; - } - data += '\\r\\n'; - } - await writeSync(page, data); - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - const cssColor = COLORS_16_TO_255[y * 16 + x]; - const r = parseInt(cssColor.slice(1, 3), 16); - const g = parseInt(cssColor.slice(3, 5), 16); - const b = parseInt(cssColor.slice(5, 7), 16); - await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); - } - } - }); - - itWebgl('foreground 16-255 dim', async () => { - let data = ''; - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - data += `\\x1b[2;38;5;${16 + y * 16 + x}m█\x1b[0m`; - } - data += '\\r\\n'; - } - await writeSync(page, data); - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - const cssColor = COLORS_16_TO_255[y * 16 + x]; - const r = parseInt(cssColor.slice(1, 3), 16); - const g = parseInt(cssColor.slice(3, 5), 16); - const b = parseInt(cssColor.slice(5, 7), 16); - // It's difficult to assert the exact color due to rounding, just ensure the color differs - // to the regular color - await pollFor(page, async () => { - const c = await getCellColor(x + 1, y + 1); - return ( - (c[0] === 0 || c[0] !== r) && - (c[1] === 0 || c[1] !== g) && - (c[2] === 0 || c[2] !== b) - ); - }, true); - } - } - }); - - itWebgl('background 16-255 dim', async () => { - let data = ''; - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - data += `\\x1b[2;48;5;${16 + y * 16 + x}m \\x1b[0m`; - } - data += '\\r\\n'; - } - await writeSync(page, data); - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - const cssColor = COLORS_16_TO_255[y * 16 + x]; - const r = parseInt(cssColor.slice(1, 3), 16); - const g = parseInt(cssColor.slice(3, 5), 16); - const b = parseInt(cssColor.slice(5, 7), 16); - await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); - } - } - }); - - itWebgl('foreground true color red', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\\x1b[38;2;${i};0;0m█\\x1b[0m`; - } - data += '\\r\\n'; - } - await writeSync(page, data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]); - } - } - }); - - itWebgl('background true color red', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\\x1b[48;2;${i};0;0m \x1b[0m`; - } - data += '\\r\\n'; - } - await writeSync(page, data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]); - } - } - }); - - itWebgl('foreground true color green', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\\x1b[38;2;0;${i};0m█\\x1b[0m`; - } - data += '\\r\\n'; - } - await writeSync(page, data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]); - } - } - }); - - itWebgl('background true color green', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\\x1b[48;2;0;${i};0m \x1b[0m`; - } - data += '\\r\\n'; - } - await writeSync(page, data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]); - } - } - }); - - itWebgl('foreground true color blue', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\\x1b[38;2;0;0;${i}m█\\x1b[0m`; - } - data += '\\r\\n'; - } - await writeSync(page, data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]); - } - } - }); - - itWebgl('background true color blue', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\\x1b[48;2;0;0;${i}m \x1b[0m`; - } - data += '\\r\\n'; - } - await writeSync(page, data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]); - } - } - }); - - itWebgl('foreground true color grey', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\\x1b[38;2;${i};${i};${i}m█\\x1b[0m`; - } - data += '\\r\\n'; - } - await writeSync(page, data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); - } - } - }); - - itWebgl('background true color grey', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\\x1b[48;2;${i};${i};${i}m \x1b[0m`; - } - data += '\\r\\n'; - } - await writeSync(page, data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); - } - } - }); - - itWebgl('foreground true color red inverse', async function(): Promise { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[7;38;2;${i};0;0m \x1b[0m`; - } - data += '\\r\\n'; - } - await writeSync(page, data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]); - } - } - }); - - itWebgl('background true color red inverse', async function(): Promise { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\\x1b[7;48;2;${i};0;0m█\\x1b[0m`; - } - data += '\\r\\n'; - } - await writeSync(page, data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]); - } - } - }); - - itWebgl('foreground true color green inverse', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\\x1b[7;38;2;0;${i};0m \x1b[0m`; - } - data += '\\r\\n'; - } - await writeSync(page, data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]); - } - } - }); - - itWebgl('background true color green inverse', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\\x1b[7;48;2;0;${i};0m█\\x1b[0m`; - } - data += '\\r\\n'; - } - await writeSync(page, data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]); - } - } - }); - - itWebgl('foreground true color blue inverse', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\\x1b[7;38;2;0;0;${i}m \x1b[0m`; - } - data += '\\r\\n'; - } - await writeSync(page, data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]); - } - } - }); - - itWebgl('background true color blue inverse', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\\x1b[7;48;2;0;0;${i}m█\\x1b[0m`; - } - data += '\\r\\n'; - } - await writeSync(page, data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]); - } - } - }); - - itWebgl('foreground true color grey inverse', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\\x1b[7;38;2;${i};${i};${i}m \x1b[0m`; - } - data += '\\r\\n'; - } - await writeSync(page, data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); - } - } - }); - - itWebgl('background true color grey inverse', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\\x1b[7;48;2;${i};${i};${i}m█\\x1b[0m`; - } - data += '\\r\\n'; - } - await writeSync(page, data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); - } - } - }); - - itWebgl('foreground true color grey invisible', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\\x1b[8;38;2;${i};${i};${i}m \x1b[0m`; - } - data += '\\r\\n'; - } - await writeSync(page, data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - await pollFor(page, () => getCellColor(x + 1, y + 1), [0, 0, 0, 255]); - } - } - }); - - itWebgl('background true color grey invisible', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\\x1b[8;48;2;${i};${i};${i}m█\\x1b[0m`; - } - data += '\\r\\n'; - } - await writeSync(page, data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); - } - } - }); - }); - - describe('minimumContrastRatio', async () => { - if (areTestsEnabled) { - before(async () => setupBrowser()); - after(async () => browser.close()); - beforeEach(async () => page.evaluate(`window.term.reset()`)); - } - - itWebgl('should adjust 0-15 colors on black background', async () => { - const theme: ITheme = { - black: '#2e3436', - red: '#cc0000', - green: '#4e9a06', - yellow: '#c4a000', - blue: '#3465a4', - magenta: '#75507b', - cyan: '#06989a', - white: '#d3d7cf', - brightBlack: '#555753', - brightRed: '#ef2929', - brightGreen: '#8ae234', - brightYellow: '#fce94f', - brightBlue: '#729fcf', - brightMagenta: '#ad7fa8', - brightCyan: '#34e2e2', - brightWhite: '#eeeeec' - }; - await page.evaluate(` - window.term.options.theme = ${JSON.stringify(theme)}; - window.term.options.minimumContrastRatio = 1; - `); - // Block elements ignore minimum contrast ratio so a different char is used here - await writeSync(page, - `\\x1b[30m■\\x1b[31m■\\x1b[32m■\\x1b[33m■\\x1b[34m■\\x1b[35m■\\x1b[36m■\\x1b[37m■\\r\\n` + - `\\x1b[90m■\\x1b[91m■\\x1b[92m■\\x1b[93m■\\x1b[94m■\\x1b[95m■\\x1b[96m■\\x1b[97m■` - ); - // Validate before minimumContrastRatio is applied - await pollFor(page, () => getCellColor(1, 1), [0x2e, 0x34, 0x36, 255]); - await pollFor(page, () => getCellColor(2, 1), [0xcc, 0x00, 0x00, 255]); - await pollFor(page, () => getCellColor(3, 1), [0x4e, 0x9a, 0x06, 255]); - await pollFor(page, () => getCellColor(4, 1), [0xc4, 0xa0, 0x00, 255]); - await pollFor(page, () => getCellColor(5, 1), [0x34, 0x65, 0xa4, 255]); - await pollFor(page, () => getCellColor(6, 1), [0x75, 0x50, 0x7b, 255]); - await pollFor(page, () => getCellColor(7, 1), [0x06, 0x98, 0x9a, 255]); - await pollFor(page, () => getCellColor(8, 1), [0xd3, 0xd7, 0xcf, 255]); - await pollFor(page, () => getCellColor(1, 2), [0x55, 0x57, 0x53, 255]); - await pollFor(page, () => getCellColor(2, 2), [0xef, 0x29, 0x29, 255]); - await pollFor(page, () => getCellColor(3, 2), [0x8a, 0xe2, 0x34, 255]); - await pollFor(page, () => getCellColor(4, 2), [0xfc, 0xe9, 0x4f, 255]); - await pollFor(page, () => getCellColor(5, 2), [0x72, 0x9f, 0xcf, 255]); - await pollFor(page, () => getCellColor(6, 2), [0xad, 0x7f, 0xa8, 255]); - await pollFor(page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]); - await pollFor(page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]); - // Setting and check for minimum contrast values, note that these are not - // exact to the contrast ratio, if the increase luminance algorithm - // changes then these will probably fail - await page.evaluate(`window.term.options.minimumContrastRatio = 10;`); - await pollFor(page, () => getCellColor(1, 1), [176, 180, 180, 255]); - await pollFor(page, () => getCellColor(2, 1), [238, 158, 158, 255]); - await pollFor(page, () => getCellColor(3, 1), [152, 198, 110, 255]); - await pollFor(page, () => getCellColor(4, 1), [208, 179, 49, 255]); - await pollFor(page, () => getCellColor(5, 1), [161, 183, 215, 255]); - await pollFor(page, () => getCellColor(6, 1), [191, 174, 194, 255]); - await pollFor(page, () => getCellColor(7, 1), [110, 197, 198, 255]); - await pollFor(page, () => getCellColor(8, 1), [211, 215, 207, 255]); - await pollFor(page, () => getCellColor(1, 2), [183, 185, 183, 255]); - await pollFor(page, () => getCellColor(2, 2), [249, 156, 156, 255]); - await pollFor(page, () => getCellColor(3, 2), [138, 226, 52, 255]); - await pollFor(page, () => getCellColor(4, 2), [252, 233, 79, 255]); - await pollFor(page, () => getCellColor(5, 2), [154, 186, 221, 255]); - await pollFor(page, () => getCellColor(6, 2), [203, 173, 199, 255]); - // Unchanged - await pollFor(page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]); - await pollFor(page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]); - }); - - itWebgl('should adjust 0-15 colors on white background', async () => { - const theme: ITheme = { - background: '#ffffff', - black: '#2e3436', - red: '#cc0000', - green: '#4e9a06', - yellow: '#c4a000', - blue: '#3465a4', - magenta: '#75507b', - cyan: '#06989a', - white: '#d3d7cf', - brightBlack: '#555753', - brightRed: '#ef2929', - brightGreen: '#8ae234', - brightYellow: '#fce94f', - brightBlue: '#729fcf', - brightMagenta: '#ad7fa8', - brightCyan: '#34e2e2', - brightWhite: '#eeeeec' - }; - await page.evaluate(` - window.term.options.theme = ${JSON.stringify(theme)}; - window.term.options.minimumContrastRatio = 1; - `); - // Block elements ignore minimum contrast ratio so a different char is used here - await writeSync(page, - `\\x1b[30m■\\x1b[31m■\\x1b[32m■\\x1b[33m■\\x1b[34m■\\x1b[35m■\\x1b[36m■\\x1b[37m■\\r\\n` + - `\\x1b[90m■\\x1b[91m■\\x1b[92m■\\x1b[93m■\\x1b[94m■\\x1b[95m■\\x1b[96m■\\x1b[97m■` - ); - // Validate before minimumContrastRatio is applied - await pollFor(page, () => getCellColor(1, 1), [0x2e, 0x34, 0x36, 255]); - await pollFor(page, () => getCellColor(2, 1), [0xcc, 0x00, 0x00, 255]); - await pollFor(page, () => getCellColor(3, 1), [0x4e, 0x9a, 0x06, 255]); - await pollFor(page, () => getCellColor(4, 1), [0xc4, 0xa0, 0x00, 255]); - await pollFor(page, () => getCellColor(5, 1), [0x34, 0x65, 0xa4, 255]); - await pollFor(page, () => getCellColor(6, 1), [0x75, 0x50, 0x7b, 255]); - await pollFor(page, () => getCellColor(7, 1), [0x06, 0x98, 0x9a, 255]); - await pollFor(page, () => getCellColor(8, 1), [0xd3, 0xd7, 0xcf, 255]); - await pollFor(page, () => getCellColor(1, 2), [0x55, 0x57, 0x53, 255]); - await pollFor(page, () => getCellColor(2, 2), [0xef, 0x29, 0x29, 255]); - await pollFor(page, () => getCellColor(3, 2), [0x8a, 0xe2, 0x34, 255]); - await pollFor(page, () => getCellColor(4, 2), [0xfc, 0xe9, 0x4f, 255]); - await pollFor(page, () => getCellColor(5, 2), [0x72, 0x9f, 0xcf, 255]); - await pollFor(page, () => getCellColor(6, 2), [0xad, 0x7f, 0xa8, 255]); - await pollFor(page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]); - await pollFor(page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]); - // Setting and check for minimum contrast values, note that these are not - // exact to the contrast ratio, if the increase luminance algorithm - // changes then these will probably fail - await page.evaluate(`window.term.options.minimumContrastRatio = 10;`); - await pollFor(page, () => getCellColor(1, 1), [46, 52, 54, 255]); - await pollFor(page, () => getCellColor(2, 1), [132, 0, 0, 255]); - await pollFor(page, () => getCellColor(3, 1), [36, 72, 0, 255]); - await pollFor(page, () => getCellColor(4, 1), [72, 59, 0, 255]); - await pollFor(page, () => getCellColor(5, 1), [32, 64, 106, 255]); - await pollFor(page, () => getCellColor(6, 1), [75, 51, 80, 255]); - await pollFor(page, () => getCellColor(7, 1), [0, 71, 72, 255]); - await pollFor(page, () => getCellColor(8, 1), [64, 64, 63, 255]); - await pollFor(page, () => getCellColor(1, 2), [61, 63, 59, 255]); - await pollFor(page, () => getCellColor(2, 2), [125, 19, 19, 255]); - await pollFor(page, () => getCellColor(3, 2), [40, 67, 13, 255]); - await pollFor(page, () => getCellColor(4, 2), [67, 63, 19, 255]); - await pollFor(page, () => getCellColor(5, 2), [45, 65, 87, 255]); - await pollFor(page, () => getCellColor(6, 2), [81, 57, 78, 255]); - await pollFor(page, () => getCellColor(7, 2), [13, 67, 67, 255]); - await pollFor(page, () => getCellColor(8, 2), [64, 64, 64, 255]); - }); - - itWebgl('should enforce half the contrast for dim cells', async () => { - const theme: ITheme = { - background: '#ffffff', - black: '#2e3436', - red: '#cc0000', - green: '#4e9a06', - yellow: '#c4a000', - blue: '#3465a4', - magenta: '#75507b', - cyan: '#06989a', - white: '#d3d7cf', - brightBlack: '#555753', - brightRed: '#ef2929', - brightGreen: '#8ae234', - brightYellow: '#fce94f', - brightBlue: '#729fcf', - brightMagenta: '#ad7fa8', - brightCyan: '#34e2e2', - brightWhite: '#eeeeec' - }; - await page.evaluate(` - window.term.options.theme = ${JSON.stringify(theme)}; - window.term.options.minimumContrastRatio = 1; - `); - // Block elements ignore minimum contrast ratio so a different char is used here - await writeSync(page, - '\\x1b[2m' + - `\\x1b[30m■\\x1b[31m■\\x1b[32m■\\x1b[33m■\\x1b[34m■\\x1b[35m■\\x1b[36m■\\x1b[37m■\\r\\n` + - `\\x1b[90m■\\x1b[91m■\\x1b[92m■\\x1b[93m■\\x1b[94m■\\x1b[95m■\\x1b[96m■\\x1b[97m■` - ); - // Validate before minimumContrastRatio is applied - await pollFor(page, () => getCellColor(1, 1), [Math.floor((255 + 0x2e) / 2), Math.floor((255 + 0x34) / 2), Math.floor((255 + 0x36) / 2), 255]); - await pollFor(page, () => getCellColor(2, 1), [Math.floor((255 + 0xcc) / 2), Math.floor((255 + 0x00) / 2), Math.floor((255 + 0x00) / 2), 255]); - await pollFor(page, () => getCellColor(3, 1), [Math.floor((255 + 0x4e) / 2), Math.floor((255 + 0x9a) / 2), Math.floor((255 + 0x06) / 2), 255]); - await pollFor(page, () => getCellColor(4, 1), [Math.floor((255 + 0xc4) / 2), Math.floor((255 + 0xa0) / 2), Math.floor((255 + 0x00) / 2), 255]); - await pollFor(page, () => getCellColor(5, 1), [Math.floor((255 + 0x34) / 2), Math.floor((255 + 0x65) / 2), Math.floor((255 + 0xa4) / 2), 255]); - await pollFor(page, () => getCellColor(6, 1), [Math.floor((255 + 0x75) / 2), Math.floor((255 + 0x50) / 2), Math.floor((255 + 0x7b) / 2), 255]); - await pollFor(page, () => getCellColor(7, 1), [Math.floor((255 + 0x06) / 2), Math.floor((255 + 0x98) / 2), Math.floor((255 + 0x9a) / 2), 255]); - await pollFor(page, () => getCellColor(8, 1), [Math.floor((255 + 0xd3) / 2), Math.floor((255 + 0xd7) / 2), Math.floor((255 + 0xcf) / 2), 255]); - await pollFor(page, () => getCellColor(1, 2), [Math.floor((255 + 0x55) / 2), Math.floor((255 + 0x57) / 2), Math.floor((255 + 0x53) / 2), 255]); - await pollFor(page, () => getCellColor(2, 2), [Math.floor((255 + 0xef) / 2), Math.floor((255 + 0x29) / 2), Math.floor((255 + 0x29) / 2), 255]); - await pollFor(page, () => getCellColor(3, 2), [Math.floor((255 + 0x8a) / 2), Math.floor((255 + 0xe2) / 2), Math.floor((255 + 0x34) / 2), 255]); - await pollFor(page, () => getCellColor(4, 2), [Math.floor((255 + 0xfc) / 2), Math.floor((255 + 0xe9) / 2), Math.floor((255 + 0x4f) / 2), 255]); - await pollFor(page, () => getCellColor(5, 2), [Math.floor((255 + 0x72) / 2), Math.floor((255 + 0x9f) / 2), Math.floor((255 + 0xcf) / 2), 255]); - await pollFor(page, () => getCellColor(6, 2), [Math.floor((255 + 0xad) / 2), Math.floor((255 + 0x7f) / 2), Math.floor((255 + 0xa8) / 2), 255]); - await pollFor(page, () => getCellColor(7, 2), [Math.floor((255 + 0x34) / 2), Math.floor((255 + 0xe2) / 2), Math.floor((255 + 0xe2) / 2), 255]); - await pollFor(page, () => getCellColor(8, 2), [Math.floor((255 + 0xee) / 2), Math.floor((255 + 0xee) / 2), Math.floor((255 + 0xec) / 2), 255]); - // Setting and check for minimum contrast values, note that these are not - // exact to the contrast ratio, if the increase luminance algorithm - // changes then these will probably fail - await page.evaluate(`window.term.options.minimumContrastRatio = 10;`); - await pollFor(page, () => getCellColor(1, 1), [150, 153, 154, 255]); - await pollFor(page, () => getCellColor(2, 1), [229, 127, 127, 255]); - await pollFor(page, () => getCellColor(3, 1), [63, 124, 4, 255]); - await pollFor(page, () => getCellColor(4, 1), [127, 104, 0, 255]); - await pollFor(page, () => getCellColor(5, 1), [153, 178, 209, 255]); - await pollFor(page, () => getCellColor(6, 1), [186, 167, 189, 255]); - await pollFor(page, () => getCellColor(7, 1), [4, 122, 124, 255]); - await pollFor(page, () => getCellColor(8, 1), [110, 112, 108, 255]); - await pollFor(page, () => getCellColor(1, 2), [170, 171, 169, 255]); - await pollFor(page, () => getCellColor(2, 2), [215, 36, 36, 255]); - await pollFor(page, () => getCellColor(3, 2), [72, 117, 25, 255]); - await pollFor(page, () => getCellColor(4, 2), [117, 109, 36, 255]); - await pollFor(page, () => getCellColor(5, 2), [72, 103, 135, 255]); - await pollFor(page, () => getCellColor(6, 2), [125, 91, 121, 255]); - await pollFor(page, () => getCellColor(7, 2), [25, 117, 117, 255]); - await pollFor(page, () => getCellColor(8, 2), [111, 111, 110, 255]); - }); - }); - - describe('selectionBackground', async () => { - if (areTestsEnabled) { - before(async () => setupBrowser()); - after(async () => browser.close()); - beforeEach(async () => page.evaluate(`window.term.reset()`)); - } - - itWebgl('should resolve the inverse foreground color based on the original background color, not the selection', async () => { - const theme: ITheme = { - foreground: '#FF0000', - background: '#00FF00', - selectionBackground: '#0000FF' - }; - await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await writeSync(page, ` █\\x1b[7m█\\x1b[0m`); - await pollFor(page, () => getCellColor(1, 1), [0, 255, 0, 255]); - await pollFor(page, () => getCellColor(2, 1), [255, 0, 0, 255]); - await pollFor(page, () => getCellColor(3, 1), [0, 255, 0, 255]); - await page.evaluate(`window.term.selectAll()`); - // Selection only cell needs to be first to ensure renderer has kicked in - await pollFor(page, () => getCellColor(1, 1), [0, 0, 255, 255]); - await pollFor(page, () => getCellColor(2, 1), [255, 0, 0, 255]); - await pollFor(page, () => getCellColor(3, 1), [0, 255, 0, 255]); - }); - }); - - describe('allowTransparency', async () => { - if (areTestsEnabled) { - before(async () => setupBrowser({ allowTransparency: true })); - after(async () => browser.close()); - beforeEach(async () => page.evaluate(`window.term.reset()`)); - } - - itWebgl('transparent background inverse', async () => { - const theme: ITheme = { - background: '#ff000080' - }; - await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - const data = `\\x1b[7m█\\x1b[0m`; - await writeSync(page, data); - // Inverse background should be opaque - await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]); - }); - }); - - describe('selectionForeground', () => { - if (areTestsEnabled) { - before(async () => setupBrowser()); - after(async () => browser.close()); - beforeEach(async () => page.evaluate(`window.term.reset()`)); - } - - itWebgl('transparent background inverse', async () => { - const theme: ITheme = { - selectionForeground: '#ff0000' - }; - await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - const data = `\\x1b[7m█\\x1b[0m`; - await writeSync(page, data); - await page.evaluate(`window.term.selectAll()`); - await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]); - }); - }); - - describe('decoration color overrides', async () => { - if (areTestsEnabled) { - before(async () => setupBrowser()); - after(async () => browser.close()); - beforeEach(async () => page.evaluate(`window.term.reset()`)); - } - - itWebgl('foregroundColor', async () => { - await page.evaluate(` - const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); - window.term.registerDecoration({ - marker, - foregroundColor: '#ff0000', - backgroundColor: '#0000ff' - }); - `); - const data = `█`; - await writeSync(page, data); - await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]); - }); - itWebgl('foregroundColor should ignore inverse', async () => { - await page.evaluate(` - const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); - window.term.registerDecoration({ - marker, - foregroundColor: '#ff0000', - backgroundColor: '#0000ff' - }); - `); - const data = `\\x1b[7m█\\x1b[0m`; - await writeSync(page, data); - await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]); - }); - itWebgl('foregroundColor should ignore inverse (only fg on decoration)', async () => { - await page.evaluate(` - const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); - window.term.registerDecoration({ - marker, - width: 2, - foregroundColor: '#ff0000' - }); - `); - const data = `\\x1b[7m█ \\x1b[0m`; - await writeSync(page, data); - await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]); // inverse foreground of '█' should be decoration fg override - await pollFor(page, () => getCellColor(2, 1), [255, 255, 255, 255]); // inverse background of ' ' should be default foreground - }); - itWebgl('backgroundColor', async () => { - await page.evaluate(` - const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); - window.term.registerDecoration({ - marker, - foregroundColor: '#ff0000', - backgroundColor: '#0000ff' - }); - `); - const data = ` `; - await writeSync(page, data); - await pollFor(page, () => getCellColor(1, 1), [0, 0, 255, 255]); - }); - itWebgl('backgroundColor should ignore inverse', async () => { - await page.evaluate(` - const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); - window.term.registerDecoration({ - marker, - foregroundColor: '#ff0000', - backgroundColor: '#0000ff' - }); - `); - const data = `\\x1b[7m \\x1b[0m`; - await writeSync(page, data); - await pollFor(page, () => getCellColor(1, 1), [0, 0, 255, 255]); - }); - itWebgl('backgroundColor should ignore inverse (only bg on decoration)', async () => { - const data = `\\x1b[7m█ \\x1b[0m`; - await writeSync(page, data); - await page.evaluate(` - const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); - window.term.registerDecoration({ - marker, - width: 2, - backgroundColor: '#0000ff' - }); - `); - await pollFor(page, () => getCellColor(1, 1), [0, 0, 0, 255]); // inverse foreground of '█' should be default - await pollFor(page, () => getCellColor(2, 1), [0, 0, 255, 255]); // inverse background of ' ' should be decoration bg override - }); - }); -}); - -async function getCellColor(col: number, row: number): Promise { - await page.evaluate(` - window.gl = window.term._core._renderService._renderer.value._gl; - window.result = new Uint8Array(4); - window.d = window.term._core._renderService.dimensions; - window.gl.readPixels( - Math.floor((${col - 0.5}) * window.d.device.cell.width), - Math.floor(window.gl.drawingBufferHeight - 1 - (${row - 0.5}) * window.d.device.cell.height), - 1, 1, window.gl.RGBA, window.gl.UNSIGNED_BYTE, window.result - ); - `); - return await page.evaluate(`Array.from(window.result)`); -} - -async function getCellPixels(col: number, row: number): Promise { - await page.evaluate(` - window.gl = window.term._core._renderService._renderer.value._gl; - window.result = new Uint8Array(window.d.device.cell.width * window.d.device.cell.height * 4); - window.d = window.term._core._renderService.dimensions; - window.gl.readPixels( - Math.floor(${col - 1} * window.d.device.cell.width), - Math.floor(window.gl.drawingBufferHeight - ${row} * window.d.device.cell.height), - window.d.device.cell.width, window.d.device.cell.height, window.gl.RGBA, window.gl.UNSIGNED_BYTE, window.result - ); - `); - return await page.evaluate(`Array.from(window.result)`); -} - -async function setupBrowser(options: ITerminalOptions = {}): Promise { - browser = await launchBrowser(); - page = await (await browser.newContext()).newPage(); - await page.setViewportSize({ width, height }); - await page.goto(APP); - await openTerminal(page, options); - await page.evaluate(` - window.addon = new WebglAddon(true); - window.term.loadAddon(window.addon); - `); -} - -const COLORS_16_TO_255 = [ - '#000000', '#00005f', '#000087', '#0000af', '#0000d7', '#0000ff', '#005f00', '#005f5f', '#005f87', '#005faf', '#005fd7', '#005fff', '#008700', '#00875f', '#008787', '#0087af', - '#0087d7', '#0087ff', '#00af00', '#00af5f', '#00af87', '#00afaf', '#00afd7', '#00afff', '#00d700', '#00d75f', '#00d787', '#00d7af', '#00d7d7', '#00d7ff', '#00ff00', '#00ff5f', - '#00ff87', '#00ffaf', '#00ffd7', '#00ffff', '#5f0000', '#5f005f', '#5f0087', '#5f00af', '#5f00d7', '#5f00ff', '#5f5f00', '#5f5f5f', '#5f5f87', '#5f5faf', '#5f5fd7', '#5f5fff', - '#5f8700', '#5f875f', '#5f8787', '#5f87af', '#5f87d7', '#5f87ff', '#5faf00', '#5faf5f', '#5faf87', '#5fafaf', '#5fafd7', '#5fafff', '#5fd700', '#5fd75f', '#5fd787', '#5fd7af', - '#5fd7d7', '#5fd7ff', '#5fff00', '#5fff5f', '#5fff87', '#5fffaf', '#5fffd7', '#5fffff', '#870000', '#87005f', '#870087', '#8700af', '#8700d7', '#8700ff', '#875f00', '#875f5f', - '#875f87', '#875faf', '#875fd7', '#875fff', '#878700', '#87875f', '#878787', '#8787af', '#8787d7', '#8787ff', '#87af00', '#87af5f', '#87af87', '#87afaf', '#87afd7', '#87afff', - '#87d700', '#87d75f', '#87d787', '#87d7af', '#87d7d7', '#87d7ff', '#87ff00', '#87ff5f', '#87ff87', '#87ffaf', '#87ffd7', '#87ffff', '#af0000', '#af005f', '#af0087', '#af00af', - '#af00d7', '#af00ff', '#af5f00', '#af5f5f', '#af5f87', '#af5faf', '#af5fd7', '#af5fff', '#af8700', '#af875f', '#af8787', '#af87af', '#af87d7', '#af87ff', '#afaf00', '#afaf5f', - '#afaf87', '#afafaf', '#afafd7', '#afafff', '#afd700', '#afd75f', '#afd787', '#afd7af', '#afd7d7', '#afd7ff', '#afff00', '#afff5f', '#afff87', '#afffaf', '#afffd7', '#afffff', - '#d70000', '#d7005f', '#d70087', '#d700af', '#d700d7', '#d700ff', '#d75f00', '#d75f5f', '#d75f87', '#d75faf', '#d75fd7', '#d75fff', '#d78700', '#d7875f', '#d78787', '#d787af', - '#d787d7', '#d787ff', '#d7af00', '#d7af5f', '#d7af87', '#d7afaf', '#d7afd7', '#d7afff', '#d7d700', '#d7d75f', '#d7d787', '#d7d7af', '#d7d7d7', '#d7d7ff', '#d7ff00', '#d7ff5f', - '#d7ff87', '#d7ffaf', '#d7ffd7', '#d7ffff', '#ff0000', '#ff005f', '#ff0087', '#ff00af', '#ff00d7', '#ff00ff', '#ff5f00', '#ff5f5f', '#ff5f87', '#ff5faf', '#ff5fd7', '#ff5fff', - '#ff8700', '#ff875f', '#ff8787', '#ff87af', '#ff87d7', '#ff87ff', '#ffaf00', '#ffaf5f', '#ffaf87', '#ffafaf', '#ffafd7', '#ffafff', '#ffd700', '#ffd75f', '#ffd787', '#ffd7af', - '#ffd7d7', '#ffd7ff', '#ffff00', '#ffff5f', '#ffff87', '#ffffaf', '#ffffd7', '#ffffff', '#080808', '#121212', '#1c1c1c', '#262626', '#303030', '#3a3a3a', '#444444', '#4e4e4e', - '#585858', '#626262', '#6c6c6c', '#767676', '#808080', '#8a8a8a', '#949494', '#9e9e9e', '#a8a8a8', '#b2b2b2', '#bcbcbc', '#c6c6c6', '#d0d0d0', '#dadada', '#e4e4e4', '#eeeeee' -]; diff --git a/addons/xterm-addon-webgl/test/WebglRenderer.test.ts b/addons/xterm-addon-webgl/test/WebglRenderer.test.ts new file mode 100644 index 00000000..690913b0 --- /dev/null +++ b/addons/xterm-addon-webgl/test/WebglRenderer.test.ts @@ -0,0 +1,1133 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert } from 'chai'; +import test from '@playwright/test'; +import { ITheme } from 'xterm'; +import { ITestContext, createTestContext, openTerminal, pollFor } from '../../../out-test/playwright/TestUtils'; + +let ctx: ITestContext; +test.beforeAll(async ({ browser }) => { + ctx = await createTestContext(browser); + await openTerminal(ctx); + await ctx.page.evaluate(` + window.addon = new WebglAddon(true); + window.term.loadAddon(window.addon); + `); +}); +test.afterAll(async () => await ctx.page.close()); + +test.describe.only('WebGL Renderer Integration Tests', async () => { + test.beforeEach(async () => { + await ctx.proxy.reset(); + ctx.page.evaluate(` + window.term.options.minimumContrastRatio = 1; + window.term.options.allowTransparency = false; + `); + // TODO: Introduce frame caching + // Clear the cached screenshot before each test + // frameDetails = undefined; + }); + + test('dispose removes renderer canvases', async function(): Promise { + assert.equal(await ctx.page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 2); + await ctx.page.evaluate(`addon.dispose()`); + assert.equal(await ctx.page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 0); + // Re-create webgl addon to avoid side effects impacting other tests + await ctx.page.evaluate(` + window.addon = new WebglAddon(true); + window.term.loadAddon(window.addon); + `); + }); + + test.describe('colors', () => { + test('foreground 0-15', async () => { + const theme: ITheme = { + black: '#010203', + red: '#040506', + green: '#070809', + yellow: '#0a0b0c', + blue: '#0d0e0f', + magenta: '#101112', + cyan: '#131415', + white: '#161718' + }; + await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.proxy.write(`\x1b[30m█\x1b[31m█\x1b[32m█\x1b[33m█\x1b[34m█\x1b[35m█\x1b[36m█\x1b[37m█`); + await pollFor(ctx.page, () => getCellColor(1, 1), [1, 2, 3, 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [4, 5, 6, 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [7, 8, 9, 255]); + await pollFor(ctx.page, () => getCellColor(4, 1), [10, 11, 12, 255]); + await pollFor(ctx.page, () => getCellColor(5, 1), [13, 14, 15, 255]); + await pollFor(ctx.page, () => getCellColor(6, 1), [16, 17, 18, 255]); + await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); + await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); + }); + + test('foreground 0-7 drawBoldTextInBrightColors', async () => { + const theme: ITheme = { + brightBlack: '#010203', + brightRed: '#040506', + brightGreen: '#070809', + brightYellow: '#0a0b0c', + brightBlue: '#0d0e0f', + brightMagenta: '#101112', + brightCyan: '#131415', + brightWhite: '#161718' + }; + await ctx.page.evaluate(` + window.term.options.theme = ${JSON.stringify(theme)}; + window.term.options.drawBoldTextInBrightColors = true; + `); + await ctx.proxy.write(`\x1b[1;30m█\x1b[1;31m█\x1b[1;32m█\x1b[1;33m█\x1b[1;34m█\x1b[1;35m█\x1b[1;36m█\x1b[1;37m█`); + await pollFor(ctx.page, () => getCellColor(1, 1), [1, 2, 3, 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [4, 5, 6, 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [7, 8, 9, 255]); + await pollFor(ctx.page, () => getCellColor(4, 1), [10, 11, 12, 255]); + await pollFor(ctx.page, () => getCellColor(5, 1), [13, 14, 15, 255]); + await pollFor(ctx.page, () => getCellColor(6, 1), [16, 17, 18, 255]); + await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); + await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); + }); + + test('background 0-15', async () => { + const theme: ITheme = { + black: '#010203', + red: '#040506', + green: '#070809', + yellow: '#0a0b0c', + blue: '#0d0e0f', + magenta: '#101112', + cyan: '#131415', + white: '#161718' + }; + await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.proxy.write(`\x1b[40m \x1b[41m \x1b[42m \x1b[43m \x1b[44m \x1b[45m \x1b[46m \x1b[47m `); + await pollFor(ctx.page, () => getCellColor(1, 1), [1, 2, 3, 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [4, 5, 6, 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [7, 8, 9, 255]); + await pollFor(ctx.page, () => getCellColor(4, 1), [10, 11, 12, 255]); + await pollFor(ctx.page, () => getCellColor(5, 1), [13, 14, 15, 255]); + await pollFor(ctx.page, () => getCellColor(6, 1), [16, 17, 18, 255]); + await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); + await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); + }); + + test('foreground 0-15 inverse', async () => { + const theme: ITheme = { + black: '#010203', + red: '#040506', + green: '#070809', + yellow: '#0a0b0c', + blue: '#0d0e0f', + magenta: '#101112', + cyan: '#131415', + white: '#161718' + }; + await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.proxy.write(`\x1b[7;30m \x1b[7;31m \x1b[7;32m \x1b[7;33m \x1b[7;34m \x1b[7;35m \x1b[7;36m \x1b[7;37m `); + await pollFor(ctx.page, () => getCellColor(1, 1), [1, 2, 3, 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [4, 5, 6, 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [7, 8, 9, 255]); + await pollFor(ctx.page, () => getCellColor(4, 1), [10, 11, 12, 255]); + await pollFor(ctx.page, () => getCellColor(5, 1), [13, 14, 15, 255]); + await pollFor(ctx.page, () => getCellColor(6, 1), [16, 17, 18, 255]); + await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); + await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); + }); + + test('background 0-15 inverse', async () => { + const theme: ITheme = { + black: '#010203', + red: '#040506', + green: '#070809', + yellow: '#0a0b0c', + blue: '#0d0e0f', + magenta: '#101112', + cyan: '#131415', + white: '#161718' + }; + await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.proxy.write(`\x1b[7;40m█\x1b[7;41m█\x1b[7;42m█\x1b[7;43m█\x1b[7;44m█\x1b[7;45m█\x1b[7;46m█\x1b[7;47m█`); + await pollFor(ctx.page, () => getCellColor(1, 1), [1, 2, 3, 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [4, 5, 6, 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [7, 8, 9, 255]); + await pollFor(ctx.page, () => getCellColor(4, 1), [10, 11, 12, 255]); + await pollFor(ctx.page, () => getCellColor(5, 1), [13, 14, 15, 255]); + await pollFor(ctx.page, () => getCellColor(6, 1), [16, 17, 18, 255]); + await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); + await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); + }); + + test('foreground 0-15 inivisible', async () => { + const theme: ITheme = { + black: '#010203', + red: '#040506', + green: '#070809', + yellow: '#0a0b0c', + blue: '#0d0e0f', + magenta: '#101112', + cyan: '#131415', + white: '#161718' + }; + await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.proxy.write(`\x1b[8;30m \x1b[8;31m \x1b[8;32m \x1b[8;33m \x1b[8;34m \x1b[8;35m \x1b[8;36m \x1b[8;37m `); + await pollFor(ctx.page, () => getCellColor(1, 1), [0, 0, 0, 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [0, 0, 0, 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [0, 0, 0, 255]); + await pollFor(ctx.page, () => getCellColor(4, 1), [0, 0, 0, 255]); + await pollFor(ctx.page, () => getCellColor(5, 1), [0, 0, 0, 255]); + await pollFor(ctx.page, () => getCellColor(6, 1), [0, 0, 0, 255]); + await pollFor(ctx.page, () => getCellColor(7, 1), [0, 0, 0, 255]); + await pollFor(ctx.page, () => getCellColor(8, 1), [0, 0, 0, 255]); + }); + + test('background 0-15 inivisible', async () => { + const theme: ITheme = { + black: '#010203', + red: '#040506', + green: '#070809', + yellow: '#0a0b0c', + blue: '#0d0e0f', + magenta: '#101112', + cyan: '#131415', + white: '#161718' + }; + await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.proxy.write(`\x1b[8;40m█\x1b[8;41m█\x1b[8;42m█\x1b[8;43m█\x1b[8;44m█\x1b[8;45m█\x1b[8;46m█\x1b[8;47m█`); + await pollFor(ctx.page, () => getCellColor(1, 1), [1, 2, 3, 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [4, 5, 6, 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [7, 8, 9, 255]); + await pollFor(ctx.page, () => getCellColor(4, 1), [10, 11, 12, 255]); + await pollFor(ctx.page, () => getCellColor(5, 1), [13, 14, 15, 255]); + await pollFor(ctx.page, () => getCellColor(6, 1), [16, 17, 18, 255]); + await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); + await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); + }); + + test('foreground 0-15 bright', async () => { + const theme: ITheme = { + brightBlack: '#010203', + brightRed: '#040506', + brightGreen: '#070809', + brightYellow: '#0a0b0c', + brightBlue: '#0d0e0f', + brightMagenta: '#101112', + brightCyan: '#131415', + brightWhite: '#161718' + }; + await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.proxy.write(`\x1b[90m█\x1b[91m█\x1b[92m█\x1b[93m█\x1b[94m█\x1b[95m█\x1b[96m█\x1b[97m█`); + await pollFor(ctx.page, () => getCellColor(1, 1), [1, 2, 3, 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [4, 5, 6, 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [7, 8, 9, 255]); + await pollFor(ctx.page, () => getCellColor(4, 1), [10, 11, 12, 255]); + await pollFor(ctx.page, () => getCellColor(5, 1), [13, 14, 15, 255]); + await pollFor(ctx.page, () => getCellColor(6, 1), [16, 17, 18, 255]); + await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); + await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); + }); + + test('background 0-15 bright', async () => { + const theme: ITheme = { + brightBlack: '#010203', + brightRed: '#040506', + brightGreen: '#070809', + brightYellow: '#0a0b0c', + brightBlue: '#0d0e0f', + brightMagenta: '#101112', + brightCyan: '#131415', + brightWhite: '#161718' + }; + await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.proxy.write(`\x1b[100m \x1b[101m \x1b[102m \x1b[103m \x1b[104m \x1b[105m \x1b[106m \x1b[107m `); + await pollFor(ctx.page, () => getCellColor(1, 1), [1, 2, 3, 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [4, 5, 6, 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [7, 8, 9, 255]); + await pollFor(ctx.page, () => getCellColor(4, 1), [10, 11, 12, 255]); + await pollFor(ctx.page, () => getCellColor(5, 1), [13, 14, 15, 255]); + await pollFor(ctx.page, () => getCellColor(6, 1), [16, 17, 18, 255]); + await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); + await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); + }); + + test('foreground 16-255', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\x1b[38;5;${16 + y * 16 + x}m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.slice(1, 3), 16); + const g = parseInt(cssColor.slice(3, 5), 16); + const b = parseInt(cssColor.slice(5, 7), 16); + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); + } + } + }); + + test('background 16-255', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\x1b[48;5;${16 + y * 16 + x}m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.slice(1, 3), 16); + const g = parseInt(cssColor.slice(3, 5), 16); + const b = parseInt(cssColor.slice(5, 7), 16); + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); + } + } + }); + + test('foreground 16-255 inverse', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\x1b[7;38;5;${16 + y * 16 + x}m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.slice(1, 3), 16); + const g = parseInt(cssColor.slice(3, 5), 16); + const b = parseInt(cssColor.slice(5, 7), 16); + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); + } + } + }); + + test('background 16-255 inverse', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\x1b[7;48;5;${16 + y * 16 + x}m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.slice(1, 3), 16); + const g = parseInt(cssColor.slice(3, 5), 16); + const b = parseInt(cssColor.slice(5, 7), 16); + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); + } + } + }); + + test('foreground 16-255 invisible', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\x1b[8;38;5;${16 + y * 16 + x}m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, 0, 0, 255]); + } + } + }); + + test('background 16-255 invisible', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\x1b[8;48;5;${16 + y * 16 + x}m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.slice(1, 3), 16); + const g = parseInt(cssColor.slice(3, 5), 16); + const b = parseInt(cssColor.slice(5, 7), 16); + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); + } + } + }); + + test('foreground 16-255 dim', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\x1b[2;38;5;${16 + y * 16 + x}m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.slice(1, 3), 16); + const g = parseInt(cssColor.slice(3, 5), 16); + const b = parseInt(cssColor.slice(5, 7), 16); + // It's difficult to assert the exact color due to rounding, just ensure the color differs + // to the regular color + await pollFor(ctx.page, async () => { + const c = await getCellColor(x + 1, y + 1); + return ( + (c[0] === 0 || c[0] !== r) && + (c[1] === 0 || c[1] !== g) && + (c[2] === 0 || c[2] !== b) + ); + }, true); + } + } + }); + + test('background 16-255 dim', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\x1b[2;48;5;${16 + y * 16 + x}m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.slice(1, 3), 16); + const g = parseInt(cssColor.slice(3, 5), 16); + const b = parseInt(cssColor.slice(5, 7), 16); + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); + } + } + }); + + test('foreground true color red', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[38;2;${i};0;0m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]); + } + } + }); + + test('background true color red', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[48;2;${i};0;0m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]); + } + } + }); + + test('foreground true color green', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[38;2;0;${i};0m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]); + } + } + }); + + test('background true color green', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[48;2;0;${i};0m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]); + } + } + }); + + test('foreground true color blue', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[38;2;0;0;${i}m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]); + } + } + }); + + test('background true color blue', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[48;2;0;0;${i}m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]); + } + } + }); + + test('foreground true color grey', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[38;2;${i};${i};${i}m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); + } + } + }); + + test('background true color grey', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[48;2;${i};${i};${i}m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); + } + } + }); + + test('foreground true color red inverse', async function(): Promise { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[7;38;2;${i};0;0m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]); + } + } + }); + + test('background true color red inverse', async function(): Promise { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[7;48;2;${i};0;0m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]); + } + } + }); + + test('foreground true color green inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[7;38;2;0;${i};0m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]); + } + } + }); + + test('background true color green inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[7;48;2;0;${i};0m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]); + } + } + }); + + test('foreground true color blue inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[7;38;2;0;0;${i}m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]); + } + } + }); + + test('background true color blue inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[7;48;2;0;0;${i}m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]); + } + } + }); + + test('foreground true color grey inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[7;38;2;${i};${i};${i}m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); + } + } + }); + + test('background true color grey inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[7;48;2;${i};${i};${i}m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); + } + } + }); + + test('foreground true color grey invisible', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[8;38;2;${i};${i};${i}m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, 0, 0, 255]); + } + } + }); + + test('background true color grey invisible', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[8;48;2;${i};${i};${i}m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); + } + } + }); + }); + + test.describe('minimumContrastRatio', async () => { + test('should adjust 0-15 colors on black background', async () => { + const theme: ITheme = { + black: '#2e3436', + red: '#cc0000', + green: '#4e9a06', + yellow: '#c4a000', + blue: '#3465a4', + magenta: '#75507b', + cyan: '#06989a', + white: '#d3d7cf', + brightBlack: '#555753', + brightRed: '#ef2929', + brightGreen: '#8ae234', + brightYellow: '#fce94f', + brightBlue: '#729fcf', + brightMagenta: '#ad7fa8', + brightCyan: '#34e2e2', + brightWhite: '#eeeeec' + }; + await ctx.page.evaluate(` + window.term.options.theme = ${JSON.stringify(theme)}; + window.term.options.minimumContrastRatio = 1; + `); + // Block elements ignore minimum contrast ratio so a different char is used here + await ctx.proxy.write( + `\x1b[30m■\x1b[31m■\x1b[32m■\x1b[33m■\x1b[34m■\x1b[35m■\x1b[36m■\x1b[37m■\r\n` + + `\x1b[90m■\x1b[91m■\x1b[92m■\x1b[93m■\x1b[94m■\x1b[95m■\x1b[96m■\x1b[97m■` + ); + // Validate before minimumContrastRatio is applied + await pollFor(ctx.page, () => getCellColor(1, 1), [0x2e, 0x34, 0x36, 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [0xcc, 0x00, 0x00, 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [0x4e, 0x9a, 0x06, 255]); + await pollFor(ctx.page, () => getCellColor(4, 1), [0xc4, 0xa0, 0x00, 255]); + await pollFor(ctx.page, () => getCellColor(5, 1), [0x34, 0x65, 0xa4, 255]); + await pollFor(ctx.page, () => getCellColor(6, 1), [0x75, 0x50, 0x7b, 255]); + await pollFor(ctx.page, () => getCellColor(7, 1), [0x06, 0x98, 0x9a, 255]); + await pollFor(ctx.page, () => getCellColor(8, 1), [0xd3, 0xd7, 0xcf, 255]); + await pollFor(ctx.page, () => getCellColor(1, 2), [0x55, 0x57, 0x53, 255]); + await pollFor(ctx.page, () => getCellColor(2, 2), [0xef, 0x29, 0x29, 255]); + await pollFor(ctx.page, () => getCellColor(3, 2), [0x8a, 0xe2, 0x34, 255]); + await pollFor(ctx.page, () => getCellColor(4, 2), [0xfc, 0xe9, 0x4f, 255]); + await pollFor(ctx.page, () => getCellColor(5, 2), [0x72, 0x9f, 0xcf, 255]); + await pollFor(ctx.page, () => getCellColor(6, 2), [0xad, 0x7f, 0xa8, 255]); + await pollFor(ctx.page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]); + await pollFor(ctx.page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]); + // Setting and check for minimum contrast values, note that these are not + // exact to the contrast ratio, if the increase luminance algorithm + // changes then these will probably fail + await ctx.page.evaluate(`window.term.options.minimumContrastRatio = 10;`); + await pollFor(ctx.page, () => getCellColor(1, 1), [176, 180, 180, 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [238, 158, 158, 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [152, 198, 110, 255]); + await pollFor(ctx.page, () => getCellColor(4, 1), [208, 179, 49, 255]); + await pollFor(ctx.page, () => getCellColor(5, 1), [161, 183, 215, 255]); + await pollFor(ctx.page, () => getCellColor(6, 1), [191, 174, 194, 255]); + await pollFor(ctx.page, () => getCellColor(7, 1), [110, 197, 198, 255]); + await pollFor(ctx.page, () => getCellColor(8, 1), [211, 215, 207, 255]); + await pollFor(ctx.page, () => getCellColor(1, 2), [183, 185, 183, 255]); + await pollFor(ctx.page, () => getCellColor(2, 2), [249, 156, 156, 255]); + await pollFor(ctx.page, () => getCellColor(3, 2), [138, 226, 52, 255]); + await pollFor(ctx.page, () => getCellColor(4, 2), [252, 233, 79, 255]); + await pollFor(ctx.page, () => getCellColor(5, 2), [154, 186, 221, 255]); + await pollFor(ctx.page, () => getCellColor(6, 2), [203, 173, 199, 255]); + // Unchanged + await pollFor(ctx.page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]); + await pollFor(ctx.page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]); + }); + + test('should adjust 0-15 colors on white background', async () => { + const theme: ITheme = { + background: '#ffffff', + black: '#2e3436', + red: '#cc0000', + green: '#4e9a06', + yellow: '#c4a000', + blue: '#3465a4', + magenta: '#75507b', + cyan: '#06989a', + white: '#d3d7cf', + brightBlack: '#555753', + brightRed: '#ef2929', + brightGreen: '#8ae234', + brightYellow: '#fce94f', + brightBlue: '#729fcf', + brightMagenta: '#ad7fa8', + brightCyan: '#34e2e2', + brightWhite: '#eeeeec' + }; + await ctx.page.evaluate(` + window.term.options.theme = ${JSON.stringify(theme)}; + window.term.options.minimumContrastRatio = 1; + `); + // Block elements ignore minimum contrast ratio so a different char is used here + await ctx.proxy.write( + `\x1b[30m■\x1b[31m■\x1b[32m■\x1b[33m■\x1b[34m■\x1b[35m■\x1b[36m■\x1b[37m■\r\n` + + `\x1b[90m■\x1b[91m■\x1b[92m■\x1b[93m■\x1b[94m■\x1b[95m■\x1b[96m■\x1b[97m■` + ); + // Validate before minimumContrastRatio is applied + await pollFor(ctx.page, () => getCellColor(1, 1), [0x2e, 0x34, 0x36, 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [0xcc, 0x00, 0x00, 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [0x4e, 0x9a, 0x06, 255]); + await pollFor(ctx.page, () => getCellColor(4, 1), [0xc4, 0xa0, 0x00, 255]); + await pollFor(ctx.page, () => getCellColor(5, 1), [0x34, 0x65, 0xa4, 255]); + await pollFor(ctx.page, () => getCellColor(6, 1), [0x75, 0x50, 0x7b, 255]); + await pollFor(ctx.page, () => getCellColor(7, 1), [0x06, 0x98, 0x9a, 255]); + await pollFor(ctx.page, () => getCellColor(8, 1), [0xd3, 0xd7, 0xcf, 255]); + await pollFor(ctx.page, () => getCellColor(1, 2), [0x55, 0x57, 0x53, 255]); + await pollFor(ctx.page, () => getCellColor(2, 2), [0xef, 0x29, 0x29, 255]); + await pollFor(ctx.page, () => getCellColor(3, 2), [0x8a, 0xe2, 0x34, 255]); + await pollFor(ctx.page, () => getCellColor(4, 2), [0xfc, 0xe9, 0x4f, 255]); + await pollFor(ctx.page, () => getCellColor(5, 2), [0x72, 0x9f, 0xcf, 255]); + await pollFor(ctx.page, () => getCellColor(6, 2), [0xad, 0x7f, 0xa8, 255]); + await pollFor(ctx.page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]); + await pollFor(ctx.page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]); + // Setting and check for minimum contrast values, note that these are not + // exact to the contrast ratio, if the increase luminance algorithm + // changes then these will probably fail + await ctx.page.evaluate(`window.term.options.minimumContrastRatio = 10;`); + await pollFor(ctx.page, () => getCellColor(1, 1), [46, 52, 54, 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [132, 0, 0, 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [36, 72, 0, 255]); + await pollFor(ctx.page, () => getCellColor(4, 1), [72, 59, 0, 255]); + await pollFor(ctx.page, () => getCellColor(5, 1), [32, 64, 106, 255]); + await pollFor(ctx.page, () => getCellColor(6, 1), [75, 51, 80, 255]); + await pollFor(ctx.page, () => getCellColor(7, 1), [0, 71, 72, 255]); + await pollFor(ctx.page, () => getCellColor(8, 1), [64, 64, 63, 255]); + await pollFor(ctx.page, () => getCellColor(1, 2), [61, 63, 59, 255]); + await pollFor(ctx.page, () => getCellColor(2, 2), [125, 19, 19, 255]); + await pollFor(ctx.page, () => getCellColor(3, 2), [40, 67, 13, 255]); + await pollFor(ctx.page, () => getCellColor(4, 2), [67, 63, 19, 255]); + await pollFor(ctx.page, () => getCellColor(5, 2), [45, 65, 87, 255]); + await pollFor(ctx.page, () => getCellColor(6, 2), [81, 57, 78, 255]); + await pollFor(ctx.page, () => getCellColor(7, 2), [13, 67, 67, 255]); + await pollFor(ctx.page, () => getCellColor(8, 2), [64, 64, 64, 255]); + }); + + test('should enforce half the contrast for dim cells', async () => { + // TODO: This test fails on webkit + if (ctx.browser.browserType().name() === 'webkit') { + test.skip(); + return; + } + const theme: ITheme = { + background: '#ffffff', + black: '#2e3436', + red: '#cc0000', + green: '#4e9a06', + yellow: '#c4a000', + blue: '#3465a4', + magenta: '#75507b', + cyan: '#06989a', + white: '#d3d7cf', + brightBlack: '#555753', + brightRed: '#ef2929', + brightGreen: '#8ae234', + brightYellow: '#fce94f', + brightBlue: '#729fcf', + brightMagenta: '#ad7fa8', + brightCyan: '#34e2e2', + brightWhite: '#eeeeec' + }; + await ctx.page.evaluate(` + window.term.options.theme = ${JSON.stringify(theme)}; + window.term.options.minimumContrastRatio = 1; + `); + // Block elements ignore minimum contrast ratio so a different char is used here + await ctx.proxy.write( + '\x1b[2m' + + `\x1b[30m■\x1b[31m■\x1b[32m■\x1b[33m■\x1b[34m■\x1b[35m■\x1b[36m■\x1b[37m■\r\n` + + `\x1b[90m■\x1b[91m■\x1b[92m■\x1b[93m■\x1b[94m■\x1b[95m■\x1b[96m■\x1b[97m■` + ); + // Validate before minimumContrastRatio is applied + await pollFor(ctx.page, () => getCellColor(1, 1), [Math.floor((255 + 0x2e) / 2), Math.floor((255 + 0x34) / 2), Math.floor((255 + 0x36) / 2), 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [Math.floor((255 + 0xcc) / 2), Math.floor((255 + 0x00) / 2), Math.floor((255 + 0x00) / 2), 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [Math.floor((255 + 0x4e) / 2), Math.floor((255 + 0x9a) / 2), Math.floor((255 + 0x06) / 2), 255]); + await pollFor(ctx.page, () => getCellColor(4, 1), [Math.floor((255 + 0xc4) / 2), Math.floor((255 + 0xa0) / 2), Math.floor((255 + 0x00) / 2), 255]); + await pollFor(ctx.page, () => getCellColor(5, 1), [Math.floor((255 + 0x34) / 2), Math.floor((255 + 0x65) / 2), Math.floor((255 + 0xa4) / 2), 255]); + await pollFor(ctx.page, () => getCellColor(6, 1), [Math.floor((255 + 0x75) / 2), Math.floor((255 + 0x50) / 2), Math.floor((255 + 0x7b) / 2), 255]); + await pollFor(ctx.page, () => getCellColor(7, 1), [Math.floor((255 + 0x06) / 2), Math.floor((255 + 0x98) / 2), Math.floor((255 + 0x9a) / 2), 255]); + await pollFor(ctx.page, () => getCellColor(8, 1), [Math.floor((255 + 0xd3) / 2), Math.floor((255 + 0xd7) / 2), Math.floor((255 + 0xcf) / 2), 255]); + await pollFor(ctx.page, () => getCellColor(1, 2), [Math.floor((255 + 0x55) / 2), Math.floor((255 + 0x57) / 2), Math.floor((255 + 0x53) / 2), 255]); + await pollFor(ctx.page, () => getCellColor(2, 2), [Math.floor((255 + 0xef) / 2), Math.floor((255 + 0x29) / 2), Math.floor((255 + 0x29) / 2), 255]); + await pollFor(ctx.page, () => getCellColor(3, 2), [Math.floor((255 + 0x8a) / 2), Math.floor((255 + 0xe2) / 2), Math.floor((255 + 0x34) / 2), 255]); + await pollFor(ctx.page, () => getCellColor(4, 2), [Math.floor((255 + 0xfc) / 2), Math.floor((255 + 0xe9) / 2), Math.floor((255 + 0x4f) / 2), 255]); + await pollFor(ctx.page, () => getCellColor(5, 2), [Math.floor((255 + 0x72) / 2), Math.floor((255 + 0x9f) / 2), Math.floor((255 + 0xcf) / 2), 255]); + await pollFor(ctx.page, () => getCellColor(6, 2), [Math.floor((255 + 0xad) / 2), Math.floor((255 + 0x7f) / 2), Math.floor((255 + 0xa8) / 2), 255]); + await pollFor(ctx.page, () => getCellColor(7, 2), [Math.floor((255 + 0x34) / 2), Math.floor((255 + 0xe2) / 2), Math.floor((255 + 0xe2) / 2), 255]); + await pollFor(ctx.page, () => getCellColor(8, 2), [Math.floor((255 + 0xee) / 2), Math.floor((255 + 0xee) / 2), Math.floor((255 + 0xec) / 2), 255]); + // Setting and check for minimum contrast values, note that these are not + // exact to the contrast ratio, if the increase luminance algorithm + // changes then these will probably fail + await ctx.page.evaluate(`window.term.options.minimumContrastRatio = 10;`); + await pollFor(ctx.page, () => getCellColor(1, 1), [150, 153, 154, 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [229, 127, 127, 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [63, 124, 4, 255]); + await pollFor(ctx.page, () => getCellColor(4, 1), [127, 104, 0, 255]); + await pollFor(ctx.page, () => getCellColor(5, 1), [153, 178, 209, 255]); + await pollFor(ctx.page, () => getCellColor(6, 1), [186, 167, 189, 255]); + await pollFor(ctx.page, () => getCellColor(7, 1), [4, 122, 124, 255]); + await pollFor(ctx.page, () => getCellColor(8, 1), [110, 112, 108, 255]); + await pollFor(ctx.page, () => getCellColor(1, 2), [170, 171, 169, 255]); + await pollFor(ctx.page, () => getCellColor(2, 2), [215, 36, 36, 255]); + await pollFor(ctx.page, () => getCellColor(3, 2), [72, 117, 25, 255]); + await pollFor(ctx.page, () => getCellColor(4, 2), [117, 109, 36, 255]); + await pollFor(ctx.page, () => getCellColor(5, 2), [72, 103, 135, 255]); + await pollFor(ctx.page, () => getCellColor(6, 2), [125, 91, 121, 255]); + await pollFor(ctx.page, () => getCellColor(7, 2), [25, 117, 117, 255]); + await pollFor(ctx.page, () => getCellColor(8, 2), [111, 111, 110, 255]); + }); + }); + + test.describe('selectionBackground', async () => { + test('should resolve the inverse foreground color based on the original background color, not the selection', async () => { + const theme: ITheme = { + foreground: '#FF0000', + background: '#00FF00', + selectionBackground: '#0000FF' + }; + await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.proxy.write(` █\x1b[7m█\x1b[0m`); + await pollFor(ctx.page, () => getCellColor(1, 1), [0, 255, 0, 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [255, 0, 0, 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [0, 255, 0, 255]); + await ctx.page.evaluate(`window.term.selectAll()`); + // Selection only cell needs to be first to ensure renderer has kicked in + await pollFor(ctx.page, () => getCellColor(1, 1), [0, 0, 255, 255]); + await pollFor(ctx.page, () => getCellColor(2, 1), [255, 0, 0, 255]); + await pollFor(ctx.page, () => getCellColor(3, 1), [0, 255, 0, 255]); + }); + }); + + test.describe('allowTransparency', async () => { + test.beforeEach(() => ctx.page.evaluate(`term.options.allowTransparency = true`)); + + test('transparent background inverse', async () => { + const theme: ITheme = { + background: '#ff000080' + }; + await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + const data = `\x1b[7m█\x1b[0m`; + await ctx.proxy.write(data); + // Inverse background should be opaque + await pollFor(ctx.page, () => getCellColor(1, 1), [255, 0, 0, 255]); + }); + }); + + test.describe('selectionForeground', () => { + test('transparent background inverse', async () => { + const theme: ITheme = { + selectionForeground: '#ff0000' + }; + await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + const data = `\x1b[7m█\x1b[0m`; + await ctx.proxy.write(data); + await ctx.page.evaluate(`window.term.selectAll()`); + await pollFor(ctx.page, () => getCellColor(1, 1), [255, 0, 0, 255]); + }); + }); + + test.describe('decoration color overrides', async () => { + test('foregroundColor', async () => { + await ctx.page.evaluate(` + const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); + window.term.registerDecoration({ + marker, + foregroundColor: '#ff0000', + backgroundColor: '#0000ff' + }); + `); + const data = `█`; + await ctx.proxy.write(data); + await pollFor(ctx.page, () => getCellColor(1, 1), [255, 0, 0, 255]); + }); + test('foregroundColor should ignore inverse', async () => { + await ctx.page.evaluate(` + const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); + window.term.registerDecoration({ + marker, + foregroundColor: '#ff0000', + backgroundColor: '#0000ff' + }); + `); + const data = `\x1b[7m█\x1b[0m`; + await ctx.proxy.write(data); + await pollFor(ctx.page, () => getCellColor(1, 1), [255, 0, 0, 255]); + }); + test('foregroundColor should ignore inverse (only fg on decoration)', async () => { + await ctx.page.evaluate(` + const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); + window.term.registerDecoration({ + marker, + width: 2, + foregroundColor: '#ff0000' + }); + `); + const data = `\x1b[7m█ \x1b[0m`; + await ctx.proxy.write(data); + await pollFor(ctx.page, () => getCellColor(1, 1), [255, 0, 0, 255]); // inverse foreground of '█' should be decoration fg override + await pollFor(ctx.page, () => getCellColor(2, 1), [255, 255, 255, 255]); // inverse background of ' ' should be default foreground + }); + test('backgroundColor', async () => { + await ctx.page.evaluate(` + const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); + window.term.registerDecoration({ + marker, + foregroundColor: '#ff0000', + backgroundColor: '#0000ff' + }); + `); + const data = ` `; + await ctx.proxy.write(data); + await pollFor(ctx.page, () => getCellColor(1, 1), [0, 0, 255, 255]); + }); + test('backgroundColor should ignore inverse', async () => { + await ctx.page.evaluate(` + const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); + window.term.registerDecoration({ + marker, + foregroundColor: '#ff0000', + backgroundColor: '#0000ff' + }); + `); + const data = `\x1b[7m \x1b[0m`; + await ctx.proxy.write(data); + await pollFor(ctx.page, () => getCellColor(1, 1), [0, 0, 255, 255]); + }); + test('backgroundColor should ignore inverse (only bg on decoration)', async () => { + const data = `\x1b[7m█ \x1b[0m`; + await ctx.proxy.write(data); + await ctx.page.evaluate(` + const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); + window.term.registerDecoration({ + marker, + width: 2, + backgroundColor: '#0000ff' + }); + `); + await pollFor(ctx.page, () => getCellColor(1, 1), [0, 0, 0, 255]); // inverse foreground of '█' should be default + await pollFor(ctx.page, () => getCellColor(2, 1), [0, 0, 255, 255]); // inverse background of ' ' should be decoration bg override + }); + }); +}); + +async function getCellColor(col: number, row: number): Promise { + await ctx.page.evaluate(` + window.gl = window.term._core._renderService._renderer.value._gl; + window.result = new Uint8Array(4); + window.d = window.term._core._renderService.dimensions; + window.gl.readPixels( + Math.floor((${col - 0.5}) * window.d.device.cell.width), + Math.floor(window.gl.drawingBufferHeight - 1 - (${row - 0.5}) * window.d.device.cell.height), + 1, 1, window.gl.RGBA, window.gl.UNSIGNED_BYTE, window.result + ); + `); + return await ctx.page.evaluate(`Array.from(window.result)`); +} + +// async function getCellPixels(col: number, row: number): Promise { +// await ctx.page.evaluate(` +// window.gl = window.term._core._renderService._renderer.value._gl; +// window.result = new Uint8Array(window.d.device.cell.width * window.d.device.cell.height * 4); +// window.d = window.term._core._renderService.dimensions; +// window.gl.readPixels( +// Math.floor(${col - 1} * window.d.device.cell.width), +// Math.floor(window.gl.drawingBufferHeight - ${row} * window.d.device.cell.height), +// window.d.device.cell.width, window.d.device.cell.height, window.gl.RGBA, window.gl.UNSIGNED_BYTE, window.result +// ); +// `); +// return await ctx.page.evaluate(`Array.from(window.result)`); +// } + +const COLORS_16_TO_255 = [ + '#000000', '#00005f', '#000087', '#0000af', '#0000d7', '#0000ff', '#005f00', '#005f5f', '#005f87', '#005faf', '#005fd7', '#005fff', '#008700', '#00875f', '#008787', '#0087af', + '#0087d7', '#0087ff', '#00af00', '#00af5f', '#00af87', '#00afaf', '#00afd7', '#00afff', '#00d700', '#00d75f', '#00d787', '#00d7af', '#00d7d7', '#00d7ff', '#00ff00', '#00ff5f', + '#00ff87', '#00ffaf', '#00ffd7', '#00ffff', '#5f0000', '#5f005f', '#5f0087', '#5f00af', '#5f00d7', '#5f00ff', '#5f5f00', '#5f5f5f', '#5f5f87', '#5f5faf', '#5f5fd7', '#5f5fff', + '#5f8700', '#5f875f', '#5f8787', '#5f87af', '#5f87d7', '#5f87ff', '#5faf00', '#5faf5f', '#5faf87', '#5fafaf', '#5fafd7', '#5fafff', '#5fd700', '#5fd75f', '#5fd787', '#5fd7af', + '#5fd7d7', '#5fd7ff', '#5fff00', '#5fff5f', '#5fff87', '#5fffaf', '#5fffd7', '#5fffff', '#870000', '#87005f', '#870087', '#8700af', '#8700d7', '#8700ff', '#875f00', '#875f5f', + '#875f87', '#875faf', '#875fd7', '#875fff', '#878700', '#87875f', '#878787', '#8787af', '#8787d7', '#8787ff', '#87af00', '#87af5f', '#87af87', '#87afaf', '#87afd7', '#87afff', + '#87d700', '#87d75f', '#87d787', '#87d7af', '#87d7d7', '#87d7ff', '#87ff00', '#87ff5f', '#87ff87', '#87ffaf', '#87ffd7', '#87ffff', '#af0000', '#af005f', '#af0087', '#af00af', + '#af00d7', '#af00ff', '#af5f00', '#af5f5f', '#af5f87', '#af5faf', '#af5fd7', '#af5fff', '#af8700', '#af875f', '#af8787', '#af87af', '#af87d7', '#af87ff', '#afaf00', '#afaf5f', + '#afaf87', '#afafaf', '#afafd7', '#afafff', '#afd700', '#afd75f', '#afd787', '#afd7af', '#afd7d7', '#afd7ff', '#afff00', '#afff5f', '#afff87', '#afffaf', '#afffd7', '#afffff', + '#d70000', '#d7005f', '#d70087', '#d700af', '#d700d7', '#d700ff', '#d75f00', '#d75f5f', '#d75f87', '#d75faf', '#d75fd7', '#d75fff', '#d78700', '#d7875f', '#d78787', '#d787af', + '#d787d7', '#d787ff', '#d7af00', '#d7af5f', '#d7af87', '#d7afaf', '#d7afd7', '#d7afff', '#d7d700', '#d7d75f', '#d7d787', '#d7d7af', '#d7d7d7', '#d7d7ff', '#d7ff00', '#d7ff5f', + '#d7ff87', '#d7ffaf', '#d7ffd7', '#d7ffff', '#ff0000', '#ff005f', '#ff0087', '#ff00af', '#ff00d7', '#ff00ff', '#ff5f00', '#ff5f5f', '#ff5f87', '#ff5faf', '#ff5fd7', '#ff5fff', + '#ff8700', '#ff875f', '#ff8787', '#ff87af', '#ff87d7', '#ff87ff', '#ffaf00', '#ffaf5f', '#ffaf87', '#ffafaf', '#ffafd7', '#ffafff', '#ffd700', '#ffd75f', '#ffd787', '#ffd7af', + '#ffd7d7', '#ffd7ff', '#ffff00', '#ffff5f', '#ffff87', '#ffffaf', '#ffffd7', '#ffffff', '#080808', '#121212', '#1c1c1c', '#262626', '#303030', '#3a3a3a', '#444444', '#4e4e4e', + '#585858', '#626262', '#6c6c6c', '#767676', '#808080', '#8a8a8a', '#949494', '#9e9e9e', '#a8a8a8', '#b2b2b2', '#bcbcbc', '#c6c6c6', '#d0d0d0', '#dadada', '#e4e4e4', '#eeeeee' +]; diff --git a/addons/xterm-addon-webgl/test/playwright.config.ts b/addons/xterm-addon-webgl/test/playwright.config.ts new file mode 100644 index 00000000..3d1fe4fb --- /dev/null +++ b/addons/xterm-addon-webgl/test/playwright.config.ts @@ -0,0 +1,35 @@ +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' + } + } + ], + reporter: 'list', + webServer: { + command: 'npm start', + port: 3000, + timeout: 120000, + reuseExistingServer: !process.env.CI + } +}; +export default config; diff --git a/addons/xterm-addon-webgl/test/tsconfig.json b/addons/xterm-addon-webgl/test/tsconfig.json index 1ff9217e..8b951c40 100644 --- a/addons/xterm-addon-webgl/test/tsconfig.json +++ b/addons/xterm-addon-webgl/test/tsconfig.json @@ -20,9 +20,8 @@ }, "strict": true, "types": [ - "../../../node_modules/@types/mocha", "../../../node_modules/@types/node", - "../../../out-test/api/TestUtils" + "../../../out-test/playwright/TestUtils" ] }, "include": [ diff --git a/bin/test_playwright.js b/bin/test_playwright.js new file mode 100644 index 00000000..bc4e7b02 --- /dev/null +++ b/bin/test_playwright.js @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +// @ts-check + +const cp = require('child_process'); +const path = require('path'); + +const configs = [ + { name: 'core', path: 'out-test/playwright/playwright.config.js' }, + { name: 'xterm-addon-webgl', path: 'addons/xterm-addon-webgl/out-test/playwright.config.js' } +]; + +function npmBinScript(script) { + return path.resolve(__dirname, `../node_modules/.bin/` + (process.platform === 'win32' ? + `${script}.cmd` : script)); +} + +for (const config of configs) { + const command = npmBinScript('playwright'); + const args = ['test', '-c', config.path, ...process.argv.slice(2)]; + console.log(`Running suite \x1b[1;34m${config.name}...\x1b[0m`); + console.log(`\n\x1b[32m${command}\x1b[0m`, args); + const run = cp.spawnSync(command, args, { + cwd: path.resolve(__dirname, '..'), + stdio: 'inherit' + } + ); + if (run.status) { + process.exit(run.status); + } +} diff --git a/package.json b/package.json index 77f223aa..77be8f26 100644 --- a/package.json +++ b/package.json @@ -38,11 +38,11 @@ "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-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-playwright": "node ./bin/test_playwright.js --workers 4", + "test-playwright-chromium": "node ./bin/test_playwright.js --workers 4 \"--project=Chrome Stable\"", + "test-playwright-firefox": "node ./bin/test_playwright.js --workers 4 \"--project=Firefox Stable\"", + "test-playwright-webkit": "node ./bin/test_playwright.js --workers 4 \"--project=WebKit\"", + "test-playwright-debug": "node ./bin/test_playwright.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", diff --git a/test/playwright/TestUtils.ts b/test/playwright/TestUtils.ts index f556e41b..68ef2d69 100644 --- a/test/playwright/TestUtils.ts +++ b/test/playwright/TestUtils.ts @@ -12,8 +12,6 @@ import * as playwright from '@playwright/test'; import { PageFunction } from 'playwright-core/types/structs'; 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'); export interface ITestContext { browser: Browser; @@ -387,7 +385,14 @@ export async function pollFor(page: playwright.Page, evalOrFn: string | (() = console.log('pollFor\n actual: ', JSON.stringify(result), ' expected: ', JSON.stringify(val)); } - if (!deepEqual(result, val)) { + let equalityCheck = true; + try { + deepStrictEqual(result, val); + } catch (e) { + equalityCheck = false; + } + + if (!equalityCheck) { if (maxDuration === undefined) { maxDuration = 2000; } From 413e884efb60805201c97f5fa84fa312302d31b5 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 10:12:06 -0700 Subject: [PATCH 54/99] Use faster pixel check method with caching --- .../test/WebglRenderer.test.ts | 68 +++++++++++-------- addons/xterm-addon-webgl/test/tsconfig.json | 1 + bin/test_playwright.js | 2 +- 3 files changed, 42 insertions(+), 29 deletions(-) diff --git a/addons/xterm-addon-webgl/test/WebglRenderer.test.ts b/addons/xterm-addon-webgl/test/WebglRenderer.test.ts index 690913b0..9c0418b4 100644 --- a/addons/xterm-addon-webgl/test/WebglRenderer.test.ts +++ b/addons/xterm-addon-webgl/test/WebglRenderer.test.ts @@ -4,9 +4,10 @@ */ import { assert } from 'chai'; -import test from '@playwright/test'; +import test, { LocatorScreenshotOptions } from '@playwright/test'; import { ITheme } from 'xterm'; -import { ITestContext, createTestContext, openTerminal, pollFor } from '../../../out-test/playwright/TestUtils'; +import { ITestContext, MaybeAsync, createTestContext, openTerminal, pollFor } from '../../../out-test/playwright/TestUtils'; +import { IImage32, decodePng } from '@lunapaint/png-codec'; let ctx: ITestContext; test.beforeAll(async ({ browser }) => { @@ -28,7 +29,7 @@ test.describe.only('WebGL Renderer Integration Tests', async () => { `); // TODO: Introduce frame caching // Clear the cached screenshot before each test - // frameDetails = undefined; + frameDetails = undefined; }); test('dispose removes renderer canvases', async function(): Promise { @@ -792,6 +793,7 @@ test.describe.only('WebGL Renderer Integration Tests', async () => { // exact to the contrast ratio, if the increase luminance algorithm // changes then these will probably fail await ctx.page.evaluate(`window.term.options.minimumContrastRatio = 10;`); + frameDetails = undefined; await pollFor(ctx.page, () => getCellColor(1, 1), [176, 180, 180, 255]); await pollFor(ctx.page, () => getCellColor(2, 1), [238, 158, 158, 255]); await pollFor(ctx.page, () => getCellColor(3, 1), [152, 198, 110, 255]); @@ -861,6 +863,7 @@ test.describe.only('WebGL Renderer Integration Tests', async () => { // exact to the contrast ratio, if the increase luminance algorithm // changes then these will probably fail await ctx.page.evaluate(`window.term.options.minimumContrastRatio = 10;`); + frameDetails = undefined; await pollFor(ctx.page, () => getCellColor(1, 1), [46, 52, 54, 255]); await pollFor(ctx.page, () => getCellColor(2, 1), [132, 0, 0, 255]); await pollFor(ctx.page, () => getCellColor(3, 1), [36, 72, 0, 255]); @@ -935,6 +938,7 @@ test.describe.only('WebGL Renderer Integration Tests', async () => { // exact to the contrast ratio, if the increase luminance algorithm // changes then these will probably fail await ctx.page.evaluate(`window.term.options.minimumContrastRatio = 10;`); + frameDetails = undefined; await pollFor(ctx.page, () => getCellColor(1, 1), [150, 153, 154, 255]); await pollFor(ctx.page, () => getCellColor(2, 1), [229, 127, 127, 255]); await pollFor(ctx.page, () => getCellColor(3, 1), [63, 124, 4, 255]); @@ -967,6 +971,7 @@ test.describe.only('WebGL Renderer Integration Tests', async () => { await pollFor(ctx.page, () => getCellColor(2, 1), [255, 0, 0, 255]); await pollFor(ctx.page, () => getCellColor(3, 1), [0, 255, 0, 255]); await ctx.page.evaluate(`window.term.selectAll()`); + frameDetails = undefined; // Selection only cell needs to be first to ensure renderer has kicked in await pollFor(ctx.page, () => getCellColor(1, 1), [0, 0, 255, 255]); await pollFor(ctx.page, () => getCellColor(2, 1), [255, 0, 0, 255]); @@ -1086,33 +1091,40 @@ test.describe.only('WebGL Renderer Integration Tests', async () => { }); }); -async function getCellColor(col: number, row: number): Promise { - await ctx.page.evaluate(` - window.gl = window.term._core._renderService._renderer.value._gl; - window.result = new Uint8Array(4); - window.d = window.term._core._renderService.dimensions; - window.gl.readPixels( - Math.floor((${col - 0.5}) * window.d.device.cell.width), - Math.floor(window.gl.drawingBufferHeight - 1 - (${row - 0.5}) * window.d.device.cell.height), - 1, 1, window.gl.RGBA, window.gl.UNSIGNED_BYTE, window.result - ); - `); - return await ctx.page.evaluate(`Array.from(window.result)`); +/** + * Gets the color of the pixel in the center of a cell. + * @param col The 1-based column index to get the color for. + * @param row The 1-based row index to get the color for. + */ +function getCellColor(col: number, row: number): MaybeAsync<[red: number, green: number, blue: number, alpha: number]> { + if (!frameDetails) { + return getFrameDetails().then(frameDetails => getCellColorInner(frameDetails, col, row)); + } + return getCellColorInner(frameDetails, col, row); } -// async function getCellPixels(col: number, row: number): Promise { -// await ctx.page.evaluate(` -// window.gl = window.term._core._renderService._renderer.value._gl; -// window.result = new Uint8Array(window.d.device.cell.width * window.d.device.cell.height * 4); -// window.d = window.term._core._renderService.dimensions; -// window.gl.readPixels( -// Math.floor(${col - 1} * window.d.device.cell.width), -// Math.floor(window.gl.drawingBufferHeight - ${row} * window.d.device.cell.height), -// window.d.device.cell.width, window.d.device.cell.height, window.gl.RGBA, window.gl.UNSIGNED_BYTE, window.result -// ); -// `); -// return await ctx.page.evaluate(`Array.from(window.result)`); -// } +let frameDetails: { cols: number, rows: number, decoded: IImage32 } | undefined = undefined; +async function getFrameDetails(): Promise<{ cols: number, rows: number, decoded: IImage32 }> { + const screenshotOptions: LocatorScreenshotOptions | undefined = process.env.DEBUG ? { path: 'out-test/playwright/screenshot.png' } : undefined; + const buffer = await ctx.page.locator('#terminal-container .xterm-screen').screenshot(screenshotOptions); + frameDetails = { + cols: await ctx.proxy.cols, + rows: await ctx.proxy.rows, + decoded: (await decodePng(buffer, { force32: true })).image + }; + return frameDetails; +} + +function getCellColorInner(frameDetails: { cols: number, rows: number, decoded: IImage32 }, col: number, row: number): [red: number, green: number, blue: number, alpha: number] { + const cellSize = { + width: frameDetails.decoded.width / frameDetails.cols, + height: frameDetails.decoded.height / frameDetails.rows + }; + const x = Math.floor((col - 1/* 1- to 0-based index */ + 0.5/* middle of cell */) * cellSize.width); + const y = Math.floor((row - 1/* 1- to 0-based index */ + 0.5/* middle of cell */) * cellSize.height); + const i = (y * frameDetails.decoded.width + x) * 4/* 4 channels per pixel */; + return Array.from(frameDetails.decoded.data.slice(i, i + 4)) as [number, number, number, number]; +} const COLORS_16_TO_255 = [ '#000000', '#00005f', '#000087', '#0000af', '#0000d7', '#0000ff', '#005f00', '#005f5f', '#005f87', '#005faf', '#005fd7', '#005fff', '#008700', '#00875f', '#008787', '#0087af', diff --git a/addons/xterm-addon-webgl/test/tsconfig.json b/addons/xterm-addon-webgl/test/tsconfig.json index 8b951c40..8a4135a6 100644 --- a/addons/xterm-addon-webgl/test/tsconfig.json +++ b/addons/xterm-addon-webgl/test/tsconfig.json @@ -21,6 +21,7 @@ "strict": true, "types": [ "../../../node_modules/@types/node", + "../../../node_modules/@lunapaint/png-codec", "../../../out-test/playwright/TestUtils" ] }, diff --git a/bin/test_playwright.js b/bin/test_playwright.js index bc4e7b02..809f29a8 100644 --- a/bin/test_playwright.js +++ b/bin/test_playwright.js @@ -9,7 +9,7 @@ const cp = require('child_process'); const path = require('path'); const configs = [ - { name: 'core', path: 'out-test/playwright/playwright.config.js' }, + // { name: 'core', path: 'out-test/playwright/playwright.config.js' }, { name: 'xterm-addon-webgl', path: 'addons/xterm-addon-webgl/out-test/playwright.config.js' } ]; From cdf02d54dadc7b625c54e6561b6044a57d69a900 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 10:22:13 -0700 Subject: [PATCH 55/99] Share test code between dom and webgl --- .../test/WebglRenderer.test.ts | 1128 +---------------- addons/xterm-addon-webgl/test/tsconfig.json | 3 +- bin/test_playwright.js | 2 +- test/playwright/Renderer.test.ts | 1118 +--------------- test/playwright/SharedRendererTests.ts | 1122 ++++++++++++++++ 5 files changed, 1141 insertions(+), 2232 deletions(-) create mode 100644 test/playwright/SharedRendererTests.ts diff --git a/addons/xterm-addon-webgl/test/WebglRenderer.test.ts b/addons/xterm-addon-webgl/test/WebglRenderer.test.ts index 9c0418b4..5fc5d300 100644 --- a/addons/xterm-addon-webgl/test/WebglRenderer.test.ts +++ b/addons/xterm-addon-webgl/test/WebglRenderer.test.ts @@ -3,16 +3,17 @@ * @license MIT */ -import { assert } from 'chai'; -import test, { LocatorScreenshotOptions } from '@playwright/test'; -import { ITheme } from 'xterm'; -import { ITestContext, MaybeAsync, createTestContext, openTerminal, pollFor } from '../../../out-test/playwright/TestUtils'; -import { IImage32, decodePng } from '@lunapaint/png-codec'; +import test from '@playwright/test'; +import { strictEqual } from 'assert'; +import { injectSharedRendererTests } from '../../../out-test/playwright/SharedRendererTests'; +import { ITestContext, createTestContext, openTerminal } from '../../../out-test/playwright/TestUtils'; let ctx: ITestContext; +const ctxWrapper: { value: ITestContext } = { value: undefined } as any; test.beforeAll(async ({ browser }) => { ctx = await createTestContext(browser); await openTerminal(ctx); + ctxWrapper.value = ctx; await ctx.page.evaluate(` window.addon = new WebglAddon(true); window.term.loadAddon(window.addon); @@ -20,22 +21,11 @@ test.beforeAll(async ({ browser }) => { }); test.afterAll(async () => await ctx.page.close()); -test.describe.only('WebGL Renderer Integration Tests', async () => { - test.beforeEach(async () => { - await ctx.proxy.reset(); - ctx.page.evaluate(` - window.term.options.minimumContrastRatio = 1; - window.term.options.allowTransparency = false; - `); - // TODO: Introduce frame caching - // Clear the cached screenshot before each test - frameDetails = undefined; - }); - +test.describe('WebGL Renderer Integration Tests', async () => { test('dispose removes renderer canvases', async function(): Promise { - assert.equal(await ctx.page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 2); + strictEqual(await ctx.page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 2); await ctx.page.evaluate(`addon.dispose()`); - assert.equal(await ctx.page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 0); + strictEqual(await ctx.page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 0); // Re-create webgl addon to avoid side effects impacting other tests await ctx.page.evaluate(` window.addon = new WebglAddon(true); @@ -43,1103 +33,5 @@ test.describe.only('WebGL Renderer Integration Tests', async () => { `); }); - test.describe('colors', () => { - test('foreground 0-15', async () => { - const theme: ITheme = { - black: '#010203', - red: '#040506', - green: '#070809', - yellow: '#0a0b0c', - blue: '#0d0e0f', - magenta: '#101112', - cyan: '#131415', - white: '#161718' - }; - await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await ctx.proxy.write(`\x1b[30m█\x1b[31m█\x1b[32m█\x1b[33m█\x1b[34m█\x1b[35m█\x1b[36m█\x1b[37m█`); - await pollFor(ctx.page, () => getCellColor(1, 1), [1, 2, 3, 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [4, 5, 6, 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [7, 8, 9, 255]); - await pollFor(ctx.page, () => getCellColor(4, 1), [10, 11, 12, 255]); - await pollFor(ctx.page, () => getCellColor(5, 1), [13, 14, 15, 255]); - await pollFor(ctx.page, () => getCellColor(6, 1), [16, 17, 18, 255]); - await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); - await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); - }); - - test('foreground 0-7 drawBoldTextInBrightColors', async () => { - const theme: ITheme = { - brightBlack: '#010203', - brightRed: '#040506', - brightGreen: '#070809', - brightYellow: '#0a0b0c', - brightBlue: '#0d0e0f', - brightMagenta: '#101112', - brightCyan: '#131415', - brightWhite: '#161718' - }; - await ctx.page.evaluate(` - window.term.options.theme = ${JSON.stringify(theme)}; - window.term.options.drawBoldTextInBrightColors = true; - `); - await ctx.proxy.write(`\x1b[1;30m█\x1b[1;31m█\x1b[1;32m█\x1b[1;33m█\x1b[1;34m█\x1b[1;35m█\x1b[1;36m█\x1b[1;37m█`); - await pollFor(ctx.page, () => getCellColor(1, 1), [1, 2, 3, 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [4, 5, 6, 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [7, 8, 9, 255]); - await pollFor(ctx.page, () => getCellColor(4, 1), [10, 11, 12, 255]); - await pollFor(ctx.page, () => getCellColor(5, 1), [13, 14, 15, 255]); - await pollFor(ctx.page, () => getCellColor(6, 1), [16, 17, 18, 255]); - await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); - await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); - }); - - test('background 0-15', async () => { - const theme: ITheme = { - black: '#010203', - red: '#040506', - green: '#070809', - yellow: '#0a0b0c', - blue: '#0d0e0f', - magenta: '#101112', - cyan: '#131415', - white: '#161718' - }; - await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await ctx.proxy.write(`\x1b[40m \x1b[41m \x1b[42m \x1b[43m \x1b[44m \x1b[45m \x1b[46m \x1b[47m `); - await pollFor(ctx.page, () => getCellColor(1, 1), [1, 2, 3, 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [4, 5, 6, 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [7, 8, 9, 255]); - await pollFor(ctx.page, () => getCellColor(4, 1), [10, 11, 12, 255]); - await pollFor(ctx.page, () => getCellColor(5, 1), [13, 14, 15, 255]); - await pollFor(ctx.page, () => getCellColor(6, 1), [16, 17, 18, 255]); - await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); - await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); - }); - - test('foreground 0-15 inverse', async () => { - const theme: ITheme = { - black: '#010203', - red: '#040506', - green: '#070809', - yellow: '#0a0b0c', - blue: '#0d0e0f', - magenta: '#101112', - cyan: '#131415', - white: '#161718' - }; - await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await ctx.proxy.write(`\x1b[7;30m \x1b[7;31m \x1b[7;32m \x1b[7;33m \x1b[7;34m \x1b[7;35m \x1b[7;36m \x1b[7;37m `); - await pollFor(ctx.page, () => getCellColor(1, 1), [1, 2, 3, 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [4, 5, 6, 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [7, 8, 9, 255]); - await pollFor(ctx.page, () => getCellColor(4, 1), [10, 11, 12, 255]); - await pollFor(ctx.page, () => getCellColor(5, 1), [13, 14, 15, 255]); - await pollFor(ctx.page, () => getCellColor(6, 1), [16, 17, 18, 255]); - await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); - await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); - }); - - test('background 0-15 inverse', async () => { - const theme: ITheme = { - black: '#010203', - red: '#040506', - green: '#070809', - yellow: '#0a0b0c', - blue: '#0d0e0f', - magenta: '#101112', - cyan: '#131415', - white: '#161718' - }; - await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await ctx.proxy.write(`\x1b[7;40m█\x1b[7;41m█\x1b[7;42m█\x1b[7;43m█\x1b[7;44m█\x1b[7;45m█\x1b[7;46m█\x1b[7;47m█`); - await pollFor(ctx.page, () => getCellColor(1, 1), [1, 2, 3, 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [4, 5, 6, 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [7, 8, 9, 255]); - await pollFor(ctx.page, () => getCellColor(4, 1), [10, 11, 12, 255]); - await pollFor(ctx.page, () => getCellColor(5, 1), [13, 14, 15, 255]); - await pollFor(ctx.page, () => getCellColor(6, 1), [16, 17, 18, 255]); - await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); - await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); - }); - - test('foreground 0-15 inivisible', async () => { - const theme: ITheme = { - black: '#010203', - red: '#040506', - green: '#070809', - yellow: '#0a0b0c', - blue: '#0d0e0f', - magenta: '#101112', - cyan: '#131415', - white: '#161718' - }; - await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await ctx.proxy.write(`\x1b[8;30m \x1b[8;31m \x1b[8;32m \x1b[8;33m \x1b[8;34m \x1b[8;35m \x1b[8;36m \x1b[8;37m `); - await pollFor(ctx.page, () => getCellColor(1, 1), [0, 0, 0, 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [0, 0, 0, 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [0, 0, 0, 255]); - await pollFor(ctx.page, () => getCellColor(4, 1), [0, 0, 0, 255]); - await pollFor(ctx.page, () => getCellColor(5, 1), [0, 0, 0, 255]); - await pollFor(ctx.page, () => getCellColor(6, 1), [0, 0, 0, 255]); - await pollFor(ctx.page, () => getCellColor(7, 1), [0, 0, 0, 255]); - await pollFor(ctx.page, () => getCellColor(8, 1), [0, 0, 0, 255]); - }); - - test('background 0-15 inivisible', async () => { - const theme: ITheme = { - black: '#010203', - red: '#040506', - green: '#070809', - yellow: '#0a0b0c', - blue: '#0d0e0f', - magenta: '#101112', - cyan: '#131415', - white: '#161718' - }; - await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await ctx.proxy.write(`\x1b[8;40m█\x1b[8;41m█\x1b[8;42m█\x1b[8;43m█\x1b[8;44m█\x1b[8;45m█\x1b[8;46m█\x1b[8;47m█`); - await pollFor(ctx.page, () => getCellColor(1, 1), [1, 2, 3, 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [4, 5, 6, 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [7, 8, 9, 255]); - await pollFor(ctx.page, () => getCellColor(4, 1), [10, 11, 12, 255]); - await pollFor(ctx.page, () => getCellColor(5, 1), [13, 14, 15, 255]); - await pollFor(ctx.page, () => getCellColor(6, 1), [16, 17, 18, 255]); - await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); - await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); - }); - - test('foreground 0-15 bright', async () => { - const theme: ITheme = { - brightBlack: '#010203', - brightRed: '#040506', - brightGreen: '#070809', - brightYellow: '#0a0b0c', - brightBlue: '#0d0e0f', - brightMagenta: '#101112', - brightCyan: '#131415', - brightWhite: '#161718' - }; - await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await ctx.proxy.write(`\x1b[90m█\x1b[91m█\x1b[92m█\x1b[93m█\x1b[94m█\x1b[95m█\x1b[96m█\x1b[97m█`); - await pollFor(ctx.page, () => getCellColor(1, 1), [1, 2, 3, 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [4, 5, 6, 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [7, 8, 9, 255]); - await pollFor(ctx.page, () => getCellColor(4, 1), [10, 11, 12, 255]); - await pollFor(ctx.page, () => getCellColor(5, 1), [13, 14, 15, 255]); - await pollFor(ctx.page, () => getCellColor(6, 1), [16, 17, 18, 255]); - await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); - await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); - }); - - test('background 0-15 bright', async () => { - const theme: ITheme = { - brightBlack: '#010203', - brightRed: '#040506', - brightGreen: '#070809', - brightYellow: '#0a0b0c', - brightBlue: '#0d0e0f', - brightMagenta: '#101112', - brightCyan: '#131415', - brightWhite: '#161718' - }; - await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await ctx.proxy.write(`\x1b[100m \x1b[101m \x1b[102m \x1b[103m \x1b[104m \x1b[105m \x1b[106m \x1b[107m `); - await pollFor(ctx.page, () => getCellColor(1, 1), [1, 2, 3, 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [4, 5, 6, 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [7, 8, 9, 255]); - await pollFor(ctx.page, () => getCellColor(4, 1), [10, 11, 12, 255]); - await pollFor(ctx.page, () => getCellColor(5, 1), [13, 14, 15, 255]); - await pollFor(ctx.page, () => getCellColor(6, 1), [16, 17, 18, 255]); - await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); - await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); - }); - - test('foreground 16-255', async () => { - let data = ''; - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - data += `\x1b[38;5;${16 + y * 16 + x}m█\x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - const cssColor = COLORS_16_TO_255[y * 16 + x]; - const r = parseInt(cssColor.slice(1, 3), 16); - const g = parseInt(cssColor.slice(3, 5), 16); - const b = parseInt(cssColor.slice(5, 7), 16); - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); - } - } - }); - - test('background 16-255', async () => { - let data = ''; - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - data += `\x1b[48;5;${16 + y * 16 + x}m \x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - const cssColor = COLORS_16_TO_255[y * 16 + x]; - const r = parseInt(cssColor.slice(1, 3), 16); - const g = parseInt(cssColor.slice(3, 5), 16); - const b = parseInt(cssColor.slice(5, 7), 16); - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); - } - } - }); - - test('foreground 16-255 inverse', async () => { - let data = ''; - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - data += `\x1b[7;38;5;${16 + y * 16 + x}m \x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - const cssColor = COLORS_16_TO_255[y * 16 + x]; - const r = parseInt(cssColor.slice(1, 3), 16); - const g = parseInt(cssColor.slice(3, 5), 16); - const b = parseInt(cssColor.slice(5, 7), 16); - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); - } - } - }); - - test('background 16-255 inverse', async () => { - let data = ''; - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - data += `\x1b[7;48;5;${16 + y * 16 + x}m█\x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - const cssColor = COLORS_16_TO_255[y * 16 + x]; - const r = parseInt(cssColor.slice(1, 3), 16); - const g = parseInt(cssColor.slice(3, 5), 16); - const b = parseInt(cssColor.slice(5, 7), 16); - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); - } - } - }); - - test('foreground 16-255 invisible', async () => { - let data = ''; - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - data += `\x1b[8;38;5;${16 + y * 16 + x}m \x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, 0, 0, 255]); - } - } - }); - - test('background 16-255 invisible', async () => { - let data = ''; - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - data += `\x1b[8;48;5;${16 + y * 16 + x}m█\x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - const cssColor = COLORS_16_TO_255[y * 16 + x]; - const r = parseInt(cssColor.slice(1, 3), 16); - const g = parseInt(cssColor.slice(3, 5), 16); - const b = parseInt(cssColor.slice(5, 7), 16); - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); - } - } - }); - - test('foreground 16-255 dim', async () => { - let data = ''; - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - data += `\x1b[2;38;5;${16 + y * 16 + x}m█\x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - const cssColor = COLORS_16_TO_255[y * 16 + x]; - const r = parseInt(cssColor.slice(1, 3), 16); - const g = parseInt(cssColor.slice(3, 5), 16); - const b = parseInt(cssColor.slice(5, 7), 16); - // It's difficult to assert the exact color due to rounding, just ensure the color differs - // to the regular color - await pollFor(ctx.page, async () => { - const c = await getCellColor(x + 1, y + 1); - return ( - (c[0] === 0 || c[0] !== r) && - (c[1] === 0 || c[1] !== g) && - (c[2] === 0 || c[2] !== b) - ); - }, true); - } - } - }); - - test('background 16-255 dim', async () => { - let data = ''; - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - data += `\x1b[2;48;5;${16 + y * 16 + x}m \x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - const cssColor = COLORS_16_TO_255[y * 16 + x]; - const r = parseInt(cssColor.slice(1, 3), 16); - const g = parseInt(cssColor.slice(3, 5), 16); - const b = parseInt(cssColor.slice(5, 7), 16); - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); - } - } - }); - - test('foreground true color red', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[38;2;${i};0;0m█\x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]); - } - } - }); - - test('background true color red', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[48;2;${i};0;0m \x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]); - } - } - }); - - test('foreground true color green', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[38;2;0;${i};0m█\x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]); - } - } - }); - - test('background true color green', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[48;2;0;${i};0m \x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]); - } - } - }); - - test('foreground true color blue', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[38;2;0;0;${i}m█\x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]); - } - } - }); - - test('background true color blue', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[48;2;0;0;${i}m \x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]); - } - } - }); - - test('foreground true color grey', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[38;2;${i};${i};${i}m█\x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); - } - } - }); - - test('background true color grey', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[48;2;${i};${i};${i}m \x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); - } - } - }); - - test('foreground true color red inverse', async function(): Promise { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[7;38;2;${i};0;0m \x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]); - } - } - }); - - test('background true color red inverse', async function(): Promise { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[7;48;2;${i};0;0m█\x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]); - } - } - }); - - test('foreground true color green inverse', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[7;38;2;0;${i};0m \x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]); - } - } - }); - - test('background true color green inverse', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[7;48;2;0;${i};0m█\x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]); - } - } - }); - - test('foreground true color blue inverse', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[7;38;2;0;0;${i}m \x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]); - } - } - }); - - test('background true color blue inverse', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[7;48;2;0;0;${i}m█\x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]); - } - } - }); - - test('foreground true color grey inverse', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[7;38;2;${i};${i};${i}m \x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); - } - } - }); - - test('background true color grey inverse', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[7;48;2;${i};${i};${i}m█\x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); - } - } - }); - - test('foreground true color grey invisible', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[8;38;2;${i};${i};${i}m \x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, 0, 0, 255]); - } - } - }); - - test('background true color grey invisible', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[8;48;2;${i};${i};${i}m█\x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); - } - } - }); - }); - - test.describe('minimumContrastRatio', async () => { - test('should adjust 0-15 colors on black background', async () => { - const theme: ITheme = { - black: '#2e3436', - red: '#cc0000', - green: '#4e9a06', - yellow: '#c4a000', - blue: '#3465a4', - magenta: '#75507b', - cyan: '#06989a', - white: '#d3d7cf', - brightBlack: '#555753', - brightRed: '#ef2929', - brightGreen: '#8ae234', - brightYellow: '#fce94f', - brightBlue: '#729fcf', - brightMagenta: '#ad7fa8', - brightCyan: '#34e2e2', - brightWhite: '#eeeeec' - }; - await ctx.page.evaluate(` - window.term.options.theme = ${JSON.stringify(theme)}; - window.term.options.minimumContrastRatio = 1; - `); - // Block elements ignore minimum contrast ratio so a different char is used here - await ctx.proxy.write( - `\x1b[30m■\x1b[31m■\x1b[32m■\x1b[33m■\x1b[34m■\x1b[35m■\x1b[36m■\x1b[37m■\r\n` + - `\x1b[90m■\x1b[91m■\x1b[92m■\x1b[93m■\x1b[94m■\x1b[95m■\x1b[96m■\x1b[97m■` - ); - // Validate before minimumContrastRatio is applied - await pollFor(ctx.page, () => getCellColor(1, 1), [0x2e, 0x34, 0x36, 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [0xcc, 0x00, 0x00, 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [0x4e, 0x9a, 0x06, 255]); - await pollFor(ctx.page, () => getCellColor(4, 1), [0xc4, 0xa0, 0x00, 255]); - await pollFor(ctx.page, () => getCellColor(5, 1), [0x34, 0x65, 0xa4, 255]); - await pollFor(ctx.page, () => getCellColor(6, 1), [0x75, 0x50, 0x7b, 255]); - await pollFor(ctx.page, () => getCellColor(7, 1), [0x06, 0x98, 0x9a, 255]); - await pollFor(ctx.page, () => getCellColor(8, 1), [0xd3, 0xd7, 0xcf, 255]); - await pollFor(ctx.page, () => getCellColor(1, 2), [0x55, 0x57, 0x53, 255]); - await pollFor(ctx.page, () => getCellColor(2, 2), [0xef, 0x29, 0x29, 255]); - await pollFor(ctx.page, () => getCellColor(3, 2), [0x8a, 0xe2, 0x34, 255]); - await pollFor(ctx.page, () => getCellColor(4, 2), [0xfc, 0xe9, 0x4f, 255]); - await pollFor(ctx.page, () => getCellColor(5, 2), [0x72, 0x9f, 0xcf, 255]); - await pollFor(ctx.page, () => getCellColor(6, 2), [0xad, 0x7f, 0xa8, 255]); - await pollFor(ctx.page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]); - await pollFor(ctx.page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]); - // Setting and check for minimum contrast values, note that these are not - // exact to the contrast ratio, if the increase luminance algorithm - // changes then these will probably fail - await ctx.page.evaluate(`window.term.options.minimumContrastRatio = 10;`); - frameDetails = undefined; - await pollFor(ctx.page, () => getCellColor(1, 1), [176, 180, 180, 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [238, 158, 158, 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [152, 198, 110, 255]); - await pollFor(ctx.page, () => getCellColor(4, 1), [208, 179, 49, 255]); - await pollFor(ctx.page, () => getCellColor(5, 1), [161, 183, 215, 255]); - await pollFor(ctx.page, () => getCellColor(6, 1), [191, 174, 194, 255]); - await pollFor(ctx.page, () => getCellColor(7, 1), [110, 197, 198, 255]); - await pollFor(ctx.page, () => getCellColor(8, 1), [211, 215, 207, 255]); - await pollFor(ctx.page, () => getCellColor(1, 2), [183, 185, 183, 255]); - await pollFor(ctx.page, () => getCellColor(2, 2), [249, 156, 156, 255]); - await pollFor(ctx.page, () => getCellColor(3, 2), [138, 226, 52, 255]); - await pollFor(ctx.page, () => getCellColor(4, 2), [252, 233, 79, 255]); - await pollFor(ctx.page, () => getCellColor(5, 2), [154, 186, 221, 255]); - await pollFor(ctx.page, () => getCellColor(6, 2), [203, 173, 199, 255]); - // Unchanged - await pollFor(ctx.page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]); - await pollFor(ctx.page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]); - }); - - test('should adjust 0-15 colors on white background', async () => { - const theme: ITheme = { - background: '#ffffff', - black: '#2e3436', - red: '#cc0000', - green: '#4e9a06', - yellow: '#c4a000', - blue: '#3465a4', - magenta: '#75507b', - cyan: '#06989a', - white: '#d3d7cf', - brightBlack: '#555753', - brightRed: '#ef2929', - brightGreen: '#8ae234', - brightYellow: '#fce94f', - brightBlue: '#729fcf', - brightMagenta: '#ad7fa8', - brightCyan: '#34e2e2', - brightWhite: '#eeeeec' - }; - await ctx.page.evaluate(` - window.term.options.theme = ${JSON.stringify(theme)}; - window.term.options.minimumContrastRatio = 1; - `); - // Block elements ignore minimum contrast ratio so a different char is used here - await ctx.proxy.write( - `\x1b[30m■\x1b[31m■\x1b[32m■\x1b[33m■\x1b[34m■\x1b[35m■\x1b[36m■\x1b[37m■\r\n` + - `\x1b[90m■\x1b[91m■\x1b[92m■\x1b[93m■\x1b[94m■\x1b[95m■\x1b[96m■\x1b[97m■` - ); - // Validate before minimumContrastRatio is applied - await pollFor(ctx.page, () => getCellColor(1, 1), [0x2e, 0x34, 0x36, 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [0xcc, 0x00, 0x00, 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [0x4e, 0x9a, 0x06, 255]); - await pollFor(ctx.page, () => getCellColor(4, 1), [0xc4, 0xa0, 0x00, 255]); - await pollFor(ctx.page, () => getCellColor(5, 1), [0x34, 0x65, 0xa4, 255]); - await pollFor(ctx.page, () => getCellColor(6, 1), [0x75, 0x50, 0x7b, 255]); - await pollFor(ctx.page, () => getCellColor(7, 1), [0x06, 0x98, 0x9a, 255]); - await pollFor(ctx.page, () => getCellColor(8, 1), [0xd3, 0xd7, 0xcf, 255]); - await pollFor(ctx.page, () => getCellColor(1, 2), [0x55, 0x57, 0x53, 255]); - await pollFor(ctx.page, () => getCellColor(2, 2), [0xef, 0x29, 0x29, 255]); - await pollFor(ctx.page, () => getCellColor(3, 2), [0x8a, 0xe2, 0x34, 255]); - await pollFor(ctx.page, () => getCellColor(4, 2), [0xfc, 0xe9, 0x4f, 255]); - await pollFor(ctx.page, () => getCellColor(5, 2), [0x72, 0x9f, 0xcf, 255]); - await pollFor(ctx.page, () => getCellColor(6, 2), [0xad, 0x7f, 0xa8, 255]); - await pollFor(ctx.page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]); - await pollFor(ctx.page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]); - // Setting and check for minimum contrast values, note that these are not - // exact to the contrast ratio, if the increase luminance algorithm - // changes then these will probably fail - await ctx.page.evaluate(`window.term.options.minimumContrastRatio = 10;`); - frameDetails = undefined; - await pollFor(ctx.page, () => getCellColor(1, 1), [46, 52, 54, 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [132, 0, 0, 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [36, 72, 0, 255]); - await pollFor(ctx.page, () => getCellColor(4, 1), [72, 59, 0, 255]); - await pollFor(ctx.page, () => getCellColor(5, 1), [32, 64, 106, 255]); - await pollFor(ctx.page, () => getCellColor(6, 1), [75, 51, 80, 255]); - await pollFor(ctx.page, () => getCellColor(7, 1), [0, 71, 72, 255]); - await pollFor(ctx.page, () => getCellColor(8, 1), [64, 64, 63, 255]); - await pollFor(ctx.page, () => getCellColor(1, 2), [61, 63, 59, 255]); - await pollFor(ctx.page, () => getCellColor(2, 2), [125, 19, 19, 255]); - await pollFor(ctx.page, () => getCellColor(3, 2), [40, 67, 13, 255]); - await pollFor(ctx.page, () => getCellColor(4, 2), [67, 63, 19, 255]); - await pollFor(ctx.page, () => getCellColor(5, 2), [45, 65, 87, 255]); - await pollFor(ctx.page, () => getCellColor(6, 2), [81, 57, 78, 255]); - await pollFor(ctx.page, () => getCellColor(7, 2), [13, 67, 67, 255]); - await pollFor(ctx.page, () => getCellColor(8, 2), [64, 64, 64, 255]); - }); - - test('should enforce half the contrast for dim cells', async () => { - // TODO: This test fails on webkit - if (ctx.browser.browserType().name() === 'webkit') { - test.skip(); - return; - } - const theme: ITheme = { - background: '#ffffff', - black: '#2e3436', - red: '#cc0000', - green: '#4e9a06', - yellow: '#c4a000', - blue: '#3465a4', - magenta: '#75507b', - cyan: '#06989a', - white: '#d3d7cf', - brightBlack: '#555753', - brightRed: '#ef2929', - brightGreen: '#8ae234', - brightYellow: '#fce94f', - brightBlue: '#729fcf', - brightMagenta: '#ad7fa8', - brightCyan: '#34e2e2', - brightWhite: '#eeeeec' - }; - await ctx.page.evaluate(` - window.term.options.theme = ${JSON.stringify(theme)}; - window.term.options.minimumContrastRatio = 1; - `); - // Block elements ignore minimum contrast ratio so a different char is used here - await ctx.proxy.write( - '\x1b[2m' + - `\x1b[30m■\x1b[31m■\x1b[32m■\x1b[33m■\x1b[34m■\x1b[35m■\x1b[36m■\x1b[37m■\r\n` + - `\x1b[90m■\x1b[91m■\x1b[92m■\x1b[93m■\x1b[94m■\x1b[95m■\x1b[96m■\x1b[97m■` - ); - // Validate before minimumContrastRatio is applied - await pollFor(ctx.page, () => getCellColor(1, 1), [Math.floor((255 + 0x2e) / 2), Math.floor((255 + 0x34) / 2), Math.floor((255 + 0x36) / 2), 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [Math.floor((255 + 0xcc) / 2), Math.floor((255 + 0x00) / 2), Math.floor((255 + 0x00) / 2), 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [Math.floor((255 + 0x4e) / 2), Math.floor((255 + 0x9a) / 2), Math.floor((255 + 0x06) / 2), 255]); - await pollFor(ctx.page, () => getCellColor(4, 1), [Math.floor((255 + 0xc4) / 2), Math.floor((255 + 0xa0) / 2), Math.floor((255 + 0x00) / 2), 255]); - await pollFor(ctx.page, () => getCellColor(5, 1), [Math.floor((255 + 0x34) / 2), Math.floor((255 + 0x65) / 2), Math.floor((255 + 0xa4) / 2), 255]); - await pollFor(ctx.page, () => getCellColor(6, 1), [Math.floor((255 + 0x75) / 2), Math.floor((255 + 0x50) / 2), Math.floor((255 + 0x7b) / 2), 255]); - await pollFor(ctx.page, () => getCellColor(7, 1), [Math.floor((255 + 0x06) / 2), Math.floor((255 + 0x98) / 2), Math.floor((255 + 0x9a) / 2), 255]); - await pollFor(ctx.page, () => getCellColor(8, 1), [Math.floor((255 + 0xd3) / 2), Math.floor((255 + 0xd7) / 2), Math.floor((255 + 0xcf) / 2), 255]); - await pollFor(ctx.page, () => getCellColor(1, 2), [Math.floor((255 + 0x55) / 2), Math.floor((255 + 0x57) / 2), Math.floor((255 + 0x53) / 2), 255]); - await pollFor(ctx.page, () => getCellColor(2, 2), [Math.floor((255 + 0xef) / 2), Math.floor((255 + 0x29) / 2), Math.floor((255 + 0x29) / 2), 255]); - await pollFor(ctx.page, () => getCellColor(3, 2), [Math.floor((255 + 0x8a) / 2), Math.floor((255 + 0xe2) / 2), Math.floor((255 + 0x34) / 2), 255]); - await pollFor(ctx.page, () => getCellColor(4, 2), [Math.floor((255 + 0xfc) / 2), Math.floor((255 + 0xe9) / 2), Math.floor((255 + 0x4f) / 2), 255]); - await pollFor(ctx.page, () => getCellColor(5, 2), [Math.floor((255 + 0x72) / 2), Math.floor((255 + 0x9f) / 2), Math.floor((255 + 0xcf) / 2), 255]); - await pollFor(ctx.page, () => getCellColor(6, 2), [Math.floor((255 + 0xad) / 2), Math.floor((255 + 0x7f) / 2), Math.floor((255 + 0xa8) / 2), 255]); - await pollFor(ctx.page, () => getCellColor(7, 2), [Math.floor((255 + 0x34) / 2), Math.floor((255 + 0xe2) / 2), Math.floor((255 + 0xe2) / 2), 255]); - await pollFor(ctx.page, () => getCellColor(8, 2), [Math.floor((255 + 0xee) / 2), Math.floor((255 + 0xee) / 2), Math.floor((255 + 0xec) / 2), 255]); - // Setting and check for minimum contrast values, note that these are not - // exact to the contrast ratio, if the increase luminance algorithm - // changes then these will probably fail - await ctx.page.evaluate(`window.term.options.minimumContrastRatio = 10;`); - frameDetails = undefined; - await pollFor(ctx.page, () => getCellColor(1, 1), [150, 153, 154, 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [229, 127, 127, 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [63, 124, 4, 255]); - await pollFor(ctx.page, () => getCellColor(4, 1), [127, 104, 0, 255]); - await pollFor(ctx.page, () => getCellColor(5, 1), [153, 178, 209, 255]); - await pollFor(ctx.page, () => getCellColor(6, 1), [186, 167, 189, 255]); - await pollFor(ctx.page, () => getCellColor(7, 1), [4, 122, 124, 255]); - await pollFor(ctx.page, () => getCellColor(8, 1), [110, 112, 108, 255]); - await pollFor(ctx.page, () => getCellColor(1, 2), [170, 171, 169, 255]); - await pollFor(ctx.page, () => getCellColor(2, 2), [215, 36, 36, 255]); - await pollFor(ctx.page, () => getCellColor(3, 2), [72, 117, 25, 255]); - await pollFor(ctx.page, () => getCellColor(4, 2), [117, 109, 36, 255]); - await pollFor(ctx.page, () => getCellColor(5, 2), [72, 103, 135, 255]); - await pollFor(ctx.page, () => getCellColor(6, 2), [125, 91, 121, 255]); - await pollFor(ctx.page, () => getCellColor(7, 2), [25, 117, 117, 255]); - await pollFor(ctx.page, () => getCellColor(8, 2), [111, 111, 110, 255]); - }); - }); - - test.describe('selectionBackground', async () => { - test('should resolve the inverse foreground color based on the original background color, not the selection', async () => { - const theme: ITheme = { - foreground: '#FF0000', - background: '#00FF00', - selectionBackground: '#0000FF' - }; - await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await ctx.proxy.write(` █\x1b[7m█\x1b[0m`); - await pollFor(ctx.page, () => getCellColor(1, 1), [0, 255, 0, 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [255, 0, 0, 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [0, 255, 0, 255]); - await ctx.page.evaluate(`window.term.selectAll()`); - frameDetails = undefined; - // Selection only cell needs to be first to ensure renderer has kicked in - await pollFor(ctx.page, () => getCellColor(1, 1), [0, 0, 255, 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [255, 0, 0, 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [0, 255, 0, 255]); - }); - }); - - test.describe('allowTransparency', async () => { - test.beforeEach(() => ctx.page.evaluate(`term.options.allowTransparency = true`)); - - test('transparent background inverse', async () => { - const theme: ITheme = { - background: '#ff000080' - }; - await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - const data = `\x1b[7m█\x1b[0m`; - await ctx.proxy.write(data); - // Inverse background should be opaque - await pollFor(ctx.page, () => getCellColor(1, 1), [255, 0, 0, 255]); - }); - }); - - test.describe('selectionForeground', () => { - test('transparent background inverse', async () => { - const theme: ITheme = { - selectionForeground: '#ff0000' - }; - await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - const data = `\x1b[7m█\x1b[0m`; - await ctx.proxy.write(data); - await ctx.page.evaluate(`window.term.selectAll()`); - await pollFor(ctx.page, () => getCellColor(1, 1), [255, 0, 0, 255]); - }); - }); - - test.describe('decoration color overrides', async () => { - test('foregroundColor', async () => { - await ctx.page.evaluate(` - const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); - window.term.registerDecoration({ - marker, - foregroundColor: '#ff0000', - backgroundColor: '#0000ff' - }); - `); - const data = `█`; - await ctx.proxy.write(data); - await pollFor(ctx.page, () => getCellColor(1, 1), [255, 0, 0, 255]); - }); - test('foregroundColor should ignore inverse', async () => { - await ctx.page.evaluate(` - const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); - window.term.registerDecoration({ - marker, - foregroundColor: '#ff0000', - backgroundColor: '#0000ff' - }); - `); - const data = `\x1b[7m█\x1b[0m`; - await ctx.proxy.write(data); - await pollFor(ctx.page, () => getCellColor(1, 1), [255, 0, 0, 255]); - }); - test('foregroundColor should ignore inverse (only fg on decoration)', async () => { - await ctx.page.evaluate(` - const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); - window.term.registerDecoration({ - marker, - width: 2, - foregroundColor: '#ff0000' - }); - `); - const data = `\x1b[7m█ \x1b[0m`; - await ctx.proxy.write(data); - await pollFor(ctx.page, () => getCellColor(1, 1), [255, 0, 0, 255]); // inverse foreground of '█' should be decoration fg override - await pollFor(ctx.page, () => getCellColor(2, 1), [255, 255, 255, 255]); // inverse background of ' ' should be default foreground - }); - test('backgroundColor', async () => { - await ctx.page.evaluate(` - const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); - window.term.registerDecoration({ - marker, - foregroundColor: '#ff0000', - backgroundColor: '#0000ff' - }); - `); - const data = ` `; - await ctx.proxy.write(data); - await pollFor(ctx.page, () => getCellColor(1, 1), [0, 0, 255, 255]); - }); - test('backgroundColor should ignore inverse', async () => { - await ctx.page.evaluate(` - const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); - window.term.registerDecoration({ - marker, - foregroundColor: '#ff0000', - backgroundColor: '#0000ff' - }); - `); - const data = `\x1b[7m \x1b[0m`; - await ctx.proxy.write(data); - await pollFor(ctx.page, () => getCellColor(1, 1), [0, 0, 255, 255]); - }); - test('backgroundColor should ignore inverse (only bg on decoration)', async () => { - const data = `\x1b[7m█ \x1b[0m`; - await ctx.proxy.write(data); - await ctx.page.evaluate(` - const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); - window.term.registerDecoration({ - marker, - width: 2, - backgroundColor: '#0000ff' - }); - `); - await pollFor(ctx.page, () => getCellColor(1, 1), [0, 0, 0, 255]); // inverse foreground of '█' should be default - await pollFor(ctx.page, () => getCellColor(2, 1), [0, 0, 255, 255]); // inverse background of ' ' should be decoration bg override - }); - }); + injectSharedRendererTests(ctxWrapper); }); - -/** - * Gets the color of the pixel in the center of a cell. - * @param col The 1-based column index to get the color for. - * @param row The 1-based row index to get the color for. - */ -function getCellColor(col: number, row: number): MaybeAsync<[red: number, green: number, blue: number, alpha: number]> { - if (!frameDetails) { - return getFrameDetails().then(frameDetails => getCellColorInner(frameDetails, col, row)); - } - return getCellColorInner(frameDetails, col, row); -} - -let frameDetails: { cols: number, rows: number, decoded: IImage32 } | undefined = undefined; -async function getFrameDetails(): Promise<{ cols: number, rows: number, decoded: IImage32 }> { - const screenshotOptions: LocatorScreenshotOptions | undefined = process.env.DEBUG ? { path: 'out-test/playwright/screenshot.png' } : undefined; - const buffer = await ctx.page.locator('#terminal-container .xterm-screen').screenshot(screenshotOptions); - frameDetails = { - cols: await ctx.proxy.cols, - rows: await ctx.proxy.rows, - decoded: (await decodePng(buffer, { force32: true })).image - }; - return frameDetails; -} - -function getCellColorInner(frameDetails: { cols: number, rows: number, decoded: IImage32 }, col: number, row: number): [red: number, green: number, blue: number, alpha: number] { - const cellSize = { - width: frameDetails.decoded.width / frameDetails.cols, - height: frameDetails.decoded.height / frameDetails.rows - }; - const x = Math.floor((col - 1/* 1- to 0-based index */ + 0.5/* middle of cell */) * cellSize.width); - const y = Math.floor((row - 1/* 1- to 0-based index */ + 0.5/* middle of cell */) * cellSize.height); - const i = (y * frameDetails.decoded.width + x) * 4/* 4 channels per pixel */; - return Array.from(frameDetails.decoded.data.slice(i, i + 4)) as [number, number, number, number]; -} - -const COLORS_16_TO_255 = [ - '#000000', '#00005f', '#000087', '#0000af', '#0000d7', '#0000ff', '#005f00', '#005f5f', '#005f87', '#005faf', '#005fd7', '#005fff', '#008700', '#00875f', '#008787', '#0087af', - '#0087d7', '#0087ff', '#00af00', '#00af5f', '#00af87', '#00afaf', '#00afd7', '#00afff', '#00d700', '#00d75f', '#00d787', '#00d7af', '#00d7d7', '#00d7ff', '#00ff00', '#00ff5f', - '#00ff87', '#00ffaf', '#00ffd7', '#00ffff', '#5f0000', '#5f005f', '#5f0087', '#5f00af', '#5f00d7', '#5f00ff', '#5f5f00', '#5f5f5f', '#5f5f87', '#5f5faf', '#5f5fd7', '#5f5fff', - '#5f8700', '#5f875f', '#5f8787', '#5f87af', '#5f87d7', '#5f87ff', '#5faf00', '#5faf5f', '#5faf87', '#5fafaf', '#5fafd7', '#5fafff', '#5fd700', '#5fd75f', '#5fd787', '#5fd7af', - '#5fd7d7', '#5fd7ff', '#5fff00', '#5fff5f', '#5fff87', '#5fffaf', '#5fffd7', '#5fffff', '#870000', '#87005f', '#870087', '#8700af', '#8700d7', '#8700ff', '#875f00', '#875f5f', - '#875f87', '#875faf', '#875fd7', '#875fff', '#878700', '#87875f', '#878787', '#8787af', '#8787d7', '#8787ff', '#87af00', '#87af5f', '#87af87', '#87afaf', '#87afd7', '#87afff', - '#87d700', '#87d75f', '#87d787', '#87d7af', '#87d7d7', '#87d7ff', '#87ff00', '#87ff5f', '#87ff87', '#87ffaf', '#87ffd7', '#87ffff', '#af0000', '#af005f', '#af0087', '#af00af', - '#af00d7', '#af00ff', '#af5f00', '#af5f5f', '#af5f87', '#af5faf', '#af5fd7', '#af5fff', '#af8700', '#af875f', '#af8787', '#af87af', '#af87d7', '#af87ff', '#afaf00', '#afaf5f', - '#afaf87', '#afafaf', '#afafd7', '#afafff', '#afd700', '#afd75f', '#afd787', '#afd7af', '#afd7d7', '#afd7ff', '#afff00', '#afff5f', '#afff87', '#afffaf', '#afffd7', '#afffff', - '#d70000', '#d7005f', '#d70087', '#d700af', '#d700d7', '#d700ff', '#d75f00', '#d75f5f', '#d75f87', '#d75faf', '#d75fd7', '#d75fff', '#d78700', '#d7875f', '#d78787', '#d787af', - '#d787d7', '#d787ff', '#d7af00', '#d7af5f', '#d7af87', '#d7afaf', '#d7afd7', '#d7afff', '#d7d700', '#d7d75f', '#d7d787', '#d7d7af', '#d7d7d7', '#d7d7ff', '#d7ff00', '#d7ff5f', - '#d7ff87', '#d7ffaf', '#d7ffd7', '#d7ffff', '#ff0000', '#ff005f', '#ff0087', '#ff00af', '#ff00d7', '#ff00ff', '#ff5f00', '#ff5f5f', '#ff5f87', '#ff5faf', '#ff5fd7', '#ff5fff', - '#ff8700', '#ff875f', '#ff8787', '#ff87af', '#ff87d7', '#ff87ff', '#ffaf00', '#ffaf5f', '#ffaf87', '#ffafaf', '#ffafd7', '#ffafff', '#ffd700', '#ffd75f', '#ffd787', '#ffd7af', - '#ffd7d7', '#ffd7ff', '#ffff00', '#ffff5f', '#ffff87', '#ffffaf', '#ffffd7', '#ffffff', '#080808', '#121212', '#1c1c1c', '#262626', '#303030', '#3a3a3a', '#444444', '#4e4e4e', - '#585858', '#626262', '#6c6c6c', '#767676', '#808080', '#8a8a8a', '#949494', '#9e9e9e', '#a8a8a8', '#b2b2b2', '#bcbcbc', '#c6c6c6', '#d0d0d0', '#dadada', '#e4e4e4', '#eeeeee' -]; diff --git a/addons/xterm-addon-webgl/test/tsconfig.json b/addons/xterm-addon-webgl/test/tsconfig.json index 8a4135a6..9523e64b 100644 --- a/addons/xterm-addon-webgl/test/tsconfig.json +++ b/addons/xterm-addon-webgl/test/tsconfig.json @@ -22,7 +22,8 @@ "types": [ "../../../node_modules/@types/node", "../../../node_modules/@lunapaint/png-codec", - "../../../out-test/playwright/TestUtils" + "../../../out-test/playwright/TestUtils", + "../../../out-test/playwright/SharedRendererTests" ] }, "include": [ diff --git a/bin/test_playwright.js b/bin/test_playwright.js index 809f29a8..bc4e7b02 100644 --- a/bin/test_playwright.js +++ b/bin/test_playwright.js @@ -9,7 +9,7 @@ const cp = require('child_process'); const path = require('path'); const configs = [ - // { name: 'core', path: 'out-test/playwright/playwright.config.js' }, + { name: 'core', path: 'out-test/playwright/playwright.config.js' }, { name: 'xterm-addon-webgl', path: 'addons/xterm-addon-webgl/out-test/playwright.config.js' } ]; diff --git a/test/playwright/Renderer.test.ts b/test/playwright/Renderer.test.ts index f407dc5d..8050001f 100644 --- a/test/playwright/Renderer.test.ts +++ b/test/playwright/Renderer.test.ts @@ -3,1126 +3,20 @@ * @license MIT */ -import { IImage32, decodePng } from '@lunapaint/png-codec'; -import { LocatorScreenshotOptions, test } from '@playwright/test'; -import { ITheme } from 'xterm'; -import { createTestContext, ITestContext, MaybeAsync, openTerminal, pollFor } from './TestUtils'; +import { test } from '@playwright/test'; +import { ITestContext, createTestContext, openTerminal } from './TestUtils'; +import { injectSharedRendererTests } from './SharedRendererTests'; let ctx: ITestContext; +const ctxWrapper: { value: ITestContext } = { value: undefined } as any; test.beforeAll(async ({ browser }) => { ctx = await createTestContext(browser); + ctxWrapper.value = ctx; await openTerminal(ctx); }); test.afterAll(async () => await ctx.page.close()); test.describe('DOM Renderer Integration Tests', async () => { - test.beforeEach(async () => { - await ctx.proxy.reset(); - ctx.page.evaluate(` - window.term.options.minimumContrastRatio = 1; - window.term.options.allowTransparency = false; - `); - // Clear the cached screenshot before each test - frameDetails = undefined; - }); - - test.describe('colors', () => { - test('foreground 0-15', async () => { - const theme: ITheme = { - black: '#010203', - red: '#040506', - green: '#070809', - yellow: '#0a0b0c', - blue: '#0d0e0f', - magenta: '#101112', - cyan: '#131415', - white: '#161718' - }; - await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await ctx.proxy.write(`\x1b[30m█\x1b[31m█\x1b[32m█\x1b[33m█\x1b[34m█\x1b[35m█\x1b[36m█\x1b[37m█`); - await pollFor(ctx.page, () => getCellColor(1, 1), [1, 2, 3, 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [4, 5, 6, 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [7, 8, 9, 255]); - await pollFor(ctx.page, () => getCellColor(4, 1), [10, 11, 12, 255]); - await pollFor(ctx.page, () => getCellColor(5, 1), [13, 14, 15, 255]); - await pollFor(ctx.page, () => getCellColor(6, 1), [16, 17, 18, 255]); - await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); - await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); - }); - }); - - test('foreground 0-7 drawBoldTextInBrightColors', async () => { - const theme: ITheme = { - brightBlack: '#010203', - brightRed: '#040506', - brightGreen: '#070809', - brightYellow: '#0a0b0c', - brightBlue: '#0d0e0f', - brightMagenta: '#101112', - brightCyan: '#131415', - brightWhite: '#161718' - }; - await ctx.page.evaluate(` - window.term.options.theme = ${JSON.stringify(theme)}; - window.term.options.drawBoldTextInBrightColors = true; - `); - await ctx.proxy.write(`\x1b[1;30m█\x1b[1;31m█\x1b[1;32m█\x1b[1;33m█\x1b[1;34m█\x1b[1;35m█\x1b[1;36m█\x1b[1;37m█`); - await pollFor(ctx.page, () => getCellColor(1, 1), [1, 2, 3, 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [4, 5, 6, 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [7, 8, 9, 255]); - await pollFor(ctx.page, () => getCellColor(4, 1), [10, 11, 12, 255]); - await pollFor(ctx.page, () => getCellColor(5, 1), [13, 14, 15, 255]); - await pollFor(ctx.page, () => getCellColor(6, 1), [16, 17, 18, 255]); - await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); - await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); - }); - - test('background 0-15', async () => { - const theme: ITheme = { - black: '#010203', - red: '#040506', - green: '#070809', - yellow: '#0a0b0c', - blue: '#0d0e0f', - magenta: '#101112', - cyan: '#131415', - white: '#161718' - }; - await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await ctx.proxy.write(`\x1b[40m \x1b[41m \x1b[42m \x1b[43m \x1b[44m \x1b[45m \x1b[46m \x1b[47m `); - await pollFor(ctx.page, () => getCellColor(1, 1), [1, 2, 3, 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [4, 5, 6, 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [7, 8, 9, 255]); - await pollFor(ctx.page, () => getCellColor(4, 1), [10, 11, 12, 255]); - await pollFor(ctx.page, () => getCellColor(5, 1), [13, 14, 15, 255]); - await pollFor(ctx.page, () => getCellColor(6, 1), [16, 17, 18, 255]); - await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); - await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); - }); - - test('foreground 0-15 inverse', async () => { - const theme: ITheme = { - black: '#010203', - red: '#040506', - green: '#070809', - yellow: '#0a0b0c', - blue: '#0d0e0f', - magenta: '#101112', - cyan: '#131415', - white: '#161718' - }; - await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await ctx.proxy.write(`\x1b[7;30m \x1b[7;31m \x1b[7;32m \x1b[7;33m \x1b[7;34m \x1b[7;35m \x1b[7;36m \x1b[7;37m `); - await pollFor(ctx.page, () => getCellColor(1, 1), [1, 2, 3, 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [4, 5, 6, 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [7, 8, 9, 255]); - await pollFor(ctx.page, () => getCellColor(4, 1), [10, 11, 12, 255]); - await pollFor(ctx.page, () => getCellColor(5, 1), [13, 14, 15, 255]); - await pollFor(ctx.page, () => getCellColor(6, 1), [16, 17, 18, 255]); - await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); - await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); - }); - - test('background 0-15 inverse', async () => { - const theme: ITheme = { - black: '#010203', - red: '#040506', - green: '#070809', - yellow: '#0a0b0c', - blue: '#0d0e0f', - magenta: '#101112', - cyan: '#131415', - white: '#161718' - }; - await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await ctx.proxy.write(`\x1b[7;40m█\x1b[7;41m█\x1b[7;42m█\x1b[7;43m█\x1b[7;44m█\x1b[7;45m█\x1b[7;46m█\x1b[7;47m█`); - await pollFor(ctx.page, () => getCellColor(1, 1), [1, 2, 3, 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [4, 5, 6, 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [7, 8, 9, 255]); - await pollFor(ctx.page, () => getCellColor(4, 1), [10, 11, 12, 255]); - await pollFor(ctx.page, () => getCellColor(5, 1), [13, 14, 15, 255]); - await pollFor(ctx.page, () => getCellColor(6, 1), [16, 17, 18, 255]); - await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); - await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); - }); - - test('foreground 0-15 inivisible', async () => { - const theme: ITheme = { - black: '#010203', - red: '#040506', - green: '#070809', - yellow: '#0a0b0c', - blue: '#0d0e0f', - magenta: '#101112', - cyan: '#131415', - white: '#161718' - }; - await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await ctx.proxy.write(`\x1b[8;30m \x1b[8;31m \x1b[8;32m \x1b[8;33m \x1b[8;34m \x1b[8;35m \x1b[8;36m \x1b[8;37m `); - await pollFor(ctx.page, () => getCellColor(1, 1), [0, 0, 0, 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [0, 0, 0, 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [0, 0, 0, 255]); - await pollFor(ctx.page, () => getCellColor(4, 1), [0, 0, 0, 255]); - await pollFor(ctx.page, () => getCellColor(5, 1), [0, 0, 0, 255]); - await pollFor(ctx.page, () => getCellColor(6, 1), [0, 0, 0, 255]); - await pollFor(ctx.page, () => getCellColor(7, 1), [0, 0, 0, 255]); - await pollFor(ctx.page, () => getCellColor(8, 1), [0, 0, 0, 255]); - }); - - test('background 0-15 inivisible', async () => { - const theme: ITheme = { - black: '#010203', - red: '#040506', - green: '#070809', - yellow: '#0a0b0c', - blue: '#0d0e0f', - magenta: '#101112', - cyan: '#131415', - white: '#161718' - }; - await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await ctx.proxy.write(`\x1b[8;40m█\x1b[8;41m█\x1b[8;42m█\x1b[8;43m█\x1b[8;44m█\x1b[8;45m█\x1b[8;46m█\x1b[8;47m█`); - await pollFor(ctx.page, () => getCellColor(1, 1), [1, 2, 3, 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [4, 5, 6, 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [7, 8, 9, 255]); - await pollFor(ctx.page, () => getCellColor(4, 1), [10, 11, 12, 255]); - await pollFor(ctx.page, () => getCellColor(5, 1), [13, 14, 15, 255]); - await pollFor(ctx.page, () => getCellColor(6, 1), [16, 17, 18, 255]); - await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); - await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); - }); - - test('foreground 0-15 bright', async () => { - const theme: ITheme = { - brightBlack: '#010203', - brightRed: '#040506', - brightGreen: '#070809', - brightYellow: '#0a0b0c', - brightBlue: '#0d0e0f', - brightMagenta: '#101112', - brightCyan: '#131415', - brightWhite: '#161718' - }; - await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await ctx.proxy.write(`\x1b[90m█\x1b[91m█\x1b[92m█\x1b[93m█\x1b[94m█\x1b[95m█\x1b[96m█\x1b[97m█`); - await pollFor(ctx.page, () => getCellColor(1, 1), [1, 2, 3, 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [4, 5, 6, 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [7, 8, 9, 255]); - await pollFor(ctx.page, () => getCellColor(4, 1), [10, 11, 12, 255]); - await pollFor(ctx.page, () => getCellColor(5, 1), [13, 14, 15, 255]); - await pollFor(ctx.page, () => getCellColor(6, 1), [16, 17, 18, 255]); - await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); - await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); - }); - - test('background 0-15 bright', async () => { - const theme: ITheme = { - brightBlack: '#010203', - brightRed: '#040506', - brightGreen: '#070809', - brightYellow: '#0a0b0c', - brightBlue: '#0d0e0f', - brightMagenta: '#101112', - brightCyan: '#131415', - brightWhite: '#161718' - }; - await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await ctx.proxy.write(`\x1b[100m \x1b[101m \x1b[102m \x1b[103m \x1b[104m \x1b[105m \x1b[106m \x1b[107m `); - await pollFor(ctx.page, () => getCellColor(1, 1), [1, 2, 3, 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [4, 5, 6, 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [7, 8, 9, 255]); - await pollFor(ctx.page, () => getCellColor(4, 1), [10, 11, 12, 255]); - await pollFor(ctx.page, () => getCellColor(5, 1), [13, 14, 15, 255]); - await pollFor(ctx.page, () => getCellColor(6, 1), [16, 17, 18, 255]); - await pollFor(ctx.page, () => getCellColor(7, 1), [19, 20, 21, 255]); - await pollFor(ctx.page, () => getCellColor(8, 1), [22, 23, 24, 255]); - }); - - test('foreground 16-255', async () => { - let data = ''; - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - data += `\x1b[38;5;${16 + y * 16 + x}m█\x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - const cssColor = COLORS_16_TO_255[y * 16 + x]; - const r = parseInt(cssColor.slice(1, 3), 16); - const g = parseInt(cssColor.slice(3, 5), 16); - const b = parseInt(cssColor.slice(5, 7), 16); - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); - } - } - }); - - test('background 16-255', async () => { - let data = ''; - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - data += `\x1b[48;5;${16 + y * 16 + x}m \x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - const cssColor = COLORS_16_TO_255[y * 16 + x]; - const r = parseInt(cssColor.slice(1, 3), 16); - const g = parseInt(cssColor.slice(3, 5), 16); - const b = parseInt(cssColor.slice(5, 7), 16); - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); - } - } - }); - - test('foreground 16-255 inverse', async () => { - let data = ''; - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - data += `\x1b[7;38;5;${16 + y * 16 + x}m \x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - const cssColor = COLORS_16_TO_255[y * 16 + x]; - const r = parseInt(cssColor.slice(1, 3), 16); - const g = parseInt(cssColor.slice(3, 5), 16); - const b = parseInt(cssColor.slice(5, 7), 16); - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); - } - } - }); - - test('background 16-255 inverse', async () => { - let data = ''; - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - data += `\x1b[7;48;5;${16 + y * 16 + x}m█\x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - const cssColor = COLORS_16_TO_255[y * 16 + x]; - const r = parseInt(cssColor.slice(1, 3), 16); - const g = parseInt(cssColor.slice(3, 5), 16); - const b = parseInt(cssColor.slice(5, 7), 16); - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); - } - } - }); - - test('foreground 16-255 invisible', async () => { - let data = ''; - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - data += `\x1b[8;38;5;${16 + y * 16 + x}m \x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, 0, 0, 255]); - } - } - }); - - test('background 16-255 invisible', async () => { - let data = ''; - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - data += `\x1b[8;48;5;${16 + y * 16 + x}m█\x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - const cssColor = COLORS_16_TO_255[y * 16 + x]; - const r = parseInt(cssColor.slice(1, 3), 16); - const g = parseInt(cssColor.slice(3, 5), 16); - const b = parseInt(cssColor.slice(5, 7), 16); - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); - } - } - }); - - test('foreground 16-255 dim', async () => { - let data = ''; - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - data += `\x1b[2;38;5;${16 + y * 16 + x}m█\x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - const cssColor = COLORS_16_TO_255[y * 16 + x]; - const r = parseInt(cssColor.slice(1, 3), 16); - const g = parseInt(cssColor.slice(3, 5), 16); - const b = parseInt(cssColor.slice(5, 7), 16); - // It's difficult to assert the exact color due to rounding, just ensure the color differs - // to the regular color - await pollFor(ctx.page, async () => { - const c = await getCellColor(x + 1, y + 1); - return ( - (c[0] === 0 || c[0] !== r) && - (c[1] === 0 || c[1] !== g) && - (c[2] === 0 || c[2] !== b) - ); - }, true); - } - } - }); - - test('background 16-255 dim', async () => { - let data = ''; - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - data += `\x1b[2;48;5;${16 + y * 16 + x}m \x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 240 / 16; y++) { - for (let x = 0; x < 16; x++) { - const cssColor = COLORS_16_TO_255[y * 16 + x]; - const r = parseInt(cssColor.slice(1, 3), 16); - const g = parseInt(cssColor.slice(3, 5), 16); - const b = parseInt(cssColor.slice(5, 7), 16); - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]); - } - } - }); - - test('foreground true color red', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[38;2;${i};0;0m█\x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]); - } - } - }); - - test('background true color red', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[48;2;${i};0;0m \x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]); - } - } - }); - - test('foreground true color green', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[38;2;0;${i};0m█\x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]); - } - } - }); - - test('background true color green', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[48;2;0;${i};0m \x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]); - } - } - }); - - test('foreground true color blue', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[38;2;0;0;${i}m█\x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]); - } - } - }); - - test('background true color blue', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[48;2;0;0;${i}m \x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]); - } - } - }); - - test('foreground true color grey', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[38;2;${i};${i};${i}m█\x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); - } - } - }); - - test('background true color grey', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[48;2;${i};${i};${i}m \x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); - } - } - }); - - test('foreground true color red inverse', async function(): Promise { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[7;38;2;${i};0;0m \x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]); - } - } - }); - - test('background true color red inverse', async function(): Promise { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[7;48;2;${i};0;0m█\x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]); - } - } - }); - - test('foreground true color green inverse', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[7;38;2;0;${i};0m \x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]); - } - } - }); - - test('background true color green inverse', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[7;48;2;0;${i};0m█\x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]); - } - } - }); - - test('foreground true color blue inverse', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[7;38;2;0;0;${i}m \x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]); - } - } - }); - - test('background true color blue inverse', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[7;48;2;0;0;${i}m█\x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]); - } - } - }); - - test('foreground true color grey inverse', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[7;38;2;${i};${i};${i}m \x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); - } - } - }); - - test('background true color grey inverse', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[7;48;2;${i};${i};${i}m█\x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); - } - } - }); - - test('foreground true color grey invisible', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[8;38;2;${i};${i};${i}m \x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [0, 0, 0, 255]); - } - } - }); - - test('background true color grey invisible', async () => { - let data = ''; - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - data += `\x1b[8;48;2;${i};${i};${i}m█\x1b[0m`; - } - data += '\r\n'; - } - await ctx.proxy.write(data); - for (let y = 0; y < 16; y++) { - for (let x = 0; x < 16; x++) { - const i = y * 16 + x; - await pollFor(ctx.page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]); - } - } - }); - - test.describe('minimumContrastRatio', async () => { - test('should adjust 0-15 colors on black background', async () => { - const theme: ITheme = { - black: '#2e3436', - red: '#cc0000', - green: '#4e9a06', - yellow: '#c4a000', - blue: '#3465a4', - magenta: '#75507b', - cyan: '#06989a', - white: '#d3d7cf', - brightBlack: '#555753', - brightRed: '#ef2929', - brightGreen: '#8ae234', - brightYellow: '#fce94f', - brightBlue: '#729fcf', - brightMagenta: '#ad7fa8', - brightCyan: '#34e2e2', - brightWhite: '#eeeeec' - }; - await ctx.page.evaluate(` - window.term.options.theme = ${JSON.stringify(theme)}; - window.term.options.minimumContrastRatio = 1; - `); - // Block elements ignore minimum contrast ratio so a different char is used here - await ctx.proxy.write( - `\x1b[30m■\x1b[31m■\x1b[32m■\x1b[33m■\x1b[34m■\x1b[35m■\x1b[36m■\x1b[37m■\r\n` + - `\x1b[90m■\x1b[91m■\x1b[92m■\x1b[93m■\x1b[94m■\x1b[95m■\x1b[96m■\x1b[97m■` - ); - // Validate before minimumContrastRatio is applied - await pollFor(ctx.page, () => getCellColor(1, 1), [0x2e, 0x34, 0x36, 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [0xcc, 0x00, 0x00, 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [0x4e, 0x9a, 0x06, 255]); - await pollFor(ctx.page, () => getCellColor(4, 1), [0xc4, 0xa0, 0x00, 255]); - await pollFor(ctx.page, () => getCellColor(5, 1), [0x34, 0x65, 0xa4, 255]); - await pollFor(ctx.page, () => getCellColor(6, 1), [0x75, 0x50, 0x7b, 255]); - await pollFor(ctx.page, () => getCellColor(7, 1), [0x06, 0x98, 0x9a, 255]); - await pollFor(ctx.page, () => getCellColor(8, 1), [0xd3, 0xd7, 0xcf, 255]); - await pollFor(ctx.page, () => getCellColor(1, 2), [0x55, 0x57, 0x53, 255]); - await pollFor(ctx.page, () => getCellColor(2, 2), [0xef, 0x29, 0x29, 255]); - await pollFor(ctx.page, () => getCellColor(3, 2), [0x8a, 0xe2, 0x34, 255]); - await pollFor(ctx.page, () => getCellColor(4, 2), [0xfc, 0xe9, 0x4f, 255]); - await pollFor(ctx.page, () => getCellColor(5, 2), [0x72, 0x9f, 0xcf, 255]); - await pollFor(ctx.page, () => getCellColor(6, 2), [0xad, 0x7f, 0xa8, 255]); - await pollFor(ctx.page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]); - await pollFor(ctx.page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]); - // Setting and check for minimum contrast values, note that these are not - // exact to the contrast ratio, if the increase luminance algorithm - // changes then these will probably fail - await ctx.page.evaluate(`window.term.options.minimumContrastRatio = 10;`); - frameDetails = undefined; - await pollFor(ctx.page, () => getCellColor(1, 1), [176, 180, 180, 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [238, 158, 158, 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [152, 198, 110, 255]); - await pollFor(ctx.page, () => getCellColor(4, 1), [208, 179, 49, 255]); - await pollFor(ctx.page, () => getCellColor(5, 1), [161, 183, 215, 255]); - await pollFor(ctx.page, () => getCellColor(6, 1), [191, 174, 194, 255]); - await pollFor(ctx.page, () => getCellColor(7, 1), [110, 197, 198, 255]); - await pollFor(ctx.page, () => getCellColor(8, 1), [211, 215, 207, 255]); - await pollFor(ctx.page, () => getCellColor(1, 2), [183, 185, 183, 255]); - await pollFor(ctx.page, () => getCellColor(2, 2), [249, 156, 156, 255]); - await pollFor(ctx.page, () => getCellColor(3, 2), [138, 226, 52, 255]); - await pollFor(ctx.page, () => getCellColor(4, 2), [252, 233, 79, 255]); - await pollFor(ctx.page, () => getCellColor(5, 2), [154, 186, 221, 255]); - await pollFor(ctx.page, () => getCellColor(6, 2), [203, 173, 199, 255]); - // Unchanged - await pollFor(ctx.page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]); - await pollFor(ctx.page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]); - }); - - test('should adjust 0-15 colors on white background', async () => { - const theme: ITheme = { - background: '#ffffff', - black: '#2e3436', - red: '#cc0000', - green: '#4e9a06', - yellow: '#c4a000', - blue: '#3465a4', - magenta: '#75507b', - cyan: '#06989a', - white: '#d3d7cf', - brightBlack: '#555753', - brightRed: '#ef2929', - brightGreen: '#8ae234', - brightYellow: '#fce94f', - brightBlue: '#729fcf', - brightMagenta: '#ad7fa8', - brightCyan: '#34e2e2', - brightWhite: '#eeeeec' - }; - await ctx.page.evaluate(` - window.term.options.theme = ${JSON.stringify(theme)}; - window.term.options.minimumContrastRatio = 1; - `); - // Block elements ignore minimum contrast ratio so a different char is used here - await ctx.proxy.write( - `\x1b[30m■\x1b[31m■\x1b[32m■\x1b[33m■\x1b[34m■\x1b[35m■\x1b[36m■\x1b[37m■\r\n` + - `\x1b[90m■\x1b[91m■\x1b[92m■\x1b[93m■\x1b[94m■\x1b[95m■\x1b[96m■\x1b[97m■` - ); - // Validate before minimumContrastRatio is applied - await pollFor(ctx.page, () => getCellColor(1, 1), [0x2e, 0x34, 0x36, 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [0xcc, 0x00, 0x00, 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [0x4e, 0x9a, 0x06, 255]); - await pollFor(ctx.page, () => getCellColor(4, 1), [0xc4, 0xa0, 0x00, 255]); - await pollFor(ctx.page, () => getCellColor(5, 1), [0x34, 0x65, 0xa4, 255]); - await pollFor(ctx.page, () => getCellColor(6, 1), [0x75, 0x50, 0x7b, 255]); - await pollFor(ctx.page, () => getCellColor(7, 1), [0x06, 0x98, 0x9a, 255]); - await pollFor(ctx.page, () => getCellColor(8, 1), [0xd3, 0xd7, 0xcf, 255]); - await pollFor(ctx.page, () => getCellColor(1, 2), [0x55, 0x57, 0x53, 255]); - await pollFor(ctx.page, () => getCellColor(2, 2), [0xef, 0x29, 0x29, 255]); - await pollFor(ctx.page, () => getCellColor(3, 2), [0x8a, 0xe2, 0x34, 255]); - await pollFor(ctx.page, () => getCellColor(4, 2), [0xfc, 0xe9, 0x4f, 255]); - await pollFor(ctx.page, () => getCellColor(5, 2), [0x72, 0x9f, 0xcf, 255]); - await pollFor(ctx.page, () => getCellColor(6, 2), [0xad, 0x7f, 0xa8, 255]); - await pollFor(ctx.page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]); - await pollFor(ctx.page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]); - // Setting and check for minimum contrast values, note that these are not - // exact to the contrast ratio, if the increase luminance algorithm - // changes then these will probably fail - await ctx.page.evaluate(`window.term.options.minimumContrastRatio = 10;`); - frameDetails = undefined; - await pollFor(ctx.page, () => getCellColor(1, 1), [46, 52, 54, 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [132, 0, 0, 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [36, 72, 0, 255]); - await pollFor(ctx.page, () => getCellColor(4, 1), [72, 59, 0, 255]); - await pollFor(ctx.page, () => getCellColor(5, 1), [32, 64, 106, 255]); - await pollFor(ctx.page, () => getCellColor(6, 1), [75, 51, 80, 255]); - await pollFor(ctx.page, () => getCellColor(7, 1), [0, 71, 72, 255]); - await pollFor(ctx.page, () => getCellColor(8, 1), [64, 64, 63, 255]); - await pollFor(ctx.page, () => getCellColor(1, 2), [61, 63, 59, 255]); - await pollFor(ctx.page, () => getCellColor(2, 2), [125, 19, 19, 255]); - await pollFor(ctx.page, () => getCellColor(3, 2), [40, 67, 13, 255]); - await pollFor(ctx.page, () => getCellColor(4, 2), [67, 63, 19, 255]); - await pollFor(ctx.page, () => getCellColor(5, 2), [45, 65, 87, 255]); - await pollFor(ctx.page, () => getCellColor(6, 2), [81, 57, 78, 255]); - await pollFor(ctx.page, () => getCellColor(7, 2), [13, 67, 67, 255]); - await pollFor(ctx.page, () => getCellColor(8, 2), [64, 64, 64, 255]); - }); - - test('should enforce half the contrast for dim cells', async () => { - // TODO: This test fails on webkit - if (ctx.browser.browserType().name() === 'webkit') { - test.skip(); - return; - } - const theme: ITheme = { - background: '#ffffff', - black: '#2e3436', - red: '#cc0000', - green: '#4e9a06', - yellow: '#c4a000', - blue: '#3465a4', - magenta: '#75507b', - cyan: '#06989a', - white: '#d3d7cf', - brightBlack: '#555753', - brightRed: '#ef2929', - brightGreen: '#8ae234', - brightYellow: '#fce94f', - brightBlue: '#729fcf', - brightMagenta: '#ad7fa8', - brightCyan: '#34e2e2', - brightWhite: '#eeeeec' - }; - await ctx.page.evaluate(` - window.term.options.theme = ${JSON.stringify(theme)}; - window.term.options.minimumContrastRatio = 1; - `); - // Block elements ignore minimum contrast ratio so a different char is used here - await ctx.proxy.write( - '\x1b[2m' + - `\x1b[30m■\x1b[31m■\x1b[32m■\x1b[33m■\x1b[34m■\x1b[35m■\x1b[36m■\x1b[37m■\r\n` + - `\x1b[90m■\x1b[91m■\x1b[92m■\x1b[93m■\x1b[94m■\x1b[95m■\x1b[96m■\x1b[97m■` - ); - // Validate before minimumContrastRatio is applied - await pollFor(ctx.page, () => getCellColor(1, 1), [Math.floor((255 + 0x2e) / 2), Math.floor((255 + 0x34) / 2), Math.floor((255 + 0x36) / 2), 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [Math.floor((255 + 0xcc) / 2), Math.floor((255 + 0x00) / 2), Math.floor((255 + 0x00) / 2), 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [Math.floor((255 + 0x4e) / 2), Math.floor((255 + 0x9a) / 2), Math.floor((255 + 0x06) / 2), 255]); - await pollFor(ctx.page, () => getCellColor(4, 1), [Math.floor((255 + 0xc4) / 2), Math.floor((255 + 0xa0) / 2), Math.floor((255 + 0x00) / 2), 255]); - await pollFor(ctx.page, () => getCellColor(5, 1), [Math.floor((255 + 0x34) / 2), Math.floor((255 + 0x65) / 2), Math.floor((255 + 0xa4) / 2), 255]); - await pollFor(ctx.page, () => getCellColor(6, 1), [Math.floor((255 + 0x75) / 2), Math.floor((255 + 0x50) / 2), Math.floor((255 + 0x7b) / 2), 255]); - await pollFor(ctx.page, () => getCellColor(7, 1), [Math.floor((255 + 0x06) / 2), Math.floor((255 + 0x98) / 2), Math.floor((255 + 0x9a) / 2), 255]); - await pollFor(ctx.page, () => getCellColor(8, 1), [Math.floor((255 + 0xd3) / 2), Math.floor((255 + 0xd7) / 2), Math.floor((255 + 0xcf) / 2), 255]); - await pollFor(ctx.page, () => getCellColor(1, 2), [Math.floor((255 + 0x55) / 2), Math.floor((255 + 0x57) / 2), Math.floor((255 + 0x53) / 2), 255]); - await pollFor(ctx.page, () => getCellColor(2, 2), [Math.floor((255 + 0xef) / 2), Math.floor((255 + 0x29) / 2), Math.floor((255 + 0x29) / 2), 255]); - await pollFor(ctx.page, () => getCellColor(3, 2), [Math.floor((255 + 0x8a) / 2), Math.floor((255 + 0xe2) / 2), Math.floor((255 + 0x34) / 2), 255]); - await pollFor(ctx.page, () => getCellColor(4, 2), [Math.floor((255 + 0xfc) / 2), Math.floor((255 + 0xe9) / 2), Math.floor((255 + 0x4f) / 2), 255]); - await pollFor(ctx.page, () => getCellColor(5, 2), [Math.floor((255 + 0x72) / 2), Math.floor((255 + 0x9f) / 2), Math.floor((255 + 0xcf) / 2), 255]); - await pollFor(ctx.page, () => getCellColor(6, 2), [Math.floor((255 + 0xad) / 2), Math.floor((255 + 0x7f) / 2), Math.floor((255 + 0xa8) / 2), 255]); - await pollFor(ctx.page, () => getCellColor(7, 2), [Math.floor((255 + 0x34) / 2), Math.floor((255 + 0xe2) / 2), Math.floor((255 + 0xe2) / 2), 255]); - await pollFor(ctx.page, () => getCellColor(8, 2), [Math.floor((255 + 0xee) / 2), Math.floor((255 + 0xee) / 2), Math.floor((255 + 0xec) / 2), 255]); - // Setting and check for minimum contrast values, note that these are not - // exact to the contrast ratio, if the increase luminance algorithm - // changes then these will probably fail - await ctx.page.evaluate(`window.term.options.minimumContrastRatio = 10;`); - frameDetails = undefined; - await pollFor(ctx.page, () => getCellColor(1, 1), [150, 153, 154, 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [229, 127, 127, 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [63, 124, 4, 255]); - await pollFor(ctx.page, () => getCellColor(4, 1), [127, 104, 0, 255]); - await pollFor(ctx.page, () => getCellColor(5, 1), [153, 178, 209, 255]); - await pollFor(ctx.page, () => getCellColor(6, 1), [186, 167, 189, 255]); - await pollFor(ctx.page, () => getCellColor(7, 1), [4, 122, 124, 255]); - await pollFor(ctx.page, () => getCellColor(8, 1), [110, 112, 108, 255]); - await pollFor(ctx.page, () => getCellColor(1, 2), [170, 171, 169, 255]); - await pollFor(ctx.page, () => getCellColor(2, 2), [215, 36, 36, 255]); - await pollFor(ctx.page, () => getCellColor(3, 2), [72, 117, 25, 255]); - await pollFor(ctx.page, () => getCellColor(4, 2), [117, 109, 36, 255]); - await pollFor(ctx.page, () => getCellColor(5, 2), [72, 103, 135, 255]); - await pollFor(ctx.page, () => getCellColor(6, 2), [125, 91, 121, 255]); - await pollFor(ctx.page, () => getCellColor(7, 2), [25, 117, 117, 255]); - await pollFor(ctx.page, () => getCellColor(8, 2), [111, 111, 110, 255]); - }); - }); - - test.describe('selectionBackground', async () => { - test('should resolve the inverse foreground color based on the original background color, not the selection', async () => { - const theme: ITheme = { - foreground: '#FF0000', - background: '#00FF00', - selectionBackground: '#0000FF' - }; - await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await ctx.proxy.write( ` █\x1b[7m█\x1b[0m`); - await pollFor(ctx.page, () => getCellColor(1, 1), [0, 255, 0, 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [255, 0, 0, 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [0, 255, 0, 255]); - await ctx.page.evaluate(`window.term.selectAll()`); - frameDetails = undefined; - // Selection only cell needs to be first to ensure renderer has kicked in - await pollFor(ctx.page, () => getCellColor(1, 1), [0, 0, 255, 255]); - await pollFor(ctx.page, () => getCellColor(2, 1), [255, 0, 0, 255]); - await pollFor(ctx.page, () => getCellColor(3, 1), [0, 255, 0, 255]); - }); - }); - - test.describe('allowTransparency', async () => { - test.beforeEach(() => ctx.page.evaluate(`term.options.allowTransparency = true`)); - - test('transparent background inverse', async () => { - const theme: ITheme = { - background: '#ff000080' - }; - await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - const data = `\x1b[7m█\x1b[0m`; - await ctx.proxy.write( data); - // Inverse background should be opaque - await pollFor(ctx.page, () => getCellColor(1, 1), [255, 0, 0, 255]); - }); - }); - - test.describe('selectionForeground', () => { - test('transparent background inverse', async () => { - const theme: ITheme = { - selectionForeground: '#ff0000' - }; - await ctx.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - const data = `\x1b[7m█\x1b[0m`; - await ctx.proxy.write( data); - await ctx.page.evaluate(`window.term.selectAll()`); - await pollFor(ctx.page, () => getCellColor(1, 1), [255, 0, 0, 255]); - }); - }); - - test.describe('decoration color overrides', async () => { - test('foregroundColor', async () => { - await ctx.page.evaluate(` - const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); - window.term.registerDecoration({ - marker, - foregroundColor: '#ff0000', - backgroundColor: '#0000ff' - }); - `); - const data = `█`; - await ctx.proxy.write( data); - await pollFor(ctx.page, () => getCellColor(1, 1), [255, 0, 0, 255]); - }); - test('foregroundColor should ignore inverse', async () => { - await ctx.page.evaluate(` - const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); - window.term.registerDecoration({ - marker, - foregroundColor: '#ff0000', - backgroundColor: '#0000ff' - }); - `); - const data = `\x1b[7m█\x1b[0m`; - await ctx.proxy.write( data); - await pollFor(ctx.page, () => getCellColor(1, 1), [255, 0, 0, 255]); - }); - test('foregroundColor should ignore inverse (only fg on decoration)', async () => { - await ctx.page.evaluate(` - const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); - window.term.registerDecoration({ - marker, - width: 2, - foregroundColor: '#ff0000' - }); - `); - const data = `\x1b[7m█ \x1b[0m`; - await ctx.proxy.write( data); - await pollFor(ctx.page, () => getCellColor(1, 1), [255, 0, 0, 255]); // inverse foreground of '█' should be decoration fg override - await pollFor(ctx.page, () => getCellColor(2, 1), [255, 255, 255, 255]); // inverse background of ' ' should be default foreground - }); - test('backgroundColor', async () => { - await ctx.page.evaluate(` - const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); - window.term.registerDecoration({ - marker, - foregroundColor: '#ff0000', - backgroundColor: '#0000ff' - }); - `); - const data = ` `; - await ctx.proxy.write( data); - await pollFor(ctx.page, () => getCellColor(1, 1), [0, 0, 255, 255]); - }); - test('backgroundColor should ignore inverse', async () => { - await ctx.page.evaluate(` - const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); - window.term.registerDecoration({ - marker, - foregroundColor: '#ff0000', - backgroundColor: '#0000ff' - }); - `); - const data = `\x1b[7m \x1b[0m`; - await ctx.proxy.write( data); - await pollFor(ctx.page, () => getCellColor(1, 1), [0, 0, 255, 255]); - }); - test('backgroundColor should ignore inverse (only bg on decoration)', async () => { - const data = `\x1b[7m█ \x1b[0m`; - await ctx.proxy.write( data); - await ctx.page.evaluate(` - const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); - window.term.registerDecoration({ - marker, - width: 2, - backgroundColor: '#0000ff' - }); - `); - await pollFor(ctx.page, () => getCellColor(1, 1), [0, 0, 0, 255]); // inverse foreground of '█' should be default - await pollFor(ctx.page, () => getCellColor(2, 1), [0, 0, 255, 255]); // inverse background of ' ' should be decoration bg override - }); - }); + injectSharedRendererTests(ctxWrapper); }); -/** - * Gets the color of the pixel in the center of a cell. - * @param col The 1-based column index to get the color for. - * @param row The 1-based row index to get the color for. - */ -function getCellColor(col: number, row: number): MaybeAsync<[red: number, green: number, blue: number, alpha: number]> { - if (!frameDetails) { - return getFrameDetails().then(frameDetails => getCellColorInner(frameDetails, col, row)); - } - return getCellColorInner(frameDetails, col, row); -} - -let frameDetails: { cols: number, rows: number, decoded: IImage32 } | undefined = undefined; -async function getFrameDetails(): Promise<{ cols: number, rows: number, decoded: IImage32 }> { - const screenshotOptions: LocatorScreenshotOptions | undefined = process.env.DEBUG ? { path: 'out-test/playwright/screenshot.png' } : undefined; - const buffer = await ctx.page.locator('#terminal-container .xterm-rows').screenshot(screenshotOptions); - frameDetails = { - cols: await ctx.proxy.cols, - rows: await ctx.proxy.rows, - decoded: (await decodePng(buffer, { force32: true })).image - }; - return frameDetails; -} - -function getCellColorInner(frameDetails: { cols: number, rows: number, decoded: IImage32 }, col: number, row: number): [red: number, green: number, blue: number, alpha: number] { - const cellSize = { - width: frameDetails.decoded.width / frameDetails.cols, - height: frameDetails.decoded.height / frameDetails.rows - }; - const x = Math.floor((col - 1/* 1- to 0-based index */ + 0.5/* middle of cell */) * cellSize.width); - const y = Math.floor((row - 1/* 1- to 0-based index */ + 0.5/* middle of cell */) * cellSize.height); - const i = (y * frameDetails.decoded.width + x) * 4/* 4 channels per pixel */; - return Array.from(frameDetails.decoded.data.slice(i, i + 4)) as [number, number, number, number]; -} - -const COLORS_16_TO_255 = [ - '#000000', '#00005f', '#000087', '#0000af', '#0000d7', '#0000ff', '#005f00', '#005f5f', '#005f87', '#005faf', '#005fd7', '#005fff', '#008700', '#00875f', '#008787', '#0087af', - '#0087d7', '#0087ff', '#00af00', '#00af5f', '#00af87', '#00afaf', '#00afd7', '#00afff', '#00d700', '#00d75f', '#00d787', '#00d7af', '#00d7d7', '#00d7ff', '#00ff00', '#00ff5f', - '#00ff87', '#00ffaf', '#00ffd7', '#00ffff', '#5f0000', '#5f005f', '#5f0087', '#5f00af', '#5f00d7', '#5f00ff', '#5f5f00', '#5f5f5f', '#5f5f87', '#5f5faf', '#5f5fd7', '#5f5fff', - '#5f8700', '#5f875f', '#5f8787', '#5f87af', '#5f87d7', '#5f87ff', '#5faf00', '#5faf5f', '#5faf87', '#5fafaf', '#5fafd7', '#5fafff', '#5fd700', '#5fd75f', '#5fd787', '#5fd7af', - '#5fd7d7', '#5fd7ff', '#5fff00', '#5fff5f', '#5fff87', '#5fffaf', '#5fffd7', '#5fffff', '#870000', '#87005f', '#870087', '#8700af', '#8700d7', '#8700ff', '#875f00', '#875f5f', - '#875f87', '#875faf', '#875fd7', '#875fff', '#878700', '#87875f', '#878787', '#8787af', '#8787d7', '#8787ff', '#87af00', '#87af5f', '#87af87', '#87afaf', '#87afd7', '#87afff', - '#87d700', '#87d75f', '#87d787', '#87d7af', '#87d7d7', '#87d7ff', '#87ff00', '#87ff5f', '#87ff87', '#87ffaf', '#87ffd7', '#87ffff', '#af0000', '#af005f', '#af0087', '#af00af', - '#af00d7', '#af00ff', '#af5f00', '#af5f5f', '#af5f87', '#af5faf', '#af5fd7', '#af5fff', '#af8700', '#af875f', '#af8787', '#af87af', '#af87d7', '#af87ff', '#afaf00', '#afaf5f', - '#afaf87', '#afafaf', '#afafd7', '#afafff', '#afd700', '#afd75f', '#afd787', '#afd7af', '#afd7d7', '#afd7ff', '#afff00', '#afff5f', '#afff87', '#afffaf', '#afffd7', '#afffff', - '#d70000', '#d7005f', '#d70087', '#d700af', '#d700d7', '#d700ff', '#d75f00', '#d75f5f', '#d75f87', '#d75faf', '#d75fd7', '#d75fff', '#d78700', '#d7875f', '#d78787', '#d787af', - '#d787d7', '#d787ff', '#d7af00', '#d7af5f', '#d7af87', '#d7afaf', '#d7afd7', '#d7afff', '#d7d700', '#d7d75f', '#d7d787', '#d7d7af', '#d7d7d7', '#d7d7ff', '#d7ff00', '#d7ff5f', - '#d7ff87', '#d7ffaf', '#d7ffd7', '#d7ffff', '#ff0000', '#ff005f', '#ff0087', '#ff00af', '#ff00d7', '#ff00ff', '#ff5f00', '#ff5f5f', '#ff5f87', '#ff5faf', '#ff5fd7', '#ff5fff', - '#ff8700', '#ff875f', '#ff8787', '#ff87af', '#ff87d7', '#ff87ff', '#ffaf00', '#ffaf5f', '#ffaf87', '#ffafaf', '#ffafd7', '#ffafff', '#ffd700', '#ffd75f', '#ffd787', '#ffd7af', - '#ffd7d7', '#ffd7ff', '#ffff00', '#ffff5f', '#ffff87', '#ffffaf', '#ffffd7', '#ffffff', '#080808', '#121212', '#1c1c1c', '#262626', '#303030', '#3a3a3a', '#444444', '#4e4e4e', - '#585858', '#626262', '#6c6c6c', '#767676', '#808080', '#8a8a8a', '#949494', '#9e9e9e', '#a8a8a8', '#b2b2b2', '#bcbcbc', '#c6c6c6', '#d0d0d0', '#dadada', '#e4e4e4', '#eeeeee' -]; diff --git a/test/playwright/SharedRendererTests.ts b/test/playwright/SharedRendererTests.ts new file mode 100644 index 00000000..2120925f --- /dev/null +++ b/test/playwright/SharedRendererTests.ts @@ -0,0 +1,1122 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IImage32, decodePng } from '@lunapaint/png-codec'; +import { LocatorScreenshotOptions, test } from '@playwright/test'; +import { ITheme } from 'xterm'; +import { ITestContext, MaybeAsync, pollFor } from './TestUtils'; + +export function injectSharedRendererTests(ctx: { value: ITestContext }): void { + test.beforeEach(async () => { + await ctx.value.proxy.reset(); + ctx.value.page.evaluate(` + window.term.options.minimumContrastRatio = 1; + window.term.options.allowTransparency = false; + `); + // Clear the cached screenshot before each test + frameDetails = undefined; + }); + + test.describe('colors', () => { + test('foreground 0-15', async () => { + const theme: ITheme = { + black: '#010203', + red: '#040506', + green: '#070809', + yellow: '#0a0b0c', + blue: '#0d0e0f', + magenta: '#101112', + cyan: '#131415', + white: '#161718' + }; + await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.value.proxy.write(`\x1b[30m█\x1b[31m█\x1b[32m█\x1b[33m█\x1b[34m█\x1b[35m█\x1b[36m█\x1b[37m█`); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [1, 2, 3, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [4, 5, 6, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [7, 8, 9, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 4, 1), [10, 11, 12, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 5, 1), [13, 14, 15, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 6, 1), [16, 17, 18, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 7, 1), [19, 20, 21, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 8, 1), [22, 23, 24, 255]); + }); + }); + + test('foreground 0-7 drawBoldTextInBrightColors', async () => { + const theme: ITheme = { + brightBlack: '#010203', + brightRed: '#040506', + brightGreen: '#070809', + brightYellow: '#0a0b0c', + brightBlue: '#0d0e0f', + brightMagenta: '#101112', + brightCyan: '#131415', + brightWhite: '#161718' + }; + await ctx.value.page.evaluate(` + window.term.options.theme = ${JSON.stringify(theme)}; + window.term.options.drawBoldTextInBrightColors = true; + `); + await ctx.value.proxy.write(`\x1b[1;30m█\x1b[1;31m█\x1b[1;32m█\x1b[1;33m█\x1b[1;34m█\x1b[1;35m█\x1b[1;36m█\x1b[1;37m█`); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [1, 2, 3, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [4, 5, 6, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [7, 8, 9, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 4, 1), [10, 11, 12, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 5, 1), [13, 14, 15, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 6, 1), [16, 17, 18, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 7, 1), [19, 20, 21, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 8, 1), [22, 23, 24, 255]); + }); + + test('background 0-15', async () => { + const theme: ITheme = { + black: '#010203', + red: '#040506', + green: '#070809', + yellow: '#0a0b0c', + blue: '#0d0e0f', + magenta: '#101112', + cyan: '#131415', + white: '#161718' + }; + await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.value.proxy.write(`\x1b[40m \x1b[41m \x1b[42m \x1b[43m \x1b[44m \x1b[45m \x1b[46m \x1b[47m `); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [1, 2, 3, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [4, 5, 6, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [7, 8, 9, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 4, 1), [10, 11, 12, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 5, 1), [13, 14, 15, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 6, 1), [16, 17, 18, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 7, 1), [19, 20, 21, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 8, 1), [22, 23, 24, 255]); + }); + + test('foreground 0-15 inverse', async () => { + const theme: ITheme = { + black: '#010203', + red: '#040506', + green: '#070809', + yellow: '#0a0b0c', + blue: '#0d0e0f', + magenta: '#101112', + cyan: '#131415', + white: '#161718' + }; + await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.value.proxy.write(`\x1b[7;30m \x1b[7;31m \x1b[7;32m \x1b[7;33m \x1b[7;34m \x1b[7;35m \x1b[7;36m \x1b[7;37m `); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [1, 2, 3, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [4, 5, 6, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [7, 8, 9, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 4, 1), [10, 11, 12, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 5, 1), [13, 14, 15, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 6, 1), [16, 17, 18, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 7, 1), [19, 20, 21, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 8, 1), [22, 23, 24, 255]); + }); + + test('background 0-15 inverse', async () => { + const theme: ITheme = { + black: '#010203', + red: '#040506', + green: '#070809', + yellow: '#0a0b0c', + blue: '#0d0e0f', + magenta: '#101112', + cyan: '#131415', + white: '#161718' + }; + await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.value.proxy.write(`\x1b[7;40m█\x1b[7;41m█\x1b[7;42m█\x1b[7;43m█\x1b[7;44m█\x1b[7;45m█\x1b[7;46m█\x1b[7;47m█`); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [1, 2, 3, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [4, 5, 6, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [7, 8, 9, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 4, 1), [10, 11, 12, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 5, 1), [13, 14, 15, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 6, 1), [16, 17, 18, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 7, 1), [19, 20, 21, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 8, 1), [22, 23, 24, 255]); + }); + + test('foreground 0-15 inivisible', async () => { + const theme: ITheme = { + black: '#010203', + red: '#040506', + green: '#070809', + yellow: '#0a0b0c', + blue: '#0d0e0f', + magenta: '#101112', + cyan: '#131415', + white: '#161718' + }; + await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.value.proxy.write(`\x1b[8;30m \x1b[8;31m \x1b[8;32m \x1b[8;33m \x1b[8;34m \x1b[8;35m \x1b[8;36m \x1b[8;37m `); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [0, 0, 0, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [0, 0, 0, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 4, 1), [0, 0, 0, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 5, 1), [0, 0, 0, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 6, 1), [0, 0, 0, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 7, 1), [0, 0, 0, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 8, 1), [0, 0, 0, 255]); + }); + + test('background 0-15 inivisible', async () => { + const theme: ITheme = { + black: '#010203', + red: '#040506', + green: '#070809', + yellow: '#0a0b0c', + blue: '#0d0e0f', + magenta: '#101112', + cyan: '#131415', + white: '#161718' + }; + await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.value.proxy.write(`\x1b[8;40m█\x1b[8;41m█\x1b[8;42m█\x1b[8;43m█\x1b[8;44m█\x1b[8;45m█\x1b[8;46m█\x1b[8;47m█`); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [1, 2, 3, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [4, 5, 6, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [7, 8, 9, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 4, 1), [10, 11, 12, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 5, 1), [13, 14, 15, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 6, 1), [16, 17, 18, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 7, 1), [19, 20, 21, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 8, 1), [22, 23, 24, 255]); + }); + + test('foreground 0-15 bright', async () => { + const theme: ITheme = { + brightBlack: '#010203', + brightRed: '#040506', + brightGreen: '#070809', + brightYellow: '#0a0b0c', + brightBlue: '#0d0e0f', + brightMagenta: '#101112', + brightCyan: '#131415', + brightWhite: '#161718' + }; + await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.value.proxy.write(`\x1b[90m█\x1b[91m█\x1b[92m█\x1b[93m█\x1b[94m█\x1b[95m█\x1b[96m█\x1b[97m█`); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [1, 2, 3, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [4, 5, 6, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [7, 8, 9, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 4, 1), [10, 11, 12, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 5, 1), [13, 14, 15, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 6, 1), [16, 17, 18, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 7, 1), [19, 20, 21, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 8, 1), [22, 23, 24, 255]); + }); + + test('background 0-15 bright', async () => { + const theme: ITheme = { + brightBlack: '#010203', + brightRed: '#040506', + brightGreen: '#070809', + brightYellow: '#0a0b0c', + brightBlue: '#0d0e0f', + brightMagenta: '#101112', + brightCyan: '#131415', + brightWhite: '#161718' + }; + await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.value.proxy.write(`\x1b[100m \x1b[101m \x1b[102m \x1b[103m \x1b[104m \x1b[105m \x1b[106m \x1b[107m `); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [1, 2, 3, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [4, 5, 6, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [7, 8, 9, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 4, 1), [10, 11, 12, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 5, 1), [13, 14, 15, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 6, 1), [16, 17, 18, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 7, 1), [19, 20, 21, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 8, 1), [22, 23, 24, 255]); + }); + + test('foreground 16-255', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\x1b[38;5;${16 + y * 16 + x}m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.value.proxy.write(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.slice(1, 3), 16); + const g = parseInt(cssColor.slice(3, 5), 16); + const b = parseInt(cssColor.slice(5, 7), 16); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, x + 1, y + 1), [r, g, b, 255]); + } + } + }); + + test('background 16-255', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\x1b[48;5;${16 + y * 16 + x}m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.value.proxy.write(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.slice(1, 3), 16); + const g = parseInt(cssColor.slice(3, 5), 16); + const b = parseInt(cssColor.slice(5, 7), 16); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, x + 1, y + 1), [r, g, b, 255]); + } + } + }); + + test('foreground 16-255 inverse', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\x1b[7;38;5;${16 + y * 16 + x}m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.value.proxy.write(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.slice(1, 3), 16); + const g = parseInt(cssColor.slice(3, 5), 16); + const b = parseInt(cssColor.slice(5, 7), 16); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, x + 1, y + 1), [r, g, b, 255]); + } + } + }); + + test('background 16-255 inverse', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\x1b[7;48;5;${16 + y * 16 + x}m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.value.proxy.write(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.slice(1, 3), 16); + const g = parseInt(cssColor.slice(3, 5), 16); + const b = parseInt(cssColor.slice(5, 7), 16); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, x + 1, y + 1), [r, g, b, 255]); + } + } + }); + + test('foreground 16-255 invisible', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\x1b[8;38;5;${16 + y * 16 + x}m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.value.proxy.write(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + await pollFor(ctx.value.page, () => getCellColor(ctx.value, x + 1, y + 1), [0, 0, 0, 255]); + } + } + }); + + test('background 16-255 invisible', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\x1b[8;48;5;${16 + y * 16 + x}m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.value.proxy.write(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.slice(1, 3), 16); + const g = parseInt(cssColor.slice(3, 5), 16); + const b = parseInt(cssColor.slice(5, 7), 16); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, x + 1, y + 1), [r, g, b, 255]); + } + } + }); + + test('foreground 16-255 dim', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\x1b[2;38;5;${16 + y * 16 + x}m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.value.proxy.write(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.slice(1, 3), 16); + const g = parseInt(cssColor.slice(3, 5), 16); + const b = parseInt(cssColor.slice(5, 7), 16); + // It's difficult to assert the exact color due to rounding, just ensure the color differs + // to the regular color + await pollFor(ctx.value.page, async () => { + const c = await getCellColor(ctx.value, x + 1, y + 1); + return ( + (c[0] === 0 || c[0] !== r) && + (c[1] === 0 || c[1] !== g) && + (c[2] === 0 || c[2] !== b) + ); + }, true); + } + } + }); + + test('background 16-255 dim', async () => { + let data = ''; + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + data += `\x1b[2;48;5;${16 + y * 16 + x}m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.value.proxy.write(data); + for (let y = 0; y < 240 / 16; y++) { + for (let x = 0; x < 16; x++) { + const cssColor = COLORS_16_TO_255[y * 16 + x]; + const r = parseInt(cssColor.slice(1, 3), 16); + const g = parseInt(cssColor.slice(3, 5), 16); + const b = parseInt(cssColor.slice(5, 7), 16); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, x + 1, y + 1), [r, g, b, 255]); + } + } + }); + + test('foreground true color red', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[38;2;${i};0;0m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.value.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.value.page, () => getCellColor(ctx.value, x + 1, y + 1), [i, 0, 0, 255]); + } + } + }); + + test('background true color red', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[48;2;${i};0;0m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.value.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.value.page, () => getCellColor(ctx.value, x + 1, y + 1), [i, 0, 0, 255]); + } + } + }); + + test('foreground true color green', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[38;2;0;${i};0m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.value.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.value.page, () => getCellColor(ctx.value, x + 1, y + 1), [0, i, 0, 255]); + } + } + }); + + test('background true color green', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[48;2;0;${i};0m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.value.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.value.page, () => getCellColor(ctx.value, x + 1, y + 1), [0, i, 0, 255]); + } + } + }); + + test('foreground true color blue', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[38;2;0;0;${i}m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.value.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.value.page, () => getCellColor(ctx.value, x + 1, y + 1), [0, 0, i, 255]); + } + } + }); + + test('background true color blue', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[48;2;0;0;${i}m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.value.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.value.page, () => getCellColor(ctx.value, x + 1, y + 1), [0, 0, i, 255]); + } + } + }); + + test('foreground true color grey', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[38;2;${i};${i};${i}m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.value.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.value.page, () => getCellColor(ctx.value, x + 1, y + 1), [i, i, i, 255]); + } + } + }); + + test('background true color grey', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[48;2;${i};${i};${i}m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.value.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.value.page, () => getCellColor(ctx.value, x + 1, y + 1), [i, i, i, 255]); + } + } + }); + + test('foreground true color red inverse', async function(): Promise { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[7;38;2;${i};0;0m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.value.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.value.page, () => getCellColor(ctx.value, x + 1, y + 1), [i, 0, 0, 255]); + } + } + }); + + test('background true color red inverse', async function(): Promise { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[7;48;2;${i};0;0m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.value.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.value.page, () => getCellColor(ctx.value, x + 1, y + 1), [i, 0, 0, 255]); + } + } + }); + + test('foreground true color green inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[7;38;2;0;${i};0m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.value.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.value.page, () => getCellColor(ctx.value, x + 1, y + 1), [0, i, 0, 255]); + } + } + }); + + test('background true color green inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[7;48;2;0;${i};0m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.value.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.value.page, () => getCellColor(ctx.value, x + 1, y + 1), [0, i, 0, 255]); + } + } + }); + + test('foreground true color blue inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[7;38;2;0;0;${i}m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.value.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.value.page, () => getCellColor(ctx.value, x + 1, y + 1), [0, 0, i, 255]); + } + } + }); + + test('background true color blue inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[7;48;2;0;0;${i}m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.value.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.value.page, () => getCellColor(ctx.value, x + 1, y + 1), [0, 0, i, 255]); + } + } + }); + + test('foreground true color grey inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[7;38;2;${i};${i};${i}m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.value.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.value.page, () => getCellColor(ctx.value, x + 1, y + 1), [i, i, i, 255]); + } + } + }); + + test('background true color grey inverse', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[7;48;2;${i};${i};${i}m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.value.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.value.page, () => getCellColor(ctx.value, x + 1, y + 1), [i, i, i, 255]); + } + } + }); + + test('foreground true color grey invisible', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[8;38;2;${i};${i};${i}m \x1b[0m`; + } + data += '\r\n'; + } + await ctx.value.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + await pollFor(ctx.value.page, () => getCellColor(ctx.value, x + 1, y + 1), [0, 0, 0, 255]); + } + } + }); + + test('background true color grey invisible', async () => { + let data = ''; + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + data += `\x1b[8;48;2;${i};${i};${i}m█\x1b[0m`; + } + data += '\r\n'; + } + await ctx.value.proxy.write(data); + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + const i = y * 16 + x; + await pollFor(ctx.value.page, () => getCellColor(ctx.value, x + 1, y + 1), [i, i, i, 255]); + } + } + }); + + test.describe('minimumContrastRatio', async () => { + test('should adjust 0-15 colors on black background', async () => { + const theme: ITheme = { + black: '#2e3436', + red: '#cc0000', + green: '#4e9a06', + yellow: '#c4a000', + blue: '#3465a4', + magenta: '#75507b', + cyan: '#06989a', + white: '#d3d7cf', + brightBlack: '#555753', + brightRed: '#ef2929', + brightGreen: '#8ae234', + brightYellow: '#fce94f', + brightBlue: '#729fcf', + brightMagenta: '#ad7fa8', + brightCyan: '#34e2e2', + brightWhite: '#eeeeec' + }; + await ctx.value.page.evaluate(` + window.term.options.theme = ${JSON.stringify(theme)}; + window.term.options.minimumContrastRatio = 1; + `); + // Block elements ignore minimum contrast ratio so a different char is used here + await ctx.value.proxy.write( + `\x1b[30m■\x1b[31m■\x1b[32m■\x1b[33m■\x1b[34m■\x1b[35m■\x1b[36m■\x1b[37m■\r\n` + + `\x1b[90m■\x1b[91m■\x1b[92m■\x1b[93m■\x1b[94m■\x1b[95m■\x1b[96m■\x1b[97m■` + ); + // Validate before minimumContrastRatio is applied + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0x2e, 0x34, 0x36, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [0xcc, 0x00, 0x00, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [0x4e, 0x9a, 0x06, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 4, 1), [0xc4, 0xa0, 0x00, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 5, 1), [0x34, 0x65, 0xa4, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 6, 1), [0x75, 0x50, 0x7b, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 7, 1), [0x06, 0x98, 0x9a, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 8, 1), [0xd3, 0xd7, 0xcf, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 2), [0x55, 0x57, 0x53, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 2), [0xef, 0x29, 0x29, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 2), [0x8a, 0xe2, 0x34, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 4, 2), [0xfc, 0xe9, 0x4f, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 5, 2), [0x72, 0x9f, 0xcf, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 6, 2), [0xad, 0x7f, 0xa8, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 7, 2), [0x34, 0xe2, 0xe2, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 8, 2), [0xee, 0xee, 0xec, 255]); + // Setting and check for minimum contrast values, note that these are not + // exact to the contrast ratio, if the increase luminance algorithm + // changes then these will probably fail + await ctx.value.page.evaluate(`window.term.options.minimumContrastRatio = 10;`); + frameDetails = undefined; + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [176, 180, 180, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [238, 158, 158, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [152, 198, 110, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 4, 1), [208, 179, 49, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 5, 1), [161, 183, 215, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 6, 1), [191, 174, 194, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 7, 1), [110, 197, 198, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 8, 1), [211, 215, 207, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 2), [183, 185, 183, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 2), [249, 156, 156, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 2), [138, 226, 52, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 4, 2), [252, 233, 79, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 5, 2), [154, 186, 221, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 6, 2), [203, 173, 199, 255]); + // Unchanged + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 7, 2), [0x34, 0xe2, 0xe2, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 8, 2), [0xee, 0xee, 0xec, 255]); + }); + + test('should adjust 0-15 colors on white background', async () => { + const theme: ITheme = { + background: '#ffffff', + black: '#2e3436', + red: '#cc0000', + green: '#4e9a06', + yellow: '#c4a000', + blue: '#3465a4', + magenta: '#75507b', + cyan: '#06989a', + white: '#d3d7cf', + brightBlack: '#555753', + brightRed: '#ef2929', + brightGreen: '#8ae234', + brightYellow: '#fce94f', + brightBlue: '#729fcf', + brightMagenta: '#ad7fa8', + brightCyan: '#34e2e2', + brightWhite: '#eeeeec' + }; + await ctx.value.page.evaluate(` + window.term.options.theme = ${JSON.stringify(theme)}; + window.term.options.minimumContrastRatio = 1; + `); + // Block elements ignore minimum contrast ratio so a different char is used here + await ctx.value.proxy.write( + `\x1b[30m■\x1b[31m■\x1b[32m■\x1b[33m■\x1b[34m■\x1b[35m■\x1b[36m■\x1b[37m■\r\n` + + `\x1b[90m■\x1b[91m■\x1b[92m■\x1b[93m■\x1b[94m■\x1b[95m■\x1b[96m■\x1b[97m■` + ); + // Validate before minimumContrastRatio is applied + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0x2e, 0x34, 0x36, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [0xcc, 0x00, 0x00, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [0x4e, 0x9a, 0x06, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 4, 1), [0xc4, 0xa0, 0x00, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 5, 1), [0x34, 0x65, 0xa4, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 6, 1), [0x75, 0x50, 0x7b, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 7, 1), [0x06, 0x98, 0x9a, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 8, 1), [0xd3, 0xd7, 0xcf, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 2), [0x55, 0x57, 0x53, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 2), [0xef, 0x29, 0x29, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 2), [0x8a, 0xe2, 0x34, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 4, 2), [0xfc, 0xe9, 0x4f, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 5, 2), [0x72, 0x9f, 0xcf, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 6, 2), [0xad, 0x7f, 0xa8, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 7, 2), [0x34, 0xe2, 0xe2, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 8, 2), [0xee, 0xee, 0xec, 255]); + // Setting and check for minimum contrast values, note that these are not + // exact to the contrast ratio, if the increase luminance algorithm + // changes then these will probably fail + await ctx.value.page.evaluate(`window.term.options.minimumContrastRatio = 10;`); + frameDetails = undefined; + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [46, 52, 54, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [132, 0, 0, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [36, 72, 0, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 4, 1), [72, 59, 0, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 5, 1), [32, 64, 106, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 6, 1), [75, 51, 80, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 7, 1), [0, 71, 72, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 8, 1), [64, 64, 63, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 2), [61, 63, 59, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 2), [125, 19, 19, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 2), [40, 67, 13, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 4, 2), [67, 63, 19, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 5, 2), [45, 65, 87, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 6, 2), [81, 57, 78, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 7, 2), [13, 67, 67, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 8, 2), [64, 64, 64, 255]); + }); + + test('should enforce half the contrast for dim cells', async () => { + // TODO: This test fails on webkit + if (ctx.value.browser.browserType().name() === 'webkit') { + test.skip(); + return; + } + const theme: ITheme = { + background: '#ffffff', + black: '#2e3436', + red: '#cc0000', + green: '#4e9a06', + yellow: '#c4a000', + blue: '#3465a4', + magenta: '#75507b', + cyan: '#06989a', + white: '#d3d7cf', + brightBlack: '#555753', + brightRed: '#ef2929', + brightGreen: '#8ae234', + brightYellow: '#fce94f', + brightBlue: '#729fcf', + brightMagenta: '#ad7fa8', + brightCyan: '#34e2e2', + brightWhite: '#eeeeec' + }; + await ctx.value.page.evaluate(` + window.term.options.theme = ${JSON.stringify(theme)}; + window.term.options.minimumContrastRatio = 1; + `); + // Block elements ignore minimum contrast ratio so a different char is used here + await ctx.value.proxy.write( + '\x1b[2m' + + `\x1b[30m■\x1b[31m■\x1b[32m■\x1b[33m■\x1b[34m■\x1b[35m■\x1b[36m■\x1b[37m■\r\n` + + `\x1b[90m■\x1b[91m■\x1b[92m■\x1b[93m■\x1b[94m■\x1b[95m■\x1b[96m■\x1b[97m■` + ); + // Validate before minimumContrastRatio is applied + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [Math.floor((255 + 0x2e) / 2), Math.floor((255 + 0x34) / 2), Math.floor((255 + 0x36) / 2), 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [Math.floor((255 + 0xcc) / 2), Math.floor((255 + 0x00) / 2), Math.floor((255 + 0x00) / 2), 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [Math.floor((255 + 0x4e) / 2), Math.floor((255 + 0x9a) / 2), Math.floor((255 + 0x06) / 2), 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 4, 1), [Math.floor((255 + 0xc4) / 2), Math.floor((255 + 0xa0) / 2), Math.floor((255 + 0x00) / 2), 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 5, 1), [Math.floor((255 + 0x34) / 2), Math.floor((255 + 0x65) / 2), Math.floor((255 + 0xa4) / 2), 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 6, 1), [Math.floor((255 + 0x75) / 2), Math.floor((255 + 0x50) / 2), Math.floor((255 + 0x7b) / 2), 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 7, 1), [Math.floor((255 + 0x06) / 2), Math.floor((255 + 0x98) / 2), Math.floor((255 + 0x9a) / 2), 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 8, 1), [Math.floor((255 + 0xd3) / 2), Math.floor((255 + 0xd7) / 2), Math.floor((255 + 0xcf) / 2), 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 2), [Math.floor((255 + 0x55) / 2), Math.floor((255 + 0x57) / 2), Math.floor((255 + 0x53) / 2), 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 2), [Math.floor((255 + 0xef) / 2), Math.floor((255 + 0x29) / 2), Math.floor((255 + 0x29) / 2), 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 2), [Math.floor((255 + 0x8a) / 2), Math.floor((255 + 0xe2) / 2), Math.floor((255 + 0x34) / 2), 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 4, 2), [Math.floor((255 + 0xfc) / 2), Math.floor((255 + 0xe9) / 2), Math.floor((255 + 0x4f) / 2), 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 5, 2), [Math.floor((255 + 0x72) / 2), Math.floor((255 + 0x9f) / 2), Math.floor((255 + 0xcf) / 2), 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 6, 2), [Math.floor((255 + 0xad) / 2), Math.floor((255 + 0x7f) / 2), Math.floor((255 + 0xa8) / 2), 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 7, 2), [Math.floor((255 + 0x34) / 2), Math.floor((255 + 0xe2) / 2), Math.floor((255 + 0xe2) / 2), 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 8, 2), [Math.floor((255 + 0xee) / 2), Math.floor((255 + 0xee) / 2), Math.floor((255 + 0xec) / 2), 255]); + // Setting and check for minimum contrast values, note that these are not + // exact to the contrast ratio, if the increase luminance algorithm + // changes then these will probably fail + await ctx.value.page.evaluate(`window.term.options.minimumContrastRatio = 10;`); + frameDetails = undefined; + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [150, 153, 154, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [229, 127, 127, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [63, 124, 4, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 4, 1), [127, 104, 0, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 5, 1), [153, 178, 209, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 6, 1), [186, 167, 189, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 7, 1), [4, 122, 124, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 8, 1), [110, 112, 108, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 2), [170, 171, 169, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 2), [215, 36, 36, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 2), [72, 117, 25, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 4, 2), [117, 109, 36, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 5, 2), [72, 103, 135, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 6, 2), [125, 91, 121, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 7, 2), [25, 117, 117, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 8, 2), [111, 111, 110, 255]); + }); + }); + + test.describe('selectionBackground', async () => { + test('should resolve the inverse foreground color based on the original background color, not the selection', async () => { + const theme: ITheme = { + foreground: '#FF0000', + background: '#00FF00', + selectionBackground: '#0000FF' + }; + await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.value.proxy.write( ` █\x1b[7m█\x1b[0m`); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 255, 0, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [255, 0, 0, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [0, 255, 0, 255]); + await ctx.value.page.evaluate(`window.term.selectAll()`); + frameDetails = undefined; + // Selection only cell needs to be first to ensure renderer has kicked in + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 255, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [255, 0, 0, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [0, 255, 0, 255]); + }); + }); + + test.describe('allowTransparency', async () => { + test.beforeEach(() => ctx.value.page.evaluate(`term.options.allowTransparency = true`)); + + test('transparent background inverse', async () => { + const theme: ITheme = { + background: '#ff000080' + }; + await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + const data = `\x1b[7m█\x1b[0m`; + await ctx.value.proxy.write( data); + // Inverse background should be opaque + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 0, 0, 255]); + }); + }); + + test.describe('selectionForeground', () => { + test('transparent background inverse', async () => { + const theme: ITheme = { + selectionForeground: '#ff0000' + }; + await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + const data = `\x1b[7m█\x1b[0m`; + await ctx.value.proxy.write( data); + await ctx.value.page.evaluate(`window.term.selectAll()`); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 0, 0, 255]); + }); + }); + + test.describe('decoration color overrides', async () => { + test('foregroundColor', async () => { + await ctx.value.page.evaluate(` + const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); + window.term.registerDecoration({ + marker, + foregroundColor: '#ff0000', + backgroundColor: '#0000ff' + }); + `); + const data = `█`; + await ctx.value.proxy.write( data); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 0, 0, 255]); + }); + test('foregroundColor should ignore inverse', async () => { + await ctx.value.page.evaluate(` + const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); + window.term.registerDecoration({ + marker, + foregroundColor: '#ff0000', + backgroundColor: '#0000ff' + }); + `); + const data = `\x1b[7m█\x1b[0m`; + await ctx.value.proxy.write( data); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 0, 0, 255]); + }); + test('foregroundColor should ignore inverse (only fg on decoration)', async () => { + await ctx.value.page.evaluate(` + const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); + window.term.registerDecoration({ + marker, + width: 2, + foregroundColor: '#ff0000' + }); + `); + const data = `\x1b[7m█ \x1b[0m`; + await ctx.value.proxy.write( data); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 0, 0, 255]); // inverse foreground of '█' should be decoration fg override + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [255, 255, 255, 255]); // inverse background of ' ' should be default foreground + }); + test('backgroundColor', async () => { + await ctx.value.page.evaluate(` + const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); + window.term.registerDecoration({ + marker, + foregroundColor: '#ff0000', + backgroundColor: '#0000ff' + }); + `); + const data = ` `; + await ctx.value.proxy.write( data); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 255, 255]); + }); + test('backgroundColor should ignore inverse', async () => { + await ctx.value.page.evaluate(` + const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); + window.term.registerDecoration({ + marker, + foregroundColor: '#ff0000', + backgroundColor: '#0000ff' + }); + `); + const data = `\x1b[7m \x1b[0m`; + await ctx.value.proxy.write( data); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 255, 255]); + }); + test('backgroundColor should ignore inverse (only bg on decoration)', async () => { + const data = `\x1b[7m█ \x1b[0m`; + await ctx.value.proxy.write( data); + await ctx.value.page.evaluate(` + const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); + window.term.registerDecoration({ + marker, + width: 2, + backgroundColor: '#0000ff' + }); + `); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]); // inverse foreground of '█' should be default + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [0, 0, 255, 255]); // inverse background of ' ' should be decoration bg override + }); + }); +} + +/** + * Gets the color of the pixel in the center of a cell. + * @param ctx The test context. + * @param col The 1-based column index to get the color for. + * @param row The 1-based row index to get the color for. + */ +function getCellColor(ctx: ITestContext, col: number, row: number): MaybeAsync<[red: number, green: number, blue: number, alpha: number]> { + if (!frameDetails) { + return getFrameDetails(ctx).then(frameDetails => getCellColorInner(frameDetails, col, row)); + } + return getCellColorInner(frameDetails, col, row); +} + +let frameDetails: { cols: number, rows: number, decoded: IImage32 } | undefined = undefined; +async function getFrameDetails(ctx: ITestContext): Promise<{ cols: number, rows: number, decoded: IImage32 }> { + const screenshotOptions: LocatorScreenshotOptions | undefined = process.env.DEBUG ? { path: 'out-test/playwright/screenshot.png' } : undefined; + const buffer = await ctx.page.locator('#terminal-container .xterm-screen').screenshot(screenshotOptions); + frameDetails = { + cols: await ctx.proxy.cols, + rows: await ctx.proxy.rows, + decoded: (await decodePng(buffer, { force32: true })).image + }; + return frameDetails; +} + +function getCellColorInner(frameDetails: { cols: number, rows: number, decoded: IImage32 }, col: number, row: number): [red: number, green: number, blue: number, alpha: number] { + const cellSize = { + width: frameDetails.decoded.width / frameDetails.cols, + height: frameDetails.decoded.height / frameDetails.rows + }; + const x = Math.floor((col - 1/* 1- to 0-based index */ + 0.5/* middle of cell */) * cellSize.width); + const y = Math.floor((row - 1/* 1- to 0-based index */ + 0.5/* middle of cell */) * cellSize.height); + const i = (y * frameDetails.decoded.width + x) * 4/* 4 channels per pixel */; + return Array.from(frameDetails.decoded.data.slice(i, i + 4)) as [number, number, number, number]; +} + +const COLORS_16_TO_255 = [ + '#000000', '#00005f', '#000087', '#0000af', '#0000d7', '#0000ff', '#005f00', '#005f5f', '#005f87', '#005faf', '#005fd7', '#005fff', '#008700', '#00875f', '#008787', '#0087af', + '#0087d7', '#0087ff', '#00af00', '#00af5f', '#00af87', '#00afaf', '#00afd7', '#00afff', '#00d700', '#00d75f', '#00d787', '#00d7af', '#00d7d7', '#00d7ff', '#00ff00', '#00ff5f', + '#00ff87', '#00ffaf', '#00ffd7', '#00ffff', '#5f0000', '#5f005f', '#5f0087', '#5f00af', '#5f00d7', '#5f00ff', '#5f5f00', '#5f5f5f', '#5f5f87', '#5f5faf', '#5f5fd7', '#5f5fff', + '#5f8700', '#5f875f', '#5f8787', '#5f87af', '#5f87d7', '#5f87ff', '#5faf00', '#5faf5f', '#5faf87', '#5fafaf', '#5fafd7', '#5fafff', '#5fd700', '#5fd75f', '#5fd787', '#5fd7af', + '#5fd7d7', '#5fd7ff', '#5fff00', '#5fff5f', '#5fff87', '#5fffaf', '#5fffd7', '#5fffff', '#870000', '#87005f', '#870087', '#8700af', '#8700d7', '#8700ff', '#875f00', '#875f5f', + '#875f87', '#875faf', '#875fd7', '#875fff', '#878700', '#87875f', '#878787', '#8787af', '#8787d7', '#8787ff', '#87af00', '#87af5f', '#87af87', '#87afaf', '#87afd7', '#87afff', + '#87d700', '#87d75f', '#87d787', '#87d7af', '#87d7d7', '#87d7ff', '#87ff00', '#87ff5f', '#87ff87', '#87ffaf', '#87ffd7', '#87ffff', '#af0000', '#af005f', '#af0087', '#af00af', + '#af00d7', '#af00ff', '#af5f00', '#af5f5f', '#af5f87', '#af5faf', '#af5fd7', '#af5fff', '#af8700', '#af875f', '#af8787', '#af87af', '#af87d7', '#af87ff', '#afaf00', '#afaf5f', + '#afaf87', '#afafaf', '#afafd7', '#afafff', '#afd700', '#afd75f', '#afd787', '#afd7af', '#afd7d7', '#afd7ff', '#afff00', '#afff5f', '#afff87', '#afffaf', '#afffd7', '#afffff', + '#d70000', '#d7005f', '#d70087', '#d700af', '#d700d7', '#d700ff', '#d75f00', '#d75f5f', '#d75f87', '#d75faf', '#d75fd7', '#d75fff', '#d78700', '#d7875f', '#d78787', '#d787af', + '#d787d7', '#d787ff', '#d7af00', '#d7af5f', '#d7af87', '#d7afaf', '#d7afd7', '#d7afff', '#d7d700', '#d7d75f', '#d7d787', '#d7d7af', '#d7d7d7', '#d7d7ff', '#d7ff00', '#d7ff5f', + '#d7ff87', '#d7ffaf', '#d7ffd7', '#d7ffff', '#ff0000', '#ff005f', '#ff0087', '#ff00af', '#ff00d7', '#ff00ff', '#ff5f00', '#ff5f5f', '#ff5f87', '#ff5faf', '#ff5fd7', '#ff5fff', + '#ff8700', '#ff875f', '#ff8787', '#ff87af', '#ff87d7', '#ff87ff', '#ffaf00', '#ffaf5f', '#ffaf87', '#ffafaf', '#ffafd7', '#ffafff', '#ffd700', '#ffd75f', '#ffd787', '#ffd7af', + '#ffd7d7', '#ffd7ff', '#ffff00', '#ffff5f', '#ffff87', '#ffffaf', '#ffffd7', '#ffffff', '#080808', '#121212', '#1c1c1c', '#262626', '#303030', '#3a3a3a', '#444444', '#4e4e4e', + '#585858', '#626262', '#6c6c6c', '#767676', '#808080', '#8a8a8a', '#949494', '#9e9e9e', '#a8a8a8', '#b2b2b2', '#bcbcbc', '#c6c6c6', '#d0d0d0', '#dadada', '#e4e4e4', '#eeeeee' +]; From 9b9062aea46f9c4195662dfeb8222b79b654e308 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 10:27:14 -0700 Subject: [PATCH 56/99] Run npm start using relative cwd --- addons/xterm-addon-webgl/test/playwright.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/xterm-addon-webgl/test/playwright.config.ts b/addons/xterm-addon-webgl/test/playwright.config.ts index 3d1fe4fb..a9446a22 100644 --- a/addons/xterm-addon-webgl/test/playwright.config.ts +++ b/addons/xterm-addon-webgl/test/playwright.config.ts @@ -26,7 +26,7 @@ const config: PlaywrightTestConfig = { ], reporter: 'list', webServer: { - command: 'npm start', + command: 'npm --cwd ../.. start', port: 3000, timeout: 120000, reuseExistingServer: !process.env.CI From 47183b29390cc80e179c4c97e9aba516cdc6e1ea Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 10:31:30 -0700 Subject: [PATCH 57/99] Fix relative path --- addons/xterm-addon-webgl/test/playwright.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/xterm-addon-webgl/test/playwright.config.ts b/addons/xterm-addon-webgl/test/playwright.config.ts index a9446a22..0cc9bb3a 100644 --- a/addons/xterm-addon-webgl/test/playwright.config.ts +++ b/addons/xterm-addon-webgl/test/playwright.config.ts @@ -26,7 +26,7 @@ const config: PlaywrightTestConfig = { ], reporter: 'list', webServer: { - command: 'npm --cwd ../.. start', + command: 'npm --cwd ../../.. start', port: 3000, timeout: 120000, reuseExistingServer: !process.env.CI From 48ba5e58560718fdcac59365572b945f2ee68112 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 10:35:38 -0700 Subject: [PATCH 58/99] Add npm start to webgl package.json --- addons/xterm-addon-webgl/package.json | 3 ++- addons/xterm-addon-webgl/test/playwright.config.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/addons/xterm-addon-webgl/package.json b/addons/xterm-addon-webgl/package.json index e8398c92..7e9765fc 100644 --- a/addons/xterm-addon-webgl/package.json +++ b/addons/xterm-addon-webgl/package.json @@ -19,7 +19,8 @@ "build": "../../node_modules/.bin/tsc -p .", "prepackage": "npm run build", "package": "../../node_modules/.bin/webpack", - "prepublishOnly": "npm run package" + "prepublishOnly": "npm run package", + "start": "node ../../demo/start" }, "peerDependencies": { "xterm": "^5.0.0" diff --git a/addons/xterm-addon-webgl/test/playwright.config.ts b/addons/xterm-addon-webgl/test/playwright.config.ts index 0cc9bb3a..3d1fe4fb 100644 --- a/addons/xterm-addon-webgl/test/playwright.config.ts +++ b/addons/xterm-addon-webgl/test/playwright.config.ts @@ -26,7 +26,7 @@ const config: PlaywrightTestConfig = { ], reporter: 'list', webServer: { - command: 'npm --cwd ../../.. start', + command: 'npm start', port: 3000, timeout: 120000, reuseExistingServer: !process.env.CI From a0c34b19df144d32a863396db02fd73172655bfb Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 10:52:21 -0700 Subject: [PATCH 59/99] Skip webgl tests on linux/firefox --- addons/xterm-addon-webgl/test/WebglRenderer.test.ts | 7 +++++++ test/playwright/Renderer.test.ts | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/addons/xterm-addon-webgl/test/WebglRenderer.test.ts b/addons/xterm-addon-webgl/test/WebglRenderer.test.ts index 5fc5d300..4da9d346 100644 --- a/addons/xterm-addon-webgl/test/WebglRenderer.test.ts +++ b/addons/xterm-addon-webgl/test/WebglRenderer.test.ts @@ -7,6 +7,7 @@ import test from '@playwright/test'; import { strictEqual } from 'assert'; import { injectSharedRendererTests } from '../../../out-test/playwright/SharedRendererTests'; import { ITestContext, createTestContext, openTerminal } from '../../../out-test/playwright/TestUtils'; +import { platform } from 'os'; let ctx: ITestContext; const ctxWrapper: { value: ITestContext } = { value: undefined } as any; @@ -22,6 +23,12 @@ test.beforeAll(async ({ browser }) => { test.afterAll(async () => await ctx.page.close()); test.describe('WebGL Renderer Integration Tests', async () => { + // HACK: webgl2 is often not supported in headless firefox on Linux + // https://github.com/microsoft/playwright/issues/11566 + if (platform() === 'linux') { + test.skip(({ browserName }) => browserName === 'firefox'); + } + test('dispose removes renderer canvases', async function(): Promise { strictEqual(await ctx.page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 2); await ctx.page.evaluate(`addon.dispose()`); diff --git a/test/playwright/Renderer.test.ts b/test/playwright/Renderer.test.ts index 8050001f..17c8b6e1 100644 --- a/test/playwright/Renderer.test.ts +++ b/test/playwright/Renderer.test.ts @@ -16,7 +16,7 @@ test.beforeAll(async ({ browser }) => { }); test.afterAll(async () => await ctx.page.close()); -test.describe('DOM Renderer Integration Tests', async () => { +test.describe('DOM Renderer Integration Tests', () => { injectSharedRendererTests(ctxWrapper); }); From 98e8c6ac73c4fe605639fa42b6ff85b671988d7a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 12:16:16 -0700 Subject: [PATCH 60/99] Add canvas renderer tests --- .eslintrc.json | 1 + .../test/CanvasRenderer.test.ts | 35 ++++++++++++++++ .../test/playwright.config.ts | 35 ++++++++++++++++ addons/xterm-addon-canvas/test/tsconfig.json | 41 +++++++++++++++++++ addons/xterm-addon-canvas/tsconfig.json | 3 +- .../test/WebglRenderer.test.ts | 4 +- bin/test_playwright.js | 3 +- demo/client.ts | 2 + test/playwright/Renderer.test.ts | 4 +- test/playwright/SharedRendererTests.ts | 13 ++++-- 10 files changed, 131 insertions(+), 10 deletions(-) create mode 100644 addons/xterm-addon-canvas/test/CanvasRenderer.test.ts create mode 100644 addons/xterm-addon-canvas/test/playwright.config.ts create mode 100644 addons/xterm-addon-canvas/test/tsconfig.json diff --git a/.eslintrc.json b/.eslintrc.json index 0626a446..892c297d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -17,6 +17,7 @@ "addons/xterm-addon-attach/src/tsconfig.json", "addons/xterm-addon-attach/test/tsconfig.json", "addons/xterm-addon-canvas/src/tsconfig.json", + "addons/xterm-addon-canvas/test/tsconfig.json", "addons/xterm-addon-fit/src/tsconfig.json", "addons/xterm-addon-fit/test/tsconfig.json", "addons/xterm-addon-image/src/tsconfig.json", diff --git a/addons/xterm-addon-canvas/test/CanvasRenderer.test.ts b/addons/xterm-addon-canvas/test/CanvasRenderer.test.ts new file mode 100644 index 00000000..c87c376f --- /dev/null +++ b/addons/xterm-addon-canvas/test/CanvasRenderer.test.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import test from '@playwright/test'; +import { ISharedRendererTestContext, injectSharedRendererTests } from '../../../out-test/playwright/SharedRendererTests'; +import { ITestContext, createTestContext, openTerminal } from '../../../out-test/playwright/TestUtils'; +import { platform } from 'os'; + +let ctx: ITestContext; +const ctxWrapper: ISharedRendererTestContext = { + value: undefined, + skipSelectionTests: true +} as any; +test.beforeAll(async ({ browser }) => { + ctx = await createTestContext(browser); + await openTerminal(ctx); + ctxWrapper.value = ctx; + await ctx.page.evaluate(` + window.addon = new CanvasAddon(true); + window.term.loadAddon(window.addon); + `); +}); +test.afterAll(async () => await ctx.page.close()); + +test.describe('Canvas Renderer Integration Tests', async () => { + // HACK: webgl2 is often not supported in headless firefox on Linux + // https://github.com/microsoft/playwright/issues/11566 + if (platform() === 'linux') { + test.skip(({ browserName }) => browserName === 'firefox'); + } + + injectSharedRendererTests(ctxWrapper); +}); diff --git a/addons/xterm-addon-canvas/test/playwright.config.ts b/addons/xterm-addon-canvas/test/playwright.config.ts new file mode 100644 index 00000000..3d1fe4fb --- /dev/null +++ b/addons/xterm-addon-canvas/test/playwright.config.ts @@ -0,0 +1,35 @@ +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' + } + } + ], + reporter: 'list', + webServer: { + command: 'npm start', + port: 3000, + timeout: 120000, + reuseExistingServer: !process.env.CI + } +}; +export default config; diff --git a/addons/xterm-addon-canvas/test/tsconfig.json b/addons/xterm-addon-canvas/test/tsconfig.json new file mode 100644 index 00000000..9523e64b --- /dev/null +++ b/addons/xterm-addon-canvas/test/tsconfig.json @@ -0,0 +1,41 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es2015", + "lib": [ + "es6", + ], + "rootDir": ".", + "outDir": "../out-test", + "sourceMap": true, + "removeComments": true, + "baseUrl": ".", + "paths": { + "common/*": [ + "../../../src/common/*" + ], + "browser/*": [ + "../../../src/browser/*" + ] + }, + "strict": true, + "types": [ + "../../../node_modules/@types/node", + "../../../node_modules/@lunapaint/png-codec", + "../../../out-test/playwright/TestUtils", + "../../../out-test/playwright/SharedRendererTests" + ] + }, + "include": [ + "./**/*", + "../../../typings/xterm.d.ts" + ], + "references": [ + { + "path": "../../../src/common" + }, + { + "path": "../../../src/browser" + } + ] +} diff --git a/addons/xterm-addon-canvas/tsconfig.json b/addons/xterm-addon-canvas/tsconfig.json index b711f30a..2d820dd1 100644 --- a/addons/xterm-addon-canvas/tsconfig.json +++ b/addons/xterm-addon-canvas/tsconfig.json @@ -2,6 +2,7 @@ "files": [], "include": [], "references": [ - { "path": "./src" } + { "path": "./src" }, + { "path": "./test" } ] } diff --git a/addons/xterm-addon-webgl/test/WebglRenderer.test.ts b/addons/xterm-addon-webgl/test/WebglRenderer.test.ts index 4da9d346..c6789613 100644 --- a/addons/xterm-addon-webgl/test/WebglRenderer.test.ts +++ b/addons/xterm-addon-webgl/test/WebglRenderer.test.ts @@ -5,12 +5,12 @@ import test from '@playwright/test'; import { strictEqual } from 'assert'; -import { injectSharedRendererTests } from '../../../out-test/playwright/SharedRendererTests'; +import { ISharedRendererTestContext, injectSharedRendererTests } from '../../../out-test/playwright/SharedRendererTests'; import { ITestContext, createTestContext, openTerminal } from '../../../out-test/playwright/TestUtils'; import { platform } from 'os'; let ctx: ITestContext; -const ctxWrapper: { value: ITestContext } = { value: undefined } as any; +const ctxWrapper: ISharedRendererTestContext = { value: undefined } as any; test.beforeAll(async ({ browser }) => { ctx = await createTestContext(browser); await openTerminal(ctx); diff --git a/bin/test_playwright.js b/bin/test_playwright.js index bc4e7b02..379c0e26 100644 --- a/bin/test_playwright.js +++ b/bin/test_playwright.js @@ -9,7 +9,8 @@ const cp = require('child_process'); const path = require('path'); const configs = [ - { name: 'core', path: 'out-test/playwright/playwright.config.js' }, + // { name: 'core', path: 'out-test/playwright/playwright.config.js' }, + { name: 'xterm-addon-canvas', path: 'addons/xterm-addon-canvas/out-test/playwright.config.js' }, { name: 'xterm-addon-webgl', path: 'addons/xterm-addon-webgl/out-test/playwright.config.js' } ]; diff --git a/demo/client.ts b/demo/client.ts index bde5ca72..7ade385e 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -48,6 +48,7 @@ export interface IWindowWithTerminal extends Window { term: TerminalType; Terminal?: typeof TerminalType; // eslint-disable-line @typescript-eslint/naming-convention AttachAddon?: typeof AttachAddon; // eslint-disable-line @typescript-eslint/naming-convention + CanvasAddon?: typeof CanvasAddon; // eslint-disable-line @typescript-eslint/naming-convention FitAddon?: typeof FitAddon; // 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 @@ -218,6 +219,7 @@ const createNewWindowButtonHandler: () => void = () => { if (document.location.pathname === '/test') { window.Terminal = Terminal; window.AttachAddon = AttachAddon; + window.CanvasAddon = CanvasAddon; window.FitAddon = FitAddon; window.ImageAddon = ImageAddon; window.SearchAddon = SearchAddon; diff --git a/test/playwright/Renderer.test.ts b/test/playwright/Renderer.test.ts index 17c8b6e1..c5c1e94d 100644 --- a/test/playwright/Renderer.test.ts +++ b/test/playwright/Renderer.test.ts @@ -5,10 +5,10 @@ import { test } from '@playwright/test'; import { ITestContext, createTestContext, openTerminal } from './TestUtils'; -import { injectSharedRendererTests } from './SharedRendererTests'; +import { ISharedRendererTestContext, injectSharedRendererTests } from './SharedRendererTests'; let ctx: ITestContext; -const ctxWrapper: { value: ITestContext } = { value: undefined } as any; +const ctxWrapper: ISharedRendererTestContext = { value: undefined } as any; test.beforeAll(async ({ browser }) => { ctx = await createTestContext(browser); ctxWrapper.value = ctx; diff --git a/test/playwright/SharedRendererTests.ts b/test/playwright/SharedRendererTests.ts index 2120925f..f8d70cc9 100644 --- a/test/playwright/SharedRendererTests.ts +++ b/test/playwright/SharedRendererTests.ts @@ -8,7 +8,12 @@ import { LocatorScreenshotOptions, test } from '@playwright/test'; import { ITheme } from 'xterm'; import { ITestContext, MaybeAsync, pollFor } from './TestUtils'; -export function injectSharedRendererTests(ctx: { value: ITestContext }): void { +export interface ISharedRendererTestContext { + value: ITestContext; + skipCanvasExceptions?: boolean; +} + +export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void { test.beforeEach(async () => { await ctx.value.proxy.reset(); ctx.value.page.evaluate(` @@ -934,7 +939,7 @@ export function injectSharedRendererTests(ctx: { value: ITestContext }): void { }); }); - test.describe('selectionBackground', async () => { + (ctx.skipCanvasExceptions ? test.describe.skip : test.describe)('selectionBackground', async () => { test('should resolve the inverse foreground color based on the original background color, not the selection', async () => { const theme: ITheme = { foreground: '#FF0000', @@ -970,7 +975,7 @@ export function injectSharedRendererTests(ctx: { value: ITestContext }): void { }); }); - test.describe('selectionForeground', () => { + (ctx.skipCanvasExceptions ? test.describe.skip : test.describe)('selectionForeground', () => { test('transparent background inverse', async () => { const theme: ITheme = { selectionForeground: '#ff0000' @@ -1050,7 +1055,7 @@ export function injectSharedRendererTests(ctx: { value: ITestContext }): void { await ctx.value.proxy.write( data); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 255, 255]); }); - test('backgroundColor should ignore inverse (only bg on decoration)', async () => { + (ctx.skipCanvasExceptions ? test.skip : test)('backgroundColor should ignore inverse (only bg on decoration)', async () => { const data = `\x1b[7m█ \x1b[0m`; await ctx.value.proxy.write( data); await ctx.value.page.evaluate(` From 02aadf7c3efc86e4763eff8dd0336e46910711aa Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 12:17:00 -0700 Subject: [PATCH 61/99] Remove firefox exception from canvas --- addons/xterm-addon-canvas/test/CanvasRenderer.test.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/addons/xterm-addon-canvas/test/CanvasRenderer.test.ts b/addons/xterm-addon-canvas/test/CanvasRenderer.test.ts index c87c376f..a129efa5 100644 --- a/addons/xterm-addon-canvas/test/CanvasRenderer.test.ts +++ b/addons/xterm-addon-canvas/test/CanvasRenderer.test.ts @@ -25,11 +25,5 @@ test.beforeAll(async ({ browser }) => { test.afterAll(async () => await ctx.page.close()); test.describe('Canvas Renderer Integration Tests', async () => { - // HACK: webgl2 is often not supported in headless firefox on Linux - // https://github.com/microsoft/playwright/issues/11566 - if (platform() === 'linux') { - test.skip(({ browserName }) => browserName === 'firefox'); - } - injectSharedRendererTests(ctxWrapper); }); From 71eb897708fa150a6cdaa7399dc8a351e54780e6 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 12:17:22 -0700 Subject: [PATCH 62/99] Add npm start to canvas --- addons/xterm-addon-canvas/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/xterm-addon-canvas/package.json b/addons/xterm-addon-canvas/package.json index 701baafe..2c8fcb61 100644 --- a/addons/xterm-addon-canvas/package.json +++ b/addons/xterm-addon-canvas/package.json @@ -19,7 +19,8 @@ "build": "../../node_modules/.bin/tsc -p .", "prepackage": "npm run build", "package": "../../node_modules/.bin/webpack", - "prepublishOnly": "npm run package" + "prepublishOnly": "npm run package", + "start": "node ../../demo/start" }, "peerDependencies": { "xterm": "^5.0.0" From 408240dd9708d5f65fdaf77d0551efda537335ae Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 12:22:38 -0700 Subject: [PATCH 63/99] Tweak canvas renderer test --- addons/xterm-addon-canvas/test/CanvasRenderer.test.ts | 5 ++--- addons/xterm-addon-webgl/test/WebglRenderer.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/addons/xterm-addon-canvas/test/CanvasRenderer.test.ts b/addons/xterm-addon-canvas/test/CanvasRenderer.test.ts index a129efa5..447d01dd 100644 --- a/addons/xterm-addon-canvas/test/CanvasRenderer.test.ts +++ b/addons/xterm-addon-canvas/test/CanvasRenderer.test.ts @@ -6,19 +6,18 @@ import test from '@playwright/test'; import { ISharedRendererTestContext, injectSharedRendererTests } from '../../../out-test/playwright/SharedRendererTests'; import { ITestContext, createTestContext, openTerminal } from '../../../out-test/playwright/TestUtils'; -import { platform } from 'os'; let ctx: ITestContext; const ctxWrapper: ISharedRendererTestContext = { value: undefined, - skipSelectionTests: true + skipCanvasExceptions: true } as any; test.beforeAll(async ({ browser }) => { ctx = await createTestContext(browser); await openTerminal(ctx); ctxWrapper.value = ctx; await ctx.page.evaluate(` - window.addon = new CanvasAddon(true); + window.addon = new window.CanvasAddon(true); window.term.loadAddon(window.addon); `); }); diff --git a/addons/xterm-addon-webgl/test/WebglRenderer.test.ts b/addons/xterm-addon-webgl/test/WebglRenderer.test.ts index c6789613..dc23a0cd 100644 --- a/addons/xterm-addon-webgl/test/WebglRenderer.test.ts +++ b/addons/xterm-addon-webgl/test/WebglRenderer.test.ts @@ -16,7 +16,7 @@ test.beforeAll(async ({ browser }) => { await openTerminal(ctx); ctxWrapper.value = ctx; await ctx.page.evaluate(` - window.addon = new WebglAddon(true); + window.addon = new window.WebglAddon(true); window.term.loadAddon(window.addon); `); }); @@ -35,7 +35,7 @@ test.describe('WebGL Renderer Integration Tests', async () => { strictEqual(await ctx.page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 0); // Re-create webgl addon to avoid side effects impacting other tests await ctx.page.evaluate(` - window.addon = new WebglAddon(true); + window.addon = new window.WebglAddon(true); window.term.loadAddon(window.addon); `); }); From beeff131a93a181607020665b499c0960bd5c86e Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 12:28:08 -0700 Subject: [PATCH 64/99] Disable canvas tests on webkit --- addons/xterm-addon-canvas/test/CanvasRenderer.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/addons/xterm-addon-canvas/test/CanvasRenderer.test.ts b/addons/xterm-addon-canvas/test/CanvasRenderer.test.ts index 447d01dd..b46c813a 100644 --- a/addons/xterm-addon-canvas/test/CanvasRenderer.test.ts +++ b/addons/xterm-addon-canvas/test/CanvasRenderer.test.ts @@ -17,12 +17,15 @@ test.beforeAll(async ({ browser }) => { await openTerminal(ctx); ctxWrapper.value = ctx; await ctx.page.evaluate(` - window.addon = new window.CanvasAddon(true); + window.addon = new CanvasAddon(true); window.term.loadAddon(window.addon); `); }); test.afterAll(async () => await ctx.page.close()); -test.describe('Canvas Renderer Integration Tests', async () => { +test.describe('Canvas Renderer Integration Tests', () => { + // HACK: The tests fail for an unknown reason + test.skip(({ browserName }) => browserName === 'webkit'); + injectSharedRendererTests(ctxWrapper); }); From ebd27de4e1a3e617a9e6c7668bed77fb4267d30e Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 12:31:27 -0700 Subject: [PATCH 65/99] Re-enable core tests --- bin/test_playwright.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/test_playwright.js b/bin/test_playwright.js index 379c0e26..f70a1833 100644 --- a/bin/test_playwright.js +++ b/bin/test_playwright.js @@ -9,7 +9,7 @@ const cp = require('child_process'); const path = require('path'); const configs = [ - // { name: 'core', path: 'out-test/playwright/playwright.config.js' }, + { name: 'core', path: 'out-test/playwright/playwright.config.js' }, { name: 'xterm-addon-canvas', path: 'addons/xterm-addon-canvas/out-test/playwright.config.js' }, { name: 'xterm-addon-webgl', path: 'addons/xterm-addon-webgl/out-test/playwright.config.js' } ]; From 1798cab2225b2bdd4037d83b74bf76b96c66deff Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 12:37:53 -0700 Subject: [PATCH 66/99] Remove webgl canvas check It complicates loading the addon --- addons/xterm-addon-webgl/test/WebglRenderer.test.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/addons/xterm-addon-webgl/test/WebglRenderer.test.ts b/addons/xterm-addon-webgl/test/WebglRenderer.test.ts index dc23a0cd..14a9cb81 100644 --- a/addons/xterm-addon-webgl/test/WebglRenderer.test.ts +++ b/addons/xterm-addon-webgl/test/WebglRenderer.test.ts @@ -29,16 +29,5 @@ test.describe('WebGL Renderer Integration Tests', async () => { test.skip(({ browserName }) => browserName === 'firefox'); } - test('dispose removes renderer canvases', async function(): Promise { - strictEqual(await ctx.page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 2); - await ctx.page.evaluate(`addon.dispose()`); - strictEqual(await ctx.page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 0); - // Re-create webgl addon to avoid side effects impacting other tests - await ctx.page.evaluate(` - window.addon = new window.WebglAddon(true); - window.term.loadAddon(window.addon); - `); - }); - injectSharedRendererTests(ctxWrapper); }); From 2e10a7f097518dc2d1b97cfc61608f58549cf7e1 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 12:42:26 -0700 Subject: [PATCH 67/99] Add window prefer to global symbols --- addons/xterm-addon-canvas/test/CanvasRenderer.test.ts | 2 +- test/playwright/TestUtils.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/xterm-addon-canvas/test/CanvasRenderer.test.ts b/addons/xterm-addon-canvas/test/CanvasRenderer.test.ts index b46c813a..76f57394 100644 --- a/addons/xterm-addon-canvas/test/CanvasRenderer.test.ts +++ b/addons/xterm-addon-canvas/test/CanvasRenderer.test.ts @@ -17,7 +17,7 @@ test.beforeAll(async ({ browser }) => { await openTerminal(ctx); ctxWrapper.value = ctx; await ctx.page.evaluate(` - window.addon = new CanvasAddon(true); + window.addon = new window.CanvasAddon(true); window.term.loadAddon(window.addon); `); }); diff --git a/test/playwright/TestUtils.ts b/test/playwright/TestUtils.ts index 68ef2d69..6bf3eb8f 100644 --- a/test/playwright/TestUtils.ts +++ b/test/playwright/TestUtils.ts @@ -363,7 +363,7 @@ export async function openTerminal(ctx: ITestContext, options: ITerminalOptions // 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 = new window.Terminal(${JSON.stringify({ allowProposedApi: true, ...options })}); window.term.open(document.querySelector('#terminal-container')); `); await ctx.page.waitForSelector('.xterm-rows'); From 328e97336b183d51d520791f8057fdb80bd9786a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 12:58:33 -0700 Subject: [PATCH 68/99] Try space out suites --- .../test/WebglRenderer.test.ts | 1 - bin/test_playwright.js | 28 +++++++++++-------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/addons/xterm-addon-webgl/test/WebglRenderer.test.ts b/addons/xterm-addon-webgl/test/WebglRenderer.test.ts index 14a9cb81..718d8e03 100644 --- a/addons/xterm-addon-webgl/test/WebglRenderer.test.ts +++ b/addons/xterm-addon-webgl/test/WebglRenderer.test.ts @@ -4,7 +4,6 @@ */ import test from '@playwright/test'; -import { strictEqual } from 'assert'; import { ISharedRendererTestContext, injectSharedRendererTests } from '../../../out-test/playwright/SharedRendererTests'; import { ITestContext, createTestContext, openTerminal } from '../../../out-test/playwright/TestUtils'; import { platform } from 'os'; diff --git a/bin/test_playwright.js b/bin/test_playwright.js index f70a1833..08ecec57 100644 --- a/bin/test_playwright.js +++ b/bin/test_playwright.js @@ -7,6 +7,7 @@ const cp = require('child_process'); const path = require('path'); +const { setTimeout } = require('timers/promises'); const configs = [ { name: 'core', path: 'out-test/playwright/playwright.config.js' }, @@ -19,17 +20,22 @@ function npmBinScript(script) { `${script}.cmd` : script)); } -for (const config of configs) { - const command = npmBinScript('playwright'); - const args = ['test', '-c', config.path, ...process.argv.slice(2)]; - console.log(`Running suite \x1b[1;34m${config.name}...\x1b[0m`); - console.log(`\n\x1b[32m${command}\x1b[0m`, args); - const run = cp.spawnSync(command, args, { - cwd: path.resolve(__dirname, '..'), - stdio: 'inherit' +async function run() { + for (const config of configs) { + const command = npmBinScript('playwright'); + const args = ['test', '-c', config.path, ...process.argv.slice(2)]; + console.log(`Running suite \x1b[1;34m${config.name}...\x1b[0m`); + console.log(`\n\x1b[32m${command}\x1b[0m`, args); + const run = cp.spawnSync(command, args, { + cwd: path.resolve(__dirname, '..'), + stdio: 'inherit' + } + ); + if (run.status) { + process.exit(run.status); } - ); - if (run.status) { - process.exit(run.status); + // Space out test runs to ensure servers don't step on each other + await setTimeout(1000); } } +run(); From 298c5b2041f78f06891a0f9e48bc2675bd441140 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 13:09:47 -0700 Subject: [PATCH 69/99] Split up playwright suites into steps --- .github/workflows/ci.yml | 8 ++++++-- bin/test_playwright.js | 20 +++++++++++++++----- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 21c11fc4..95de7362 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -259,8 +259,12 @@ jobs: ls -R - name: Build demo run: yarn build-demo - - name: Integration tests - run: yarn test-playwright-${{ matrix.browser }} --forbid-only + - name: Integration tests (core) + run: yarn test-playwright-${{ matrix.browser }} --forbid-only --suite=core + - name: Integration tests (xterm-addon-canvas) + run: yarn test-playwright-${{ matrix.browser }} --forbid-only --suite=xterm-addon-canvas + - name: Integration tests (xterm-addon-webgl) + run: yarn test-playwright-${{ matrix.browser }} --forbid-only --suite=xterm-addon-webgl test-api: needs: build diff --git a/bin/test_playwright.js b/bin/test_playwright.js index 08ecec57..d6c9d3dd 100644 --- a/bin/test_playwright.js +++ b/bin/test_playwright.js @@ -7,14 +7,26 @@ const cp = require('child_process'); const path = require('path'); -const { setTimeout } = require('timers/promises'); -const configs = [ +let argv = process.argv.slice(2); +let suiteFilter = undefined; +while (argv.some(e => e.startsWith('--suite='))) { + const i = argv.findIndex(e => e.startsWith('--suite=')); + const match = argv[i].match(/--suite=(?.+)/) + suiteFilter = match?.groups?.suitename ?? undefined; + argv.splice(i, 1); +} + +let configs = [ { name: 'core', path: 'out-test/playwright/playwright.config.js' }, { name: 'xterm-addon-canvas', path: 'addons/xterm-addon-canvas/out-test/playwright.config.js' }, { name: 'xterm-addon-webgl', path: 'addons/xterm-addon-webgl/out-test/playwright.config.js' } ]; +if (suiteFilter) { + configs = configs.filter(e => e.name === suiteFilter); +} + function npmBinScript(script) { return path.resolve(__dirname, `../node_modules/.bin/` + (process.platform === 'win32' ? `${script}.cmd` : script)); @@ -23,7 +35,7 @@ function npmBinScript(script) { async function run() { for (const config of configs) { const command = npmBinScript('playwright'); - const args = ['test', '-c', config.path, ...process.argv.slice(2)]; + const args = ['test', '-c', config.path, ...argv.slice(2)]; console.log(`Running suite \x1b[1;34m${config.name}...\x1b[0m`); console.log(`\n\x1b[32m${command}\x1b[0m`, args); const run = cp.spawnSync(command, args, { @@ -34,8 +46,6 @@ async function run() { if (run.status) { process.exit(run.status); } - // Space out test runs to ensure servers don't step on each other - await setTimeout(1000); } } run(); From 4af4d8dcc6d57e531e5dc7261226d7ced475091d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 13:12:49 -0700 Subject: [PATCH 70/99] Put webgl first --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 95de7362..0bcc308c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -259,12 +259,12 @@ jobs: ls -R - name: Build demo run: yarn build-demo + - name: Integration tests (xterm-addon-webgl) + run: yarn test-playwright-${{ matrix.browser }} --forbid-only --suite=xterm-addon-webgl - name: Integration tests (core) run: yarn test-playwright-${{ matrix.browser }} --forbid-only --suite=core - name: Integration tests (xterm-addon-canvas) run: yarn test-playwright-${{ matrix.browser }} --forbid-only --suite=xterm-addon-canvas - - name: Integration tests (xterm-addon-webgl) - run: yarn test-playwright-${{ matrix.browser }} --forbid-only --suite=xterm-addon-webgl test-api: needs: build From 534b03b99f2c98ea2a220d7c242166383c262a46 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 13:19:29 -0700 Subject: [PATCH 71/99] Revert "Put webgl first" This reverts commit 4af4d8dcc6d57e531e5dc7261226d7ced475091d. --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0bcc308c..95de7362 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -259,12 +259,12 @@ jobs: ls -R - name: Build demo run: yarn build-demo - - name: Integration tests (xterm-addon-webgl) - run: yarn test-playwright-${{ matrix.browser }} --forbid-only --suite=xterm-addon-webgl - name: Integration tests (core) run: yarn test-playwright-${{ matrix.browser }} --forbid-only --suite=core - name: Integration tests (xterm-addon-canvas) run: yarn test-playwright-${{ matrix.browser }} --forbid-only --suite=xterm-addon-canvas + - name: Integration tests (xterm-addon-webgl) + run: yarn test-playwright-${{ matrix.browser }} --forbid-only --suite=xterm-addon-webgl test-api: needs: build From 2d7450520871aed22ce8b79488cd0536a086019d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 13:26:10 -0700 Subject: [PATCH 72/99] Avoid importing os --- addons/xterm-addon-webgl/test/WebglRenderer.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/addons/xterm-addon-webgl/test/WebglRenderer.test.ts b/addons/xterm-addon-webgl/test/WebglRenderer.test.ts index 718d8e03..d24ed5a3 100644 --- a/addons/xterm-addon-webgl/test/WebglRenderer.test.ts +++ b/addons/xterm-addon-webgl/test/WebglRenderer.test.ts @@ -6,7 +6,6 @@ import test from '@playwright/test'; import { ISharedRendererTestContext, injectSharedRendererTests } from '../../../out-test/playwright/SharedRendererTests'; import { ITestContext, createTestContext, openTerminal } from '../../../out-test/playwright/TestUtils'; -import { platform } from 'os'; let ctx: ITestContext; const ctxWrapper: ISharedRendererTestContext = { value: undefined } as any; @@ -24,9 +23,7 @@ test.afterAll(async () => await ctx.page.close()); test.describe('WebGL Renderer Integration Tests', async () => { // HACK: webgl2 is often not supported in headless firefox on Linux // https://github.com/microsoft/playwright/issues/11566 - if (platform() === 'linux') { - test.skip(({ browserName }) => browserName === 'firefox'); - } + test.skip(({ browserName, userAgent}) => (userAgent?.includes('Linux') ?? false) && browserName === 'firefox'); injectSharedRendererTests(ctxWrapper); }); From b99158f8f0d4540a2a1d201d14781d00a5c6e4ae Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 13:30:27 -0700 Subject: [PATCH 73/99] Add workers back to playwright ci --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 95de7362..b33c2545 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -260,11 +260,11 @@ jobs: - name: Build demo run: yarn build-demo - name: Integration tests (core) - run: yarn test-playwright-${{ matrix.browser }} --forbid-only --suite=core + run: yarn test-playwright-${{ matrix.browser }} --forbid-only --workers 4 --suite=core - name: Integration tests (xterm-addon-canvas) - run: yarn test-playwright-${{ matrix.browser }} --forbid-only --suite=xterm-addon-canvas + run: yarn test-playwright-${{ matrix.browser }} --forbid-only --workers 4 --suite=xterm-addon-canvas - name: Integration tests (xterm-addon-webgl) - run: yarn test-playwright-${{ matrix.browser }} --forbid-only --suite=xterm-addon-webgl + run: yarn test-playwright-${{ matrix.browser }} --forbid-only --workers 4 --suite=xterm-addon-webgl test-api: needs: build From 3d3f58b6ffa969374559a43eaa313e6662e0f86e Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 14:10:37 -0700 Subject: [PATCH 74/99] Have playwright addon tests only start server, not build it --- addons/xterm-addon-canvas/package.json | 2 +- addons/xterm-addon-canvas/test/playwright.config.ts | 2 +- addons/xterm-addon-webgl/package.json | 2 +- addons/xterm-addon-webgl/test/WebglRenderer.test.ts | 2 +- addons/xterm-addon-webgl/test/playwright.config.ts | 2 +- demo/start-server-only.js | 10 ++++++++++ package.json | 1 + 7 files changed, 16 insertions(+), 5 deletions(-) create mode 100644 demo/start-server-only.js diff --git a/addons/xterm-addon-canvas/package.json b/addons/xterm-addon-canvas/package.json index 2c8fcb61..76b97758 100644 --- a/addons/xterm-addon-canvas/package.json +++ b/addons/xterm-addon-canvas/package.json @@ -20,7 +20,7 @@ "prepackage": "npm run build", "package": "../../node_modules/.bin/webpack", "prepublishOnly": "npm run package", - "start": "node ../../demo/start" + "start-server-only": "node ../../demo/start-server-only" }, "peerDependencies": { "xterm": "^5.0.0" diff --git a/addons/xterm-addon-canvas/test/playwright.config.ts b/addons/xterm-addon-canvas/test/playwright.config.ts index 3d1fe4fb..b0e565c5 100644 --- a/addons/xterm-addon-canvas/test/playwright.config.ts +++ b/addons/xterm-addon-canvas/test/playwright.config.ts @@ -26,7 +26,7 @@ const config: PlaywrightTestConfig = { ], reporter: 'list', webServer: { - command: 'npm start', + command: 'npm run start-server-only', port: 3000, timeout: 120000, reuseExistingServer: !process.env.CI diff --git a/addons/xterm-addon-webgl/package.json b/addons/xterm-addon-webgl/package.json index 7e9765fc..1f88e150 100644 --- a/addons/xterm-addon-webgl/package.json +++ b/addons/xterm-addon-webgl/package.json @@ -20,7 +20,7 @@ "prepackage": "npm run build", "package": "../../node_modules/.bin/webpack", "prepublishOnly": "npm run package", - "start": "node ../../demo/start" + "start-server-only": "node ../../demo/start-server-only" }, "peerDependencies": { "xterm": "^5.0.0" diff --git a/addons/xterm-addon-webgl/test/WebglRenderer.test.ts b/addons/xterm-addon-webgl/test/WebglRenderer.test.ts index d24ed5a3..99710cb9 100644 --- a/addons/xterm-addon-webgl/test/WebglRenderer.test.ts +++ b/addons/xterm-addon-webgl/test/WebglRenderer.test.ts @@ -23,7 +23,7 @@ test.afterAll(async () => await ctx.page.close()); test.describe('WebGL Renderer Integration Tests', async () => { // HACK: webgl2 is often not supported in headless firefox on Linux // https://github.com/microsoft/playwright/issues/11566 - test.skip(({ browserName, userAgent}) => (userAgent?.includes('Linux') ?? false) && browserName === 'firefox'); + test.skip(({ browserName, userAgent }) => (userAgent?.includes('Linux') ?? false) && browserName === 'firefox'); injectSharedRendererTests(ctxWrapper); }); diff --git a/addons/xterm-addon-webgl/test/playwright.config.ts b/addons/xterm-addon-webgl/test/playwright.config.ts index 3d1fe4fb..b0e565c5 100644 --- a/addons/xterm-addon-webgl/test/playwright.config.ts +++ b/addons/xterm-addon-webgl/test/playwright.config.ts @@ -26,7 +26,7 @@ const config: PlaywrightTestConfig = { ], reporter: 'list', webServer: { - command: 'npm start', + command: 'npm run start-server-only', port: 3000, timeout: 120000, reuseExistingServer: !process.env.CI diff --git a/demo/start-server-only.js b/demo/start-server-only.js new file mode 100644 index 00000000..b240966f --- /dev/null +++ b/demo/start-server-only.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +// @ts-check + +const startServer = require('./server.js'); + +startServer(); diff --git a/package.json b/package.json index 77be8f26..534f44ec 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", + "start-server-only": "node demo/start-server-only", "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/", From 14872e091ea6d576512613aaa77f57cf4452fd18 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 14:21:18 -0700 Subject: [PATCH 75/99] Revert "Avoid importing os" This reverts commit 2d7450520871aed22ce8b79488cd0536a086019d. --- addons/xterm-addon-webgl/test/WebglRenderer.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/addons/xterm-addon-webgl/test/WebglRenderer.test.ts b/addons/xterm-addon-webgl/test/WebglRenderer.test.ts index 99710cb9..718d8e03 100644 --- a/addons/xterm-addon-webgl/test/WebglRenderer.test.ts +++ b/addons/xterm-addon-webgl/test/WebglRenderer.test.ts @@ -6,6 +6,7 @@ import test from '@playwright/test'; import { ISharedRendererTestContext, injectSharedRendererTests } from '../../../out-test/playwright/SharedRendererTests'; import { ITestContext, createTestContext, openTerminal } from '../../../out-test/playwright/TestUtils'; +import { platform } from 'os'; let ctx: ITestContext; const ctxWrapper: ISharedRendererTestContext = { value: undefined } as any; @@ -23,7 +24,9 @@ test.afterAll(async () => await ctx.page.close()); test.describe('WebGL Renderer Integration Tests', async () => { // HACK: webgl2 is often not supported in headless firefox on Linux // https://github.com/microsoft/playwright/issues/11566 - test.skip(({ browserName, userAgent }) => (userAgent?.includes('Linux') ?? false) && browserName === 'firefox'); + if (platform() === 'linux') { + test.skip(({ browserName }) => browserName === 'firefox'); + } injectSharedRendererTests(ctxWrapper); }); From 9f5ff468e1ae010cc1d9e6b421a6e5d15e3df09d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 19:55:10 -0700 Subject: [PATCH 76/99] Remove selection color test buttons There's too many theme colors, we'll leave it to the console --- demo/client.ts | 16 ---------------- demo/index.html | 10 +--------- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index 8cc266aa..7ade385e 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -120,8 +120,6 @@ const actionElements = { findResults: document.querySelector('#find-results') }; const paddingElement = document.getElementById('padding') as HTMLInputElement; -const selectionBackgroundElement = document.getElementById('selectionBackground') as HTMLInputElement; -const selectionInactiveBackgroundElement = document.getElementById('selectionInactiveBackground') as HTMLInputElement; const xtermjsTheme = { foreground: '#F8F8F8', @@ -149,18 +147,6 @@ function setPadding(): void { addons.fit.instance.fit(); } -function setSelectionInactiveBackground(): void { - const theme = { ...term.options.theme }; - theme.selectionInactiveBackground = selectionInactiveBackgroundElement.value; - term.options.theme = theme; -} - -function setSelectionBackground(): void { - const theme = { ...term.options.theme }; - theme.selectionBackground = selectionBackgroundElement.value; - term.options.theme = theme; -} - function getSearchOptions(): ISearchOptions { return { regex: (document.getElementById('regex') as HTMLInputElement).checked, @@ -349,8 +335,6 @@ function createTerminal(): void { resizeObserver.observe(terminalContainer); addDomListener(paddingElement, 'change', setPadding); - addDomListener(selectionInactiveBackgroundElement, 'change', setSelectionInactiveBackground); - addDomListener(selectionBackgroundElement, 'change', setSelectionBackground); addDomListener(actionElements.findNext, 'keydown', (e) => { if (e.key === 'Enter') { diff --git a/demo/index.html b/demo/index.html index f56af820..7d7329a7 100644 --- a/demo/index.html +++ b/demo/index.html @@ -75,14 +75,6 @@
-
- - -
-
- - -

Test

@@ -112,7 +104,7 @@
Weblinks Addon
- +
Image Test
From f159a862e412b4db0bb61e1cf9b1a854c5fcc4b9 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 19:57:17 -0700 Subject: [PATCH 77/99] Add default xterm.js theme inactive selection bg --- demo/client.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/demo/client.ts b/demo/client.ts index 7ade385e..fbe87195 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -125,6 +125,7 @@ const xtermjsTheme = { foreground: '#F8F8F8', background: '#2D2E2C', selectionBackground: '#5DA5D533', + selectionInactiveBackground: '#444444', black: '#1E1E1D', brightBlack: '#262625', red: '#CE5C5C', From fa168bd4c040a28266eefd90e70bda6454b26523 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Sep 2023 20:21:58 -0700 Subject: [PATCH 78/99] Add a test which fails without the change --- demo/client.ts | 2 +- test/playwright/SharedRendererTests.ts | 26 ++++++++++++++++++++++++++ test/playwright/TestUtils.ts | 4 ++-- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index fbe87195..06949a8a 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -125,7 +125,7 @@ const xtermjsTheme = { foreground: '#F8F8F8', background: '#2D2E2C', selectionBackground: '#5DA5D533', - selectionInactiveBackground: '#444444', + selectionInactiveBackground: '#555555AA', black: '#1E1E1D', brightBlack: '#262625', red: '#CE5C5C', diff --git a/test/playwright/SharedRendererTests.ts b/test/playwright/SharedRendererTests.ts index f8d70cc9..a943cd69 100644 --- a/test/playwright/SharedRendererTests.ts +++ b/test/playwright/SharedRendererTests.ts @@ -960,6 +960,32 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void }); }); + (ctx.skipCanvasExceptions ? test.describe.skip : test.describe)('selectionInactiveBackground', async () => { + test.only('should render the the inactive selection when not focused', async () => { + const theme: ITheme = { + selectionBackground: '#FF000080', + selectionInactiveBackground: '#0000FF80' + }; + await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.value.proxy.focus(); + // Check both the cursor line and another line + await ctx.value.proxy.writeln('_'); + await ctx.value.proxy.write('_'); + await ctx.value.page.evaluate(`window.term.selectAll()`); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [128, 0, 0, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [128, 0, 0, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 2), [128, 0, 0, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 2), [128, 0, 0, 255]); + await ctx.value.page.evaluate(`document.activeElement.blur()`); + frameDetails = undefined; + // Selection only cell needs to be first to ensure renderer has kicked in + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 128, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [0, 0, 128, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 2), [0, 0, 128, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 2), [0, 0, 128, 255]); + }); + }); + test.describe('allowTransparency', async () => { test.beforeEach(() => ctx.value.page.evaluate(`term.options.allowTransparency = true`)); diff --git a/test/playwright/TestUtils.ts b/test/playwright/TestUtils.ts index 6bf3eb8f..02a08907 100644 --- a/test/playwright/TestUtils.ts +++ b/test/playwright/TestUtils.ts @@ -400,8 +400,8 @@ export async function pollFor(page: playwright.Page, evalOrFn: string | (() = deepStrictEqual(result, val, ([ `pollFor max duration exceeded.`, (`Last comparison: ` + - `${typeof result === 'object' ? JSON.stringify(result) : result} !== ` + - `${typeof val === 'object' ? JSON.stringify(val) : val}`), + `${typeof result === 'object' ? JSON.stringify(result) : result} (actual) !== ` + + `${typeof val === 'object' ? JSON.stringify(val) : val} (expected)`), `Stack: ${stack}` ].join('\n'))); } From 5126481de2aafa8256b47d0254fc0c80d1d7371c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 9 Sep 2023 04:55:56 -0700 Subject: [PATCH 79/99] Remove only --- test/playwright/SharedRendererTests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/playwright/SharedRendererTests.ts b/test/playwright/SharedRendererTests.ts index a943cd69..5e245c14 100644 --- a/test/playwright/SharedRendererTests.ts +++ b/test/playwright/SharedRendererTests.ts @@ -961,7 +961,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void }); (ctx.skipCanvasExceptions ? test.describe.skip : test.describe)('selectionInactiveBackground', async () => { - test.only('should render the the inactive selection when not focused', async () => { + test('should render the the inactive selection when not focused', async () => { const theme: ITheme = { selectionBackground: '#FF000080', selectionInactiveBackground: '#0000FF80' From 4e69307074215b42e5fb4e152a67b1403b1d3929 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 9 Sep 2023 05:59:24 -0700 Subject: [PATCH 80/99] Add regression test --- test/playwright/SharedRendererTests.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/test/playwright/SharedRendererTests.ts b/test/playwright/SharedRendererTests.ts index f8d70cc9..4fb6db13 100644 --- a/test/playwright/SharedRendererTests.ts +++ b/test/playwright/SharedRendererTests.ts @@ -144,7 +144,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void await pollFor(ctx.value.page, () => getCellColor(ctx.value, 8, 1), [22, 23, 24, 255]); }); - test('foreground 0-15 inivisible', async () => { + test('foreground 0-15 invisible', async () => { const theme: ITheme = { black: '#010203', red: '#040506', @@ -167,7 +167,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void await pollFor(ctx.value.page, () => getCellColor(ctx.value, 8, 1), [0, 0, 0, 255]); }); - test('background 0-15 inivisible', async () => { + test('background 0-15 invisible', async () => { const theme: ITheme = { black: '#010203', red: '#040506', @@ -1070,6 +1070,17 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [0, 0, 255, 255]); // inverse background of ' ' should be decoration bg override }); }); + + test.describe('regression tests', () => { + test('#4758: multiple invisible text characters without SGR change should not be rendered', async () => { + // Regression test: #4758 when multiple invisible characters are used + await ctx.value.proxy.writeln(`\x1b[8m■■`); + // Full refresh as the before result is the same as after + await ctx.value.proxy.refresh(0, await ctx.value.proxy.rows - 1); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [0, 0, 0, 255]); + }); + }); } /** From 57ab29bb8529bf39703c7f2dac17c2dcba9a4250 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 9 Sep 2023 06:02:32 -0700 Subject: [PATCH 81/99] Handle canvas renderer's more delayed rendering --- test/playwright/SharedRendererTests.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/playwright/SharedRendererTests.ts b/test/playwright/SharedRendererTests.ts index 4fb6db13..d811ad21 100644 --- a/test/playwright/SharedRendererTests.ts +++ b/test/playwright/SharedRendererTests.ts @@ -1074,11 +1074,13 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void test.describe('regression tests', () => { test('#4758: multiple invisible text characters without SGR change should not be rendered', async () => { // Regression test: #4758 when multiple invisible characters are used - await ctx.value.proxy.writeln(`\x1b[8m■■`); + await ctx.value.proxy.writeln(`■\x1b[8m■■`); // Full refresh as the before result is the same as after await ctx.value.proxy.refresh(0, await ctx.value.proxy.rows - 1); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]); + // Control to ensure rendering has occurred + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 255, 255, 255]); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [0, 0, 0, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [0, 0, 0, 255]); }); }); } From 056206d7a4ab2be784343fc4f06c9432a7cbb723 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 9 Sep 2023 06:09:07 -0700 Subject: [PATCH 82/99] Clear theme at start of every renderer test --- test/playwright/SharedRendererTests.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/playwright/SharedRendererTests.ts b/test/playwright/SharedRendererTests.ts index d811ad21..8a7eabf4 100644 --- a/test/playwright/SharedRendererTests.ts +++ b/test/playwright/SharedRendererTests.ts @@ -19,6 +19,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void ctx.value.page.evaluate(` window.term.options.minimumContrastRatio = 1; window.term.options.allowTransparency = false; + window.term.options.theme = undefined; `); // Clear the cached screenshot before each test frameDetails = undefined; From a10113543b073619696dc3b7cdaf49cb7e15faa7 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 9 Sep 2023 06:21:37 -0700 Subject: [PATCH 83/99] Check add empty cells that are being asserted on --- test/playwright/SharedRendererTests.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/playwright/SharedRendererTests.ts b/test/playwright/SharedRendererTests.ts index 4dcf590b..a2e79286 100644 --- a/test/playwright/SharedRendererTests.ts +++ b/test/playwright/SharedRendererTests.ts @@ -970,8 +970,8 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); await ctx.value.proxy.focus(); // Check both the cursor line and another line - await ctx.value.proxy.writeln('_'); - await ctx.value.proxy.write('_'); + await ctx.value.proxy.writeln('_ '); + await ctx.value.proxy.write('_ '); await ctx.value.page.evaluate(`window.term.selectAll()`); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [128, 0, 0, 255]); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [128, 0, 0, 255]); From 575cd8d28b3bb8d27d80197f9bc03745f265fb28 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 9 Sep 2023 06:51:13 -0700 Subject: [PATCH 84/99] Avoid block elements in renderer tests Fixes #4772 --- test/playwright/SharedRendererTests.ts | 57 ++++++++++++-------------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/test/playwright/SharedRendererTests.ts b/test/playwright/SharedRendererTests.ts index a2e79286..e19ff146 100644 --- a/test/playwright/SharedRendererTests.ts +++ b/test/playwright/SharedRendererTests.ts @@ -38,7 +38,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void white: '#161718' }; await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await ctx.value.proxy.write(`\x1b[30m█\x1b[31m█\x1b[32m█\x1b[33m█\x1b[34m█\x1b[35m█\x1b[36m█\x1b[37m█`); + await ctx.value.proxy.write(`\x1b[30m■\x1b[31m■\x1b[32m■\x1b[33m■\x1b[34m■\x1b[35m■\x1b[36m■\x1b[37m■`); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [1, 2, 3, 255]); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [4, 5, 6, 255]); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [7, 8, 9, 255]); @@ -65,7 +65,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void window.term.options.theme = ${JSON.stringify(theme)}; window.term.options.drawBoldTextInBrightColors = true; `); - await ctx.value.proxy.write(`\x1b[1;30m█\x1b[1;31m█\x1b[1;32m█\x1b[1;33m█\x1b[1;34m█\x1b[1;35m█\x1b[1;36m█\x1b[1;37m█`); + await ctx.value.proxy.write(`\x1b[1;30m■\x1b[1;31m■\x1b[1;32m■\x1b[1;33m■\x1b[1;34m■\x1b[1;35m■\x1b[1;36m■\x1b[1;37m■`); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [1, 2, 3, 255]); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [4, 5, 6, 255]); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [7, 8, 9, 255]); @@ -134,7 +134,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void white: '#161718' }; await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await ctx.value.proxy.write(`\x1b[7;40m█\x1b[7;41m█\x1b[7;42m█\x1b[7;43m█\x1b[7;44m█\x1b[7;45m█\x1b[7;46m█\x1b[7;47m█`); + await ctx.value.proxy.write(`\x1b[7;40m■\x1b[7;41m■\x1b[7;42m■\x1b[7;43m■\x1b[7;44m■\x1b[7;45m■\x1b[7;46m■\x1b[7;47m■`); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [1, 2, 3, 255]); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [4, 5, 6, 255]); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [7, 8, 9, 255]); @@ -180,7 +180,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void white: '#161718' }; await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await ctx.value.proxy.write(`\x1b[8;40m█\x1b[8;41m█\x1b[8;42m█\x1b[8;43m█\x1b[8;44m█\x1b[8;45m█\x1b[8;46m█\x1b[8;47m█`); + await ctx.value.proxy.write(`\x1b[8;40m■\x1b[8;41m■\x1b[8;42m■\x1b[8;43m■\x1b[8;44m■\x1b[8;45m■\x1b[8;46m■\x1b[8;47m■`); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [1, 2, 3, 255]); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [4, 5, 6, 255]); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [7, 8, 9, 255]); @@ -203,7 +203,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void brightWhite: '#161718' }; await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await ctx.value.proxy.write(`\x1b[90m█\x1b[91m█\x1b[92m█\x1b[93m█\x1b[94m█\x1b[95m█\x1b[96m█\x1b[97m█`); + await ctx.value.proxy.write(`\x1b[90m■\x1b[91m■\x1b[92m■\x1b[93m■\x1b[94m■\x1b[95m■\x1b[96m■\x1b[97m■`); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [1, 2, 3, 255]); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [4, 5, 6, 255]); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [7, 8, 9, 255]); @@ -241,7 +241,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void let data = ''; for (let y = 0; y < 240 / 16; y++) { for (let x = 0; x < 16; x++) { - data += `\x1b[38;5;${16 + y * 16 + x}m█\x1b[0m`; + data += `\x1b[38;5;${16 + y * 16 + x}m■\x1b[0m`; } data += '\r\n'; } @@ -301,7 +301,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void let data = ''; for (let y = 0; y < 240 / 16; y++) { for (let x = 0; x < 16; x++) { - data += `\x1b[7;48;5;${16 + y * 16 + x}m█\x1b[0m`; + data += `\x1b[7;48;5;${16 + y * 16 + x}m■\x1b[0m`; } data += '\r\n'; } @@ -337,7 +337,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void let data = ''; for (let y = 0; y < 240 / 16; y++) { for (let x = 0; x < 16; x++) { - data += `\x1b[8;48;5;${16 + y * 16 + x}m█\x1b[0m`; + data += `\x1b[8;48;5;${16 + y * 16 + x}m■\x1b[0m`; } data += '\r\n'; } @@ -357,7 +357,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void let data = ''; for (let y = 0; y < 240 / 16; y++) { for (let x = 0; x < 16; x++) { - data += `\x1b[2;38;5;${16 + y * 16 + x}m█\x1b[0m`; + data += `\x1b[2;38;5;${16 + y * 16 + x}m■\x1b[0m`; } data += '\r\n'; } @@ -407,7 +407,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void for (let y = 0; y < 16; y++) { for (let x = 0; x < 16; x++) { const i = y * 16 + x; - data += `\x1b[38;2;${i};0;0m█\x1b[0m`; + data += `\x1b[38;2;${i};0;0m■\x1b[0m`; } data += '\r\n'; } @@ -443,7 +443,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void for (let y = 0; y < 16; y++) { for (let x = 0; x < 16; x++) { const i = y * 16 + x; - data += `\x1b[38;2;0;${i};0m█\x1b[0m`; + data += `\x1b[38;2;0;${i};0m■\x1b[0m`; } data += '\r\n'; } @@ -479,7 +479,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void for (let y = 0; y < 16; y++) { for (let x = 0; x < 16; x++) { const i = y * 16 + x; - data += `\x1b[38;2;0;0;${i}m█\x1b[0m`; + data += `\x1b[38;2;0;0;${i}m■\x1b[0m`; } data += '\r\n'; } @@ -515,7 +515,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void for (let y = 0; y < 16; y++) { for (let x = 0; x < 16; x++) { const i = y * 16 + x; - data += `\x1b[38;2;${i};${i};${i}m█\x1b[0m`; + data += `\x1b[38;2;${i};${i};${i}m■\x1b[0m`; } data += '\r\n'; } @@ -569,7 +569,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void for (let y = 0; y < 16; y++) { for (let x = 0; x < 16; x++) { const i = y * 16 + x; - data += `\x1b[7;48;2;${i};0;0m█\x1b[0m`; + data += `\x1b[7;48;2;${i};0;0m■\x1b[0m`; } data += '\r\n'; } @@ -605,7 +605,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void for (let y = 0; y < 16; y++) { for (let x = 0; x < 16; x++) { const i = y * 16 + x; - data += `\x1b[7;48;2;0;${i};0m█\x1b[0m`; + data += `\x1b[7;48;2;0;${i};0m■\x1b[0m`; } data += '\r\n'; } @@ -641,7 +641,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void for (let y = 0; y < 16; y++) { for (let x = 0; x < 16; x++) { const i = y * 16 + x; - data += `\x1b[7;48;2;0;0;${i}m█\x1b[0m`; + data += `\x1b[7;48;2;0;0;${i}m■\x1b[0m`; } data += '\r\n'; } @@ -677,7 +677,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void for (let y = 0; y < 16; y++) { for (let x = 0; x < 16; x++) { const i = y * 16 + x; - data += `\x1b[7;48;2;${i};${i};${i}m█\x1b[0m`; + data += `\x1b[7;48;2;${i};${i};${i}m■\x1b[0m`; } data += '\r\n'; } @@ -712,7 +712,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void for (let y = 0; y < 16; y++) { for (let x = 0; x < 16; x++) { const i = y * 16 + x; - data += `\x1b[8;48;2;${i};${i};${i}m█\x1b[0m`; + data += `\x1b[8;48;2;${i};${i};${i}m■\x1b[0m`; } data += '\r\n'; } @@ -749,7 +749,6 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void window.term.options.theme = ${JSON.stringify(theme)}; window.term.options.minimumContrastRatio = 1; `); - // Block elements ignore minimum contrast ratio so a different char is used here await ctx.value.proxy.write( `\x1b[30m■\x1b[31m■\x1b[32m■\x1b[33m■\x1b[34m■\x1b[35m■\x1b[36m■\x1b[37m■\r\n` + `\x1b[90m■\x1b[91m■\x1b[92m■\x1b[93m■\x1b[94m■\x1b[95m■\x1b[96m■\x1b[97m■` @@ -819,7 +818,6 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void window.term.options.theme = ${JSON.stringify(theme)}; window.term.options.minimumContrastRatio = 1; `); - // Block elements ignore minimum contrast ratio so a different char is used here await ctx.value.proxy.write( `\x1b[30m■\x1b[31m■\x1b[32m■\x1b[33m■\x1b[34m■\x1b[35m■\x1b[36m■\x1b[37m■\r\n` + `\x1b[90m■\x1b[91m■\x1b[92m■\x1b[93m■\x1b[94m■\x1b[95m■\x1b[96m■\x1b[97m■` @@ -893,7 +891,6 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void window.term.options.theme = ${JSON.stringify(theme)}; window.term.options.minimumContrastRatio = 1; `); - // Block elements ignore minimum contrast ratio so a different char is used here await ctx.value.proxy.write( '\x1b[2m' + `\x1b[30m■\x1b[31m■\x1b[32m■\x1b[33m■\x1b[34m■\x1b[35m■\x1b[36m■\x1b[37m■\r\n` + @@ -948,7 +945,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void selectionBackground: '#0000FF' }; await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await ctx.value.proxy.write( ` █\x1b[7m█\x1b[0m`); + await ctx.value.proxy.write( ` ■\x1b[7m■\x1b[0m`); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 255, 0, 255]); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [255, 0, 0, 255]); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [0, 255, 0, 255]); @@ -995,7 +992,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void background: '#ff000080' }; await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - const data = `\x1b[7m█\x1b[0m`; + const data = `\x1b[7m■\x1b[0m`; await ctx.value.proxy.write( data); // Inverse background should be opaque await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 0, 0, 255]); @@ -1008,7 +1005,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void selectionForeground: '#ff0000' }; await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - const data = `\x1b[7m█\x1b[0m`; + const data = `\x1b[7m■\x1b[0m`; await ctx.value.proxy.write( data); await ctx.value.page.evaluate(`window.term.selectAll()`); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 0, 0, 255]); @@ -1025,7 +1022,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void backgroundColor: '#0000ff' }); `); - const data = `█`; + const data = `■`; await ctx.value.proxy.write( data); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 0, 0, 255]); }); @@ -1038,7 +1035,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void backgroundColor: '#0000ff' }); `); - const data = `\x1b[7m█\x1b[0m`; + const data = `\x1b[7m■\x1b[0m`; await ctx.value.proxy.write( data); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 0, 0, 255]); }); @@ -1051,9 +1048,9 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void foregroundColor: '#ff0000' }); `); - const data = `\x1b[7m█ \x1b[0m`; + const data = `\x1b[7m■ \x1b[0m`; await ctx.value.proxy.write( data); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 0, 0, 255]); // inverse foreground of '█' should be decoration fg override + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 0, 0, 255]); // inverse foreground of '■' should be decoration fg override await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [255, 255, 255, 255]); // inverse background of ' ' should be default foreground }); test('backgroundColor', async () => { @@ -1083,7 +1080,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 255, 255]); }); (ctx.skipCanvasExceptions ? test.skip : test)('backgroundColor should ignore inverse (only bg on decoration)', async () => { - const data = `\x1b[7m█ \x1b[0m`; + const data = `\x1b[7m■ \x1b[0m`; await ctx.value.proxy.write( data); await ctx.value.page.evaluate(` const marker = window.term.registerMarker(-window.term.buffer.active.cursorY); @@ -1093,7 +1090,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void backgroundColor: '#0000ff' }); `); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]); // inverse foreground of '█' should be default + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]); // inverse foreground of '■' should be default await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [0, 0, 255, 255]); // inverse background of ' ' should be decoration bg override }); }); From 681ba4887fe40e39d2cc288ebcf6e6158f09a6eb Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 9 Sep 2023 07:11:32 -0700 Subject: [PATCH 85/99] Fix viewport error during term dispose Fixes #4775 --- src/browser/Viewport.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index c6e8732f..a8e1a498 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -119,10 +119,10 @@ export class Viewport extends Disposable implements IViewport { private _innerRefresh(): void { if (this._charSizeService.height > 0) { - this._currentRowHeight = this._renderService.dimensions.device.cell.height / this._coreBrowserService.dpr; - this._currentDeviceCellHeight = this._renderService.dimensions.device.cell.height; + this._currentRowHeight = this._renderDimensions.device.cell.height / this._coreBrowserService.dpr; + this._currentDeviceCellHeight = this._renderDimensions.device.cell.height; this._lastRecordedViewportHeight = this._viewportElement.offsetHeight; - const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._renderService.dimensions.css.canvas.height); + const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._renderDimensions.css.canvas.height); if (this._lastRecordedBufferHeight !== newBufferHeight) { this._lastRecordedBufferHeight = newBufferHeight; this._scrollArea.style.height = this._lastRecordedBufferHeight + 'px'; From 1792eaf05fe32c855231edd9e8986433206bbe43 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 9 Sep 2023 07:45:31 -0700 Subject: [PATCH 86/99] Fix MCR test on webkit, add pollForApproximate Fixes #4769 --- test/playwright/SharedRendererTests.ts | 72 ++++++++++++-------------- test/playwright/TestUtils.ts | 67 +++++++++++++++++++----- 2 files changed, 89 insertions(+), 50 deletions(-) diff --git a/test/playwright/SharedRendererTests.ts b/test/playwright/SharedRendererTests.ts index e19ff146..889bfc40 100644 --- a/test/playwright/SharedRendererTests.ts +++ b/test/playwright/SharedRendererTests.ts @@ -6,7 +6,7 @@ import { IImage32, decodePng } from '@lunapaint/png-codec'; import { LocatorScreenshotOptions, test } from '@playwright/test'; import { ITheme } from 'xterm'; -import { ITestContext, MaybeAsync, pollFor } from './TestUtils'; +import { ITestContext, MaybeAsync, pollFor, pollForApproximate } from './TestUtils'; export interface ISharedRendererTestContext { value: ITestContext; @@ -863,11 +863,6 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void }); test('should enforce half the contrast for dim cells', async () => { - // TODO: This test fails on webkit - if (ctx.value.browser.browserType().name() === 'webkit') { - test.skip(); - return; - } const theme: ITheme = { background: '#ffffff', black: '#2e3436', @@ -897,43 +892,44 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void `\x1b[90m■\x1b[91m■\x1b[92m■\x1b[93m■\x1b[94m■\x1b[95m■\x1b[96m■\x1b[97m■` ); // Validate before minimumContrastRatio is applied - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [Math.floor((255 + 0x2e) / 2), Math.floor((255 + 0x34) / 2), Math.floor((255 + 0x36) / 2), 255]); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [Math.floor((255 + 0xcc) / 2), Math.floor((255 + 0x00) / 2), Math.floor((255 + 0x00) / 2), 255]); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [Math.floor((255 + 0x4e) / 2), Math.floor((255 + 0x9a) / 2), Math.floor((255 + 0x06) / 2), 255]); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 4, 1), [Math.floor((255 + 0xc4) / 2), Math.floor((255 + 0xa0) / 2), Math.floor((255 + 0x00) / 2), 255]); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 5, 1), [Math.floor((255 + 0x34) / 2), Math.floor((255 + 0x65) / 2), Math.floor((255 + 0xa4) / 2), 255]); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 6, 1), [Math.floor((255 + 0x75) / 2), Math.floor((255 + 0x50) / 2), Math.floor((255 + 0x7b) / 2), 255]); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 7, 1), [Math.floor((255 + 0x06) / 2), Math.floor((255 + 0x98) / 2), Math.floor((255 + 0x9a) / 2), 255]); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 8, 1), [Math.floor((255 + 0xd3) / 2), Math.floor((255 + 0xd7) / 2), Math.floor((255 + 0xcf) / 2), 255]); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 2), [Math.floor((255 + 0x55) / 2), Math.floor((255 + 0x57) / 2), Math.floor((255 + 0x53) / 2), 255]); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 2), [Math.floor((255 + 0xef) / 2), Math.floor((255 + 0x29) / 2), Math.floor((255 + 0x29) / 2), 255]); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 2), [Math.floor((255 + 0x8a) / 2), Math.floor((255 + 0xe2) / 2), Math.floor((255 + 0x34) / 2), 255]); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 4, 2), [Math.floor((255 + 0xfc) / 2), Math.floor((255 + 0xe9) / 2), Math.floor((255 + 0x4f) / 2), 255]); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 5, 2), [Math.floor((255 + 0x72) / 2), Math.floor((255 + 0x9f) / 2), Math.floor((255 + 0xcf) / 2), 255]); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 6, 2), [Math.floor((255 + 0xad) / 2), Math.floor((255 + 0x7f) / 2), Math.floor((255 + 0xa8) / 2), 255]); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 7, 2), [Math.floor((255 + 0x34) / 2), Math.floor((255 + 0xe2) / 2), Math.floor((255 + 0xe2) / 2), 255]); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 8, 2), [Math.floor((255 + 0xee) / 2), Math.floor((255 + 0xee) / 2), Math.floor((255 + 0xec) / 2), 255]); + const marginOfError = 1; + await pollForApproximate(ctx.value.page, marginOfError, () => getCellColor(ctx.value, 1, 1), [Math.floor((255 + 0x2e) / 2), Math.floor((255 + 0x34) / 2), Math.floor((255 + 0x36) / 2), 255]); + await pollForApproximate(ctx.value.page, marginOfError, () => getCellColor(ctx.value, 2, 1), [Math.floor((255 + 0xcc) / 2), Math.floor((255 + 0x00) / 2), Math.floor((255 + 0x00) / 2), 255]); + await pollForApproximate(ctx.value.page, marginOfError, () => getCellColor(ctx.value, 3, 1), [Math.floor((255 + 0x4e) / 2), Math.floor((255 + 0x9a) / 2), Math.floor((255 + 0x06) / 2), 255]); + await pollForApproximate(ctx.value.page, marginOfError, () => getCellColor(ctx.value, 4, 1), [Math.floor((255 + 0xc4) / 2), Math.floor((255 + 0xa0) / 2), Math.floor((255 + 0x00) / 2), 255]); + await pollForApproximate(ctx.value.page, marginOfError, () => getCellColor(ctx.value, 5, 1), [Math.floor((255 + 0x34) / 2), Math.floor((255 + 0x65) / 2), Math.floor((255 + 0xa4) / 2), 255]); + await pollForApproximate(ctx.value.page, marginOfError, () => getCellColor(ctx.value, 6, 1), [Math.floor((255 + 0x75) / 2), Math.floor((255 + 0x50) / 2), Math.floor((255 + 0x7b) / 2), 255]); + await pollForApproximate(ctx.value.page, marginOfError, () => getCellColor(ctx.value, 7, 1), [Math.floor((255 + 0x06) / 2), Math.floor((255 + 0x98) / 2), Math.floor((255 + 0x9a) / 2), 255]); + await pollForApproximate(ctx.value.page, marginOfError, () => getCellColor(ctx.value, 8, 1), [Math.floor((255 + 0xd3) / 2), Math.floor((255 + 0xd7) / 2), Math.floor((255 + 0xcf) / 2), 255]); + await pollForApproximate(ctx.value.page, marginOfError, () => getCellColor(ctx.value, 1, 2), [Math.floor((255 + 0x55) / 2), Math.floor((255 + 0x57) / 2), Math.floor((255 + 0x53) / 2), 255]); + await pollForApproximate(ctx.value.page, marginOfError, () => getCellColor(ctx.value, 2, 2), [Math.floor((255 + 0xef) / 2), Math.floor((255 + 0x29) / 2), Math.floor((255 + 0x29) / 2), 255]); + await pollForApproximate(ctx.value.page, marginOfError, () => getCellColor(ctx.value, 3, 2), [Math.floor((255 + 0x8a) / 2), Math.floor((255 + 0xe2) / 2), Math.floor((255 + 0x34) / 2), 255]); + await pollForApproximate(ctx.value.page, marginOfError, () => getCellColor(ctx.value, 4, 2), [Math.floor((255 + 0xfc) / 2), Math.floor((255 + 0xe9) / 2), Math.floor((255 + 0x4f) / 2), 255]); + await pollForApproximate(ctx.value.page, marginOfError, () => getCellColor(ctx.value, 5, 2), [Math.floor((255 + 0x72) / 2), Math.floor((255 + 0x9f) / 2), Math.floor((255 + 0xcf) / 2), 255]); + await pollForApproximate(ctx.value.page, marginOfError, () => getCellColor(ctx.value, 6, 2), [Math.floor((255 + 0xad) / 2), Math.floor((255 + 0x7f) / 2), Math.floor((255 + 0xa8) / 2), 255]); + await pollForApproximate(ctx.value.page, marginOfError, () => getCellColor(ctx.value, 7, 2), [Math.floor((255 + 0x34) / 2), Math.floor((255 + 0xe2) / 2), Math.floor((255 + 0xe2) / 2), 255]); + await pollForApproximate(ctx.value.page, marginOfError, () => getCellColor(ctx.value, 8, 2), [Math.floor((255 + 0xee) / 2), Math.floor((255 + 0xee) / 2), Math.floor((255 + 0xec) / 2), 255]); // Setting and check for minimum contrast values, note that these are not // exact to the contrast ratio, if the increase luminance algorithm // changes then these will probably fail await ctx.value.page.evaluate(`window.term.options.minimumContrastRatio = 10;`); frameDetails = undefined; - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [150, 153, 154, 255]); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [229, 127, 127, 255]); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [63, 124, 4, 255]); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 4, 1), [127, 104, 0, 255]); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 5, 1), [153, 178, 209, 255]); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 6, 1), [186, 167, 189, 255]); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 7, 1), [4, 122, 124, 255]); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 8, 1), [110, 112, 108, 255]); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 2), [170, 171, 169, 255]); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 2), [215, 36, 36, 255]); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 2), [72, 117, 25, 255]); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 4, 2), [117, 109, 36, 255]); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 5, 2), [72, 103, 135, 255]); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 6, 2), [125, 91, 121, 255]); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 7, 2), [25, 117, 117, 255]); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 8, 2), [111, 111, 110, 255]); + await pollForApproximate(ctx.value.page, marginOfError, () => getCellColor(ctx.value, 1, 1), [150, 153, 154, 255]); + await pollForApproximate(ctx.value.page, marginOfError, () => getCellColor(ctx.value, 2, 1), [229, 127, 127, 255]); + await pollForApproximate(ctx.value.page, marginOfError, () => getCellColor(ctx.value, 3, 1), [63, 124, 4, 255]); + await pollForApproximate(ctx.value.page, marginOfError, () => getCellColor(ctx.value, 4, 1), [127, 104, 0, 255]); + await pollForApproximate(ctx.value.page, marginOfError, () => getCellColor(ctx.value, 5, 1), [153, 178, 209, 255]); + await pollForApproximate(ctx.value.page, marginOfError, () => getCellColor(ctx.value, 6, 1), [186, 167, 189, 255]); + await pollForApproximate(ctx.value.page, marginOfError, () => getCellColor(ctx.value, 7, 1), [4, 122, 124, 255]); + await pollForApproximate(ctx.value.page, marginOfError, () => getCellColor(ctx.value, 8, 1), [110, 112, 108, 255]); + await pollForApproximate(ctx.value.page, marginOfError, () => getCellColor(ctx.value, 1, 2), [170, 171, 169, 255]); + await pollForApproximate(ctx.value.page, marginOfError, () => getCellColor(ctx.value, 2, 2), [215, 36, 36, 255]); + await pollForApproximate(ctx.value.page, marginOfError, () => getCellColor(ctx.value, 3, 2), [72, 117, 25, 255]); + await pollForApproximate(ctx.value.page, marginOfError, () => getCellColor(ctx.value, 4, 2), [117, 109, 36, 255]); + await pollForApproximate(ctx.value.page, marginOfError, () => getCellColor(ctx.value, 5, 2), [72, 103, 135, 255]); + await pollForApproximate(ctx.value.page, marginOfError, () => getCellColor(ctx.value, 6, 2), [125, 91, 121, 255]); + await pollForApproximate(ctx.value.page, marginOfError, () => getCellColor(ctx.value, 7, 2), [25, 117, 117, 255]); + await pollForApproximate(ctx.value.page, marginOfError, () => getCellColor(ctx.value, 8, 2), [111, 111, 110, 255]); }); }); diff --git a/test/playwright/TestUtils.ts b/test/playwright/TestUtils.ts index 02a08907..18a1f1bb 100644 --- a/test/playwright/TestUtils.ts +++ b/test/playwright/TestUtils.ts @@ -374,8 +374,17 @@ export async function openTerminal(ctx: ITestContext, options: ITerminalOptions export type MaybeAsync = Promise | T; -export async function pollFor(page: playwright.Page, evalOrFn: string | (() => MaybeAsync), val: T, preFn?: () => Promise, maxDuration?: number, stack?: string): Promise { - stack ??= new Error().stack; +interface IPollForOptions { + equalityFn?: (a: T, b: T) => boolean; + maxDuration?: number; + stack?: string; +} + +export async function pollFor(page: playwright.Page, evalOrFn: string | (() => MaybeAsync), val: T, preFn?: () => Promise, options?: IPollForOptions): Promise { + if (!options) { + options = {}; + } + options.stack ??= new Error().stack; if (preFn) { await preFn(); } @@ -385,32 +394,66 @@ export async function pollFor(page: playwright.Page, evalOrFn: string | (() = console.log('pollFor\n actual: ', JSON.stringify(result), ' expected: ', JSON.stringify(val)); } - let equalityCheck = true; - try { - deepStrictEqual(result, val); - } catch (e) { - equalityCheck = false; + let equalityCheck: boolean; + if (options.equalityFn) { + equalityCheck = options.equalityFn(result as T, val); + } else { + equalityCheck = true; + try { + deepStrictEqual(result, val); + } catch (e) { + equalityCheck = false; + } } if (!equalityCheck) { - if (maxDuration === undefined) { - maxDuration = 2000; + if (options.maxDuration === undefined) { + options.maxDuration = 2000; } - if (maxDuration <= 0) { + if (options.maxDuration <= 0) { deepStrictEqual(result, val, ([ `pollFor max duration exceeded.`, (`Last comparison: ` + `${typeof result === 'object' ? JSON.stringify(result) : result} (actual) !== ` + `${typeof val === 'object' ? JSON.stringify(val) : val} (expected)`), - `Stack: ${stack}` + `Stack: ${options.stack}` ].join('\n'))); } return new Promise(r => { - setTimeout(() => r(pollFor(page, evalOrFn, val, preFn, maxDuration! - 10, stack)), 10); + setTimeout(() => r(pollFor(page, evalOrFn, val, preFn, { + ...options, + maxDuration: options!.maxDuration! - 10, + stack: options!.stack + })), 10); }); } } +export async function pollForApproximate(page: playwright.Page, marginOfError: number, evalOrFn: string | (() => MaybeAsync), val: T, preFn?: () => Promise, maxDuration?: number, stack?: string): Promise { + await pollFor(page, evalOrFn, val, preFn, { + maxDuration, + stack, + equalityFn: (a, b) => { + if (a === b) { + return true; + } + if (Array.isArray(a) && Array.isArray(b) && a.length === b.length) { + let success = true; + for (let i = 0 ; i < a.length; i++) { + if (Math.abs(a[i] - b[i]) > marginOfError) { + success = false; + break; + } + } + if (success) { + return true; + } + } + return false; + } + }); +} + export async function writeSync(page: playwright.Page, data: string): Promise { await page.evaluate(` window.ready = false; From 851c2e39af2303a241b3f5c88b45bd19e95f3ada Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 9 Sep 2023 08:37:55 -0700 Subject: [PATCH 87/99] Fix webgl not respecting MCR for inverse Part of #4759 --- src/browser/renderer/shared/TextureAtlas.ts | 2 +- test/playwright/SharedRendererTests.ts | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/browser/renderer/shared/TextureAtlas.ts b/src/browser/renderer/shared/TextureAtlas.ts index dd059574..56c77b68 100644 --- a/src/browser/renderer/shared/TextureAtlas.ts +++ b/src/browser/renderer/shared/TextureAtlas.ts @@ -309,7 +309,7 @@ export class TextureAtlas implements ITextureAtlas { } private _getForegroundColor(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, dim: boolean, bold: boolean, excludeFromContrastRatioDemands: boolean): IColor { - const minimumContrastColor = this._getMinimumContrastColor(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, false, bold, dim, excludeFromContrastRatioDemands); + const minimumContrastColor = this._getMinimumContrastColor(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold, dim, excludeFromContrastRatioDemands); if (minimumContrastColor) { return minimumContrastColor; } diff --git a/test/playwright/SharedRendererTests.ts b/test/playwright/SharedRendererTests.ts index 889bfc40..d746ca4c 100644 --- a/test/playwright/SharedRendererTests.ts +++ b/test/playwright/SharedRendererTests.ts @@ -1102,6 +1102,19 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [0, 0, 0, 255]); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [0, 0, 0, 255]); }); + test.only('#4759: minimum contrast ratio should be respected on inverse text', async () => { + const theme: ITheme = { + foreground: '#aaaaaa', + background: '#333333' + }; + await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.value.proxy.write(`\x1b[7m■■`); + // Validate before minimumContrastRatio is applied + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0x33, 0x33, 0x33, 255]); + await ctx.value.page.evaluate(`window.term.options.minimumContrastRatio = 10;`); + frameDetails = undefined; + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]); + }); }); } From 1aac37e14f9741061fed2dc06d7449fe2bc2a6ff Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 9 Sep 2023 08:51:50 -0700 Subject: [PATCH 88/99] Improve regression tests for 4759 --- test/playwright/SharedRendererTests.ts | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/test/playwright/SharedRendererTests.ts b/test/playwright/SharedRendererTests.ts index d746ca4c..849da743 100644 --- a/test/playwright/SharedRendererTests.ts +++ b/test/playwright/SharedRendererTests.ts @@ -1102,7 +1102,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [0, 0, 0, 255]); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [0, 0, 0, 255]); }); - test.only('#4759: minimum contrast ratio should be respected on inverse text', async () => { + test('#4759: minimum contrast ratio should be respected on inverse text', async () => { const theme: ITheme = { foreground: '#aaaaaa', background: '#333333' @@ -1110,10 +1110,32 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); await ctx.value.proxy.write(`\x1b[7m■■`); // Validate before minimumContrastRatio is applied - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0x33, 0x33, 0x33, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [51, 51, 51, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [51, 51, 51, 255]); await ctx.value.page.evaluate(`window.term.options.minimumContrastRatio = 10;`); frameDetails = undefined; await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [0, 0, 0, 255]); + }); + test('#4759: minimum contrast ratio should be respected on selected inverse text', async () => { + const theme: ITheme = { + foreground: '#777777', + background: '#555555', + selectionBackground: '#666666' // Slightly more contrast needed for selection + }; + await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.value.proxy.write(`\x1b[7m■■`); + await ctx.value.proxy.selectAll(); + // Validate before minimumContrastRatio is applied + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [85, 85, 85, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [85, 85, 85, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [102, 102, 102, 255]); + await ctx.value.page.evaluate(`window.term.options.minimumContrastRatio = 10;`); + await ctx.value.proxy.selectAll(); + frameDetails = undefined; + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 255, 255, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [255, 255, 255, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [102, 102, 102, 255]); }); }); } From b0868967cbe2348a30826e1ed2f06abae59d81ce Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 9 Sep 2023 08:52:43 -0700 Subject: [PATCH 89/99] Fix inverse MCR with selection for dom renderer Fixes #4759 --- src/browser/renderer/dom/DomRendererRowFactory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index 2d21a647..614b2301 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -419,7 +419,7 @@ export class DomRendererRowFactory { break; case Attributes.CM_DEFAULT: default: - if (!this._applyMinimumContrast(charElement, resolvedBg, colors.foreground, cell, bgOverride, undefined)) { + if (!this._applyMinimumContrast(charElement, resolvedBg, colors.foreground, cell, bgOverride, fgOverride)) { if (isInverse) { classes.push(`xterm-fg-${INVERTED_DEFAULT_COLOR}`); } From fe4528c9bed849476e4ac57e56f432e3840f1ff3 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 9 Sep 2023 09:00:57 -0700 Subject: [PATCH 90/99] Add one regression test to canvas exceptions --- test/playwright/SharedRendererTests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/playwright/SharedRendererTests.ts b/test/playwright/SharedRendererTests.ts index 849da743..a19a7537 100644 --- a/test/playwright/SharedRendererTests.ts +++ b/test/playwright/SharedRendererTests.ts @@ -1117,7 +1117,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [0, 0, 0, 255]); }); - test('#4759: minimum contrast ratio should be respected on selected inverse text', async () => { + (ctx.skipCanvasExceptions ? test.skip : test)('#4759: minimum contrast ratio should be respected on selected inverse text', async () => { const theme: ITheme = { foreground: '#777777', background: '#555555', From b50d21434e5d356dc400663f65eaf7ab4f05211e Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 10 Sep 2023 07:23:54 -0700 Subject: [PATCH 91/99] Add edge debug target --- .vscode/launch.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 5e481d5c..0eb8eb4e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -43,7 +43,14 @@ { "type": "chrome", "request": "launch", - "name": "Demo Client", + "name": "Demo Client (Chrome)", + "url": "http://127.0.0.1:3000", + "webRoot": "${workspaceFolder}/" + }, + { + "type": "msedge", + "request": "launch", + "name": "Demo Client (Edge)", "url": "http://127.0.0.1:3000", "webRoot": "${workspaceFolder}/" }, From b55d6994518b50af2044a9848a1faed9e441e0e5 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 10 Sep 2023 07:41:31 -0700 Subject: [PATCH 92/99] Fix regression in force mouse selection Shift+left click is means to force terminal selection and not passthrough any mouse events, even if a mouse mode is enabled. The change this is reverting from #4583 caused this feature to break by always installing a global listener on Terminal.element so the SelectionService never got a chance to cancel is. Fixes #4781 --- src/browser/Terminal.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index a092e1bc..131774f1 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -729,10 +729,8 @@ export class Terminal extends CoreTerminal implements ITerminal { if (!(events & CoreMouseEventType.UP)) { this._document!.removeEventListener('mouseup', requestedEvents.mouseup!); - el.removeEventListener('mouseup', requestedEvents.mouseup!); requestedEvents.mouseup = null; } else if (!requestedEvents.mouseup) { - el.addEventListener('mouseup', eventListeners.mouseup); requestedEvents.mouseup = eventListeners.mouseup; } From 1d57ac5c63a82eda1001443f02e01c9102864996 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 10 Sep 2023 07:46:00 -0700 Subject: [PATCH 93/99] Fix server debugging This broke somewhere along the way, the changes update from the legacy attach method of --inspect-brk to stopOnEntry and also updates for the introduction of .nvmrc. --- .vscode/launch.json | 8 +++----- package.json | 1 - 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 0eb8eb4e..5dbd01ce 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -59,11 +59,9 @@ "request": "launch", "name": "Demo Server", "runtimeExecutable": "npm", - "runtimeArgs": [ - "run", - "start-debug" - ], - "port": 9229, + "runtimeArgs": ["start"], + "stopOnEntry": true, + "runtimeVersion": "16", "serverReadyAction": { "action": "openExternally", "pattern": "App listening to (http://.*?:[0-9]+)" diff --git a/package.json b/package.json index 534f44ec..23661d73 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,6 @@ "start": "node demo/start", "start-server-only": "node demo/start-server-only", "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/", "test": "npm run test-unit", From cc43fe5191a27c23b1d707a6298438ca58804066 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 10 Sep 2023 08:19:46 -0700 Subject: [PATCH 94/99] Fix possible exception when disposing terminal Fixes #4757 --- src/browser/services/RenderService.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index 0f18a233..e6d259c2 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -203,11 +203,14 @@ export class RenderService extends Disposable implements IRenderService { public setRenderer(renderer: IRenderer): void { this._renderer.value = renderer; - this._renderer.value.onRequestRedraw(e => this.refreshRows(e.start, e.end, true)); + // If the value was not set, the terminal is being disposed so ignore it + if (this._renderer.value) { + this._renderer.value.onRequestRedraw(e => this.refreshRows(e.start, e.end, true)); - // Force a refresh - this._needsSelectionRefresh = true; - this._fullRefresh(); + // Force a refresh + this._needsSelectionRefresh = true; + this._fullRefresh(); + } } public addRefreshCallback(callback: FrameRequestCallback): number { From 3b15fb6211cbf0eaab3fc5bda3131b8a0ee838b3 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 10 Sep 2023 08:27:18 -0700 Subject: [PATCH 95/99] Fix block cursor rendering in DOM renderer Fixes #4773 --- src/browser/renderer/dom/DomRenderer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index 98213e08..5f276954 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -205,8 +205,8 @@ export class DomRenderer extends Disposable implements IRenderer { ` animation: blink_block` + `_` + this._terminalClass + ` 1s step-end infinite;` + `}` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS} {` + - ` background-color: ${colors.cursor.css};` + - ` color: ${colors.cursorAccent.css};` + + ` background-color: ${colors.cursor.css} !important;` + + ` color: ${colors.cursorAccent.css} !important;` + `}` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_OUTLINE_CLASS} {` + ` outline: 1px solid ${colors.cursor.css};` + From c609f5cca038e51f2c516b115a6928a8f0b402cd Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 10 Sep 2023 08:30:43 -0700 Subject: [PATCH 96/99] Add regression test for block cursor focus/selection --- test/playwright/SharedRendererTests.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/playwright/SharedRendererTests.ts b/test/playwright/SharedRendererTests.ts index a19a7537..e1e56bc9 100644 --- a/test/playwright/SharedRendererTests.ts +++ b/test/playwright/SharedRendererTests.ts @@ -1137,6 +1137,16 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [255, 255, 255, 255]); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [102, 102, 102, 255]); }); + test('#4773: block cursor should render when the cell is selected', async () => { + const theme: ITheme = { + cursor: '#0000FF', + selectionBackground: '#FF0000' + }; + await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.value.proxy.focus(); + await ctx.value.proxy.selectAll(); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 255, 255]); + }); }); } From 6b1c1e07516efd229b57a9cfd63c1cf53521e5a4 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 10 Sep 2023 08:56:04 -0700 Subject: [PATCH 97/99] Force 50% workers in CI to reduce flakiness --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b33c2545..fb003eb7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -259,12 +259,12 @@ jobs: ls -R - name: Build demo run: yarn build-demo - - name: Integration tests (core) - run: yarn test-playwright-${{ matrix.browser }} --forbid-only --workers 4 --suite=core + - name: Integration tests (core) # Tests use 50% workers to reduce flakiness + run: yarn test-playwright-${{ matrix.browser }} --workers=50% --forbid-only --suite=core - name: Integration tests (xterm-addon-canvas) - run: yarn test-playwright-${{ matrix.browser }} --forbid-only --workers 4 --suite=xterm-addon-canvas + run: yarn test-playwright-${{ matrix.browser }} --workers=50% --forbid-only --suite=xterm-addon-canvas - name: Integration tests (xterm-addon-webgl) - run: yarn test-playwright-${{ matrix.browser }} --forbid-only --workers 4 --suite=xterm-addon-webgl + run: yarn test-playwright-${{ matrix.browser }} --workers=50% --forbid-only --suite=xterm-addon-webgl test-api: needs: build From b91de01a4d6816dd565a0da0a63924c258eb7952 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 10 Sep 2023 08:56:19 -0700 Subject: [PATCH 98/99] Fix initial args not being passed through to pw --- bin/test_playwright.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/test_playwright.js b/bin/test_playwright.js index d6c9d3dd..02c2a763 100644 --- a/bin/test_playwright.js +++ b/bin/test_playwright.js @@ -35,7 +35,7 @@ function npmBinScript(script) { async function run() { for (const config of configs) { const command = npmBinScript('playwright'); - const args = ['test', '-c', config.path, ...argv.slice(2)]; + const args = ['test', '-c', config.path, ...argv]; console.log(`Running suite \x1b[1;34m${config.name}...\x1b[0m`); console.log(`\n\x1b[32m${command}\x1b[0m`, args); const run = cp.spawnSync(command, args, { From efcca3fb3e8ffdd091771ee4e68270cd3b5a5c3b Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 10 Sep 2023 08:56:36 -0700 Subject: [PATCH 99/99] Run pw tests faster on high core machines --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 23661d73..4915cf3e 100644 --- a/package.json +++ b/package.json @@ -38,11 +38,11 @@ "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": "node ./bin/test_playwright.js --workers 4", - "test-playwright-chromium": "node ./bin/test_playwright.js --workers 4 \"--project=Chrome Stable\"", - "test-playwright-firefox": "node ./bin/test_playwright.js --workers 4 \"--project=Firefox Stable\"", - "test-playwright-webkit": "node ./bin/test_playwright.js --workers 4 \"--project=WebKit\"", - "test-playwright-debug": "node ./bin/test_playwright.js --headed --workers 1 --timeout 30000", + "test-playwright": "node ./bin/test_playwright.js --workers=75%", + "test-playwright-chromium": "node ./bin/test_playwright.js --workers=75% \"--project=Chrome Stable\"", + "test-playwright-firefox": "node ./bin/test_playwright.js --workers=75% \"--project=Firefox Stable\"", + "test-playwright-webkit": "node ./bin/test_playwright.js --workers=75% \"--project=WebKit\"", + "test-playwright-debug": "node ./bin/test_playwright.js --workers=1 --headed --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",