Merge branch 'master' into add-balena-real-world-use

This commit is contained in:
Daniel Imms
2024-07-03 08:12:10 -07:00
committed by GitHub
59 changed files with 2122 additions and 1849 deletions
+17 -15
View File
@@ -11,7 +11,6 @@
"src/browser/tsconfig.json",
"src/common/tsconfig.json",
"src/headless/tsconfig.json",
"test/api/tsconfig.json",
"test/benchmark/tsconfig.json",
"test/playwright/tsconfig.json",
"addons/addon-attach/src/tsconfig.json",
@@ -52,11 +51,25 @@
"**/*.js"
],
"plugins": [
"@stylistic/ts",
"@typescript-eslint",
"jsdoc"
],
"rules": {
"no-extra-semi": "error",
"@stylistic/ts/indent": [
"warn",
2
],
"@stylistic/ts/semi": [
"warn",
"always"
],
"@stylistic/ts/quotes": [
"warn",
"single",
{ "allowTemplateLiterals": true }
],
"@typescript-eslint/array-type": [
"warn",
{
@@ -81,10 +94,6 @@
}
}
],
"@typescript-eslint/indent": [
"warn",
2
],
"@typescript-eslint/member-delimiter-style": [
"warn",
{
@@ -144,15 +153,7 @@
"@typescript-eslint/no-useless-constructor": "warn",
"@typescript-eslint/prefer-namespace-keyword": "warn",
"@typescript-eslint/type-annotation-spacing": "warn",
"@typescript-eslint/quotes": [
"warn",
"single",
{ "allowTemplateLiterals": true }
],
"@typescript-eslint/semi": [
"warn",
"always"
],
"comma-dangle": [
"warn",
{
@@ -193,6 +194,7 @@
}
],
"no-eval": "warn",
"no-extra-semi": "error",
"no-irregular-whitespace": "warn",
"no-restricted-imports": [
"warn",
+17 -14
View File
@@ -6,11 +6,25 @@
},
"parser": "@typescript-eslint/parser",
"plugins": [
"@stylistic/ts",
"@typescript-eslint",
"jsdoc"
],
"rules": {
"no-extra-semi": "error",
"@stylistic/ts/indent": [
"warn",
2
],
"@stylistic/ts/semi": [
"warn",
"always"
],
"@stylistic/ts/quotes": [
"warn",
"single",
{ "allowTemplateLiterals": true }
],
"@typescript-eslint/array-type": [
"warn",
{
@@ -24,10 +38,6 @@
"allowExpressions": true
}
],
"@typescript-eslint/indent": [
"warn",
2
],
"@typescript-eslint/member-delimiter-style": [
"warn",
{
@@ -48,15 +58,7 @@
],
"@typescript-eslint/prefer-namespace-keyword": "warn",
"@typescript-eslint/type-annotation-spacing": "warn",
"@typescript-eslint/quotes": [
"warn",
"single",
{ "allowTemplateLiterals": true }
],
"@typescript-eslint/semi": [
"warn",
"always"
],
"comma-dangle": [
"warn",
{
@@ -86,6 +88,7 @@
"ignorePattern": "^ *(?<ps_description>\\* Ps=)"
}
],
"no-extra-semi": "error",
"no-irregular-whitespace": "warn",
"no-trailing-spaces": "warn",
"object-curly-spacing": [
+5 -83
View File
@@ -112,7 +112,7 @@ jobs:
./node_modules/.bin/nyc report --reporter=cobertura
exit $EXIT_CODE
test-unit-parallel:
test-unit:
timeout-minutes: 20
strategy:
matrix:
@@ -150,48 +150,7 @@ jobs:
- name: Unit tests
run: yarn test-unit --forbid-only
test-api-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
browser: [chromium, firefox]
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
shell: bash
run: |
if [ "$RUNNER_OS" == "Windows" ]; then
pwsh -Command "7z x compressed-build.zip -aoa -o${{ github.workspace }}"
else
unzip -o compressed-build.zip
fi
ls -R
- name: Integration tests (${{ matrix.browser }})
run: yarn test-api-${{ matrix.browser }} --headless --forbid-only
test-playwright-parallel:
test-integration:
timeout-minutes: 20
strategy:
matrix:
@@ -232,48 +191,11 @@ jobs:
- name: Build demo
run: yarn build-demo
- name: Integration tests (core) # Tests use 50% workers to reduce flakiness
run: yarn test-playwright-${{ matrix.browser }} --workers=50% --forbid-only --suite=core
run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=core
- name: Integration tests (addon-canvas)
run: yarn test-playwright-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-canvas
run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-canvas
- name: Integration tests (addon-webgl)
run: yarn test-playwright-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-webgl
test-api:
needs: build
timeout-minutes: 20
strategy:
matrix:
node-version: [18] # just one as integration tests are about testing in browser
runs-on: [windows] # macos is flaky
browser: [chromium, firefox]
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
- uses: actions/download-artifact@v3
with:
name: build-artifacts
- name: Unzip artifacts
shell: bash
run: |
if [ "$RUNNER_OS" == "Windows" ]; then
pwsh -Command "7z x compressed-build.zip -aoa -o${{ github.workspace }}"
else
unzip -o compressed-build.zip
fi
ls -R
- name: Integration tests (${{ matrix.browser }})
run: yarn test-api-${{ matrix.browser }} --headless --forbid-only
run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-webgl
release-dry-run:
needs: build
+2
View File
@@ -224,6 +224,8 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**Helix Editor Playground**](https://github.com/tomgroenwoldt/helix-editor-playground): Online playground for the terminal based helix editor.
- [**Coder**](https://github.com/coder/coder): Self-Hosted Remote Development Environments
- [**Wave Terminal**](https://waveterm.dev): An open-source, ai-native, terminal built for seamless workflows.
- [**eva**](https://github.com/info24/eva): Eva is a web application for SSH remote login, developed in Go.
- [**OpenSFTP**](https://opensftp.com): Super beautiful SSH and SFTP integrated workspace client.
- [**balena**](https://www.balena.io/): Balena is a full-stack solution for developing, deploying, updating, and troubleshooting IoT Edge devices. We use xterm.js to manage & debug devices on [balenaCloud](https://www.balena.io/cloud).
- [And much more...](https://github.com/xtermjs/xterm.js/network/dependents?package_id=UGFja2FnZS0xNjYzMjc4OQ%3D%3D)
@@ -1,50 +0,0 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import WebSocket = require('ws');
import { openTerminal, pollFor, launchBrowser } from '../../../out-test/api/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('AttachAddon', () => {
before(async function(): Promise<any> {
browser = await launchBrowser();
page = await (await browser.newContext()).newPage();
await page.setViewportSize({ width, height });
});
after(async () => {
await browser.close();
});
beforeEach(async () => await page.goto(APP));
it('string', async function(): Promise<any> {
await openTerminal(page);
const port = 8080;
const server = new WebSocket.Server({ port });
server.on('connection', socket => socket.send('foo'));
await page.evaluate(`window.term.loadAddon(new window.AttachAddon(new WebSocket('ws://localhost:${port}')))`);
await pollFor(page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foo');
server.close();
});
it('utf8', async function(): Promise<any> {
await openTerminal(page);
const port = 8080;
const server = new WebSocket.Server({ port });
const data = new Uint8Array([102, 111, 111]);
server.on('connection', socket => socket.send(data));
await page.evaluate(`window.term.loadAddon(new window.AttachAddon(new WebSocket('ws://localhost:${port}')))`);
await pollFor(page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foo');
server.close();
});
});
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import WebSocket = require('ws');
import test from '@playwright/test';
import { ITestContext, createTestContext, openTerminal, pollFor, timeout } from '../../../out-test/playwright/TestUtils';
let ctx: ITestContext;
test.beforeAll(async ({ browser }) => {
ctx = await createTestContext(browser);
await openTerminal(ctx);
});
test.afterAll(async () => await ctx.page.close());
test.describe('Search Tests', () => {
test.beforeEach(async () => {
await ctx.proxy.reset();
});
test('string', async function(): Promise<any> {
const port = 8080;
const server = new WebSocket.Server({ port });
server.on('connection', socket => socket.send('foo'));
await ctx.page.evaluate(`window.term.loadAddon(new window.AttachAddon(new WebSocket('ws://localhost:${port}')))`);
await pollFor(ctx.page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foo');
server.close();
});
test('utf8', async function(): Promise<any> {
const port = 8080;
const server = new WebSocket.Server({ port });
const data = new Uint8Array([102, 111, 111]);
server.on('connection', socket => socket.send(data));
await ctx.page.evaluate(`window.term.loadAddon(new window.AttachAddon(new WebSocket('ws://localhost:${port}')))`);
await pollFor(ctx.page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foo');
server.close();
});
});
@@ -0,0 +1,35 @@
import { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
testDir: '.',
timeout: 10000,
projects: [
{
name: 'ChromeStable',
use: {
browserName: 'chromium',
channel: 'chrome'
}
},
{
name: 'FirefoxStable',
use: {
browserName: 'firefox'
}
},
{
name: 'WebKit',
use: {
browserName: 'webkit'
}
}
],
reporter: 'list',
webServer: {
command: 'npm run start-server-only',
port: 3000,
timeout: 120000,
reuseExistingServer: !process.env.CI
}
};
export default config;
+20 -4
View File
@@ -1,23 +1,39 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2021",
"target": "ESNext",
"lib": [
"es2015"
"es2021",
],
"rootDir": ".",
"outDir": "../out-test",
"sourceMap": true,
"removeComments": true,
"baseUrl": ".",
"paths": {
"common/*": [
"../../../src/common/*"
],
"browser/*": [
"../../../src/browser/*"
]
},
"strict": true,
"types": [
"../../../node_modules/@types/mocha",
"../../../node_modules/@types/node",
"../../../out-test/api/TestUtils"
"../../../out-test/playwright/TestUtils"
]
},
"include": [
"./**/*",
"../../../typings/xterm.d.ts"
],
"references": [
{
"path": "../../../src/common"
},
{
"path": "../../../src/browser"
}
]
}
@@ -5,14 +5,14 @@ const config: PlaywrightTestConfig = {
timeout: 10000,
projects: [
{
name: 'Chrome Stable',
name: 'ChromeStable',
use: {
browserName: 'chromium',
channel: 'chrome'
}
},
{
name: 'Firefox Stable',
name: 'FirefoxStable',
use: {
browserName: 'firefox'
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2015",
"target": "ESNext",
"lib": [
"es6",
],
@@ -1,88 +0,0 @@
/**
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { openTerminal, launchBrowser, writeSync, getBrowserType } from '../../../out-test/api/TestUtils';
import { Browser, BrowserContext, Page } from '@playwright/test';
import { beforeEach } from 'mocha';
const APP = 'http://127.0.0.1:3001/test';
let browser: Browser;
let context: BrowserContext;
let page: Page;
const width = 800;
const height = 600;
describe('ClipboardAddon', () => {
before(async function (): Promise<any> {
browser = await launchBrowser({
// Enable clipboard access in firefox, mainly for readText
firefoxUserPrefs: {
// eslint-disable-next-line @typescript-eslint/naming-convention
'dom.events.testing.asyncClipboard': true,
// eslint-disable-next-line @typescript-eslint/naming-convention
'dom.events.asyncClipboard.readText': true
}
});
context = await browser.newContext();
if (getBrowserType().name() !== 'webkit') {
// Enable clipboard access in chromium without user gesture
context.grantPermissions(['clipboard-read', 'clipboard-write']);
}
page = await context.newPage();
await page.setViewportSize({ width, height });
await page.goto(APP);
await openTerminal(page);
await page.evaluate(`
window.clipboardAddon = new ClipboardAddon();
window.term.loadAddon(window.clipboardAddon);
`);
});
after(() => {
browser.close();
});
beforeEach(async () => {
await page.evaluate(`window.term.reset()`);
});
const testDataEncoded = 'aGVsbG8gd29ybGQ=';
const testDataDecoded = 'hello world';
describe('write data', async function (): Promise<any> {
it('simple string', async () => {
await writeSync(page, `\x1b]52;c;${testDataEncoded}\x07`);
assert.deepEqual(await page.evaluate(() => window.navigator.clipboard.readText()), testDataDecoded);
});
it('invalid base64 string', async () => {
await writeSync(page, `\x1b]52;c;${testDataEncoded}invalid\x07`);
assert.deepEqual(await page.evaluate(() => window.navigator.clipboard.readText()), '');
});
it('empty string', async () => {
await writeSync(page, `\x1b]52;c;${testDataEncoded}\x07`);
await writeSync(page, `\x1b]52;c;\x07`);
assert.deepEqual(await page.evaluate(() => window.navigator.clipboard.readText()), '');
});
});
describe('read data', async function (): Promise<any> {
it('simple string', async () => {
await page.evaluate(`
window.data = [];
window.term.onData(e => data.push(e));
`);
await page.evaluate(() => window.navigator.clipboard.writeText('hello world'));
await writeSync(page, `\x1b]52;c;?\x07`);
assert.deepEqual(await page.evaluate('window.data'), [`\x1b]52;c;${testDataEncoded}\x07`]);
});
it('clear clipboard', async () => {
await writeSync(page, `\x1b]52;c;!\x07`);
await writeSync(page, `\x1b]52;c;?\x07`);
assert.deepEqual(await page.evaluate(() => window.navigator.clipboard.readText()), '');
});
});
});
@@ -0,0 +1,78 @@
/**
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
* @license MIT
*/
import test from '@playwright/test';
import { deepEqual, ok, strictEqual } from 'assert';
import { ITestContext, createTestContext, launchBrowser, openTerminal, timeout } from '../../../out-test/playwright/TestUtils';
let ctx: ITestContext;
test.beforeAll(async ({ browser }, testInfo) => {
ctx = await createTestContext(browser);
await openTerminal(ctx);
});
test.afterAll(async () => {
await ctx.page.close();
});
test.describe('ClipboardAddon', () => {
test.beforeEach(async ({}, testInfo) => {
// DEBT: This test doesn't work since the migration to @playwright/test
if (ctx.browser.browserType().name() !== 'chromium') {
testInfo.skip();
return;
}
if (ctx.browser.browserType().name() === 'chromium') {
// Enable clipboard access in chromium without user gesture
await ctx.page.context().grantPermissions(['clipboard-read', 'clipboard-write']);
}
await ctx.page.evaluate(`
window.term.reset()
window.clipboard?.dispose();
window.clipboard = new ClipboardAddon();
window.term.loadAddon(window.clipboard);
`);
});
test.beforeEach(async () => {
await ctx.proxy.reset();
});
const testDataEncoded = 'aGVsbG8gd29ybGQ=';
const testDataDecoded = 'hello world';
test.describe('write data', async function (): Promise<any> {
test('simple string', async () => {
await ctx.proxy.write(`\x1b]52;c;${testDataEncoded}\x07`);
deepEqual(await ctx.page.evaluate(() => window.navigator.clipboard.readText()), testDataDecoded);
});
test('invalid base64 string', async () => {
await ctx.proxy.write(`\x1b]52;c;${testDataEncoded}invalid\x07`);
deepEqual(await ctx.page.evaluate(() => window.navigator.clipboard.readText()), '');
});
test('empty string', async () => {
await ctx.proxy.write(`\x1b]52;c;${testDataEncoded}\x07`);
await ctx.proxy.write(`\x1b]52;c;\x07`);
deepEqual(await ctx.page.evaluate(() => window.navigator.clipboard.readText()), '');
});
});
test.describe('read data', async function (): Promise<any> {
test('simple string', async () => {
await ctx.page.evaluate(`
window.data = [];
window.term.onData(e => data.push(e));
`);
await ctx.page.evaluate(() => window.navigator.clipboard.writeText('hello world'));
await ctx.proxy.write(`\x1b]52;c;?\x07`);
deepEqual(await ctx.page.evaluate('window.data'), [`\x1b]52;c;${testDataEncoded}\x07`]);
});
test('clear clipboard', async () => {
await ctx.proxy.write(`\x1b]52;c;!\x07`);
await ctx.proxy.write(`\x1b]52;c;?\x07`);
deepEqual(await ctx.page.evaluate(() => window.navigator.clipboard.readText()), '');
});
});
});
@@ -0,0 +1,35 @@
import { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
testDir: '.',
timeout: 10000,
projects: [
{
name: 'ChromeStable',
use: {
browserName: 'chromium',
channel: 'chrome'
}
},
{
name: 'FirefoxStable',
use: {
browserName: 'firefox'
}
},
{
name: 'WebKit',
use: {
browserName: 'webkit'
}
}
],
reporter: 'list',
webServer: {
command: 'npm run start-server-only',
port: 3000,
timeout: 120000,
reuseExistingServer: !process.env.CI
}
};
export default config;
+20 -4
View File
@@ -1,23 +1,39 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2021",
"target": "ESNext",
"lib": [
"es2015"
"es2021",
],
"rootDir": ".",
"outDir": "../out-test",
"sourceMap": true,
"removeComments": true,
"baseUrl": ".",
"paths": {
"common/*": [
"../../../src/common/*"
],
"browser/*": [
"../../../src/browser/*"
]
},
"strict": true,
"types": [
"../../../node_modules/@types/mocha",
"../../../node_modules/@types/node",
"../../../out-test/api/TestUtils"
"../../../out-test/playwright/TestUtils"
]
},
"include": [
"./**/*",
"../../../typings/xterm.d.ts"
],
"references": [
{
"path": "../../../src/common"
},
{
"path": "../../../src/browser"
}
]
}
+3 -1
View File
@@ -13,8 +13,10 @@ declare module '@xterm/addon-clipboard' {
export class ClipboardAddon implements ITerminalAddon {
/**
* Creates a new clipboard addon.
* @param base64 An optional base64 encoder/decoder.
* @param provider An optional clipboard provider.
*/
constructor(provider?: IClipboardProvider);
constructor(base64?: IBase64, provider?: IClipboardProvider);
/**
* Activates the addon
-135
View File
@@ -1,135 +0,0 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { openTerminal, launchBrowser, timeout } from '../../../out-test/api/TestUtils';
import { Browser, Page } from '@playwright/test';
const APP = 'http://127.0.0.1:3001/test';
let browser: Browser;
let page: Page;
const width = 1024;
const height = 768;
describe('FitAddon', () => {
before(async function(): Promise<any> {
browser = await launchBrowser();
page = await (await browser.newContext()).newPage();
await page.setViewportSize({ width, height });
await page.goto(APP);
});
beforeEach(async function(): Promise<any> {
await page.evaluate(`document.querySelector('#terminal-container').style.display=''`);
await openTerminal(page);
});
after(async () => {
await browser.close();
});
afterEach(async function(): Promise<any> {
await page.evaluate(`window.term.dispose()`);
});
it('no terminal', async function(): Promise<any> {
await page.evaluate(`window.fit = new FitAddon();`);
assert.equal(await page.evaluate(`window.fit.proposeDimensions()`), undefined);
});
describe('proposeDimensions', () => {
afterEach(() => unloadFit());
it('default', async function(): Promise<any> {
await loadFit();
const dimensions: {cols: number, rows: number} = await page.evaluate(`window.fit.proposeDimensions()`);
assert.isAbove(dimensions.cols, 85);
assert.isBelow(dimensions.cols, 88);
assert.isAbove(dimensions.rows, 24);
assert.isBelow(dimensions.rows, 29);
});
it('width', async function(): Promise<any> {
await loadFit(1008);
const dimensions: {cols: number, rows: number} = await page.evaluate(`window.fit.proposeDimensions()`);
assert.isAbove(dimensions.cols, 108);
assert.isBelow(dimensions.cols, 111);
assert.isAbove(dimensions.rows, 24);
assert.isBelow(dimensions.rows, 29);
});
it('small', async function(): Promise<any> {
await loadFit(1, 1);
assert.deepEqual(await page.evaluate(`window.fit.proposeDimensions()`), {
cols: 2,
rows: 1
});
});
it('hidden', async function(): Promise<any> {
await page.evaluate(`window.term.dispose()`);
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 loadFit();
const dimensions: { cols: number, rows: number } | undefined = await page.evaluate(`window.fit.proposeDimensions()`);
// The value of dims will be undefined if the char measure strategy falls back to the DOM
// method, so only assert if it's not undefined.
if (dimensions) {
assert.isAbove(dimensions.cols, 85);
assert.isBelow(dimensions.cols, 88);
assert.isAbove(dimensions.rows, 24);
assert.isBelow(dimensions.rows, 29);
}
});
});
describe('fit', () => {
afterEach(() => unloadFit());
it('default', async function(): Promise<any> {
await loadFit();
await page.evaluate(`window.fit.fit()`);
const cols: number = await page.evaluate(`window.term.cols`);
const rows: number = await page.evaluate(`window.term.rows`);
assert.isAbove(cols, 85);
assert.isBelow(cols, 88);
assert.isAbove(rows, 24);
assert.isBelow(rows, 29);
});
it('width', async function(): Promise<any> {
await loadFit(1008);
await page.evaluate(`window.fit.fit()`);
const cols: number = await page.evaluate(`window.term.cols`);
const rows: number = await page.evaluate(`window.term.rows`);
assert.isAbove(cols, 108);
assert.isBelow(cols, 111);
assert.isAbove(rows, 24);
assert.isBelow(rows, 29);
});
it('small', async function(): Promise<any> {
await loadFit(1, 1);
await page.evaluate(`window.fit.fit()`);
assert.equal(await page.evaluate(`window.term.cols`), 2);
assert.equal(await page.evaluate(`window.term.rows`), 1);
});
});
});
async function loadFit(width: number = 800, height: number = 450): Promise<void> {
await page.evaluate(`
window.fit = new FitAddon();
window.term.loadAddon(window.fit);
document.querySelector('#terminal-container').style.width='${width}px';
document.querySelector('#terminal-container').style.height='${height}px';
`);
}
async function unloadFit(): Promise<void> {
await page.evaluate(`window.fit.dispose();`);
}
+138
View File
@@ -0,0 +1,138 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import test from '@playwright/test';
import { deepEqual, ok, strictEqual } from 'assert';
import { ITestContext, createTestContext, openTerminal, timeout } from '../../../out-test/playwright/TestUtils';
let ctx: ITestContext;
test.beforeAll(async ({ browser }) => {
ctx = await createTestContext(browser);
await openTerminal(ctx);
});
test.afterAll(async () => await ctx.page.close());
test.describe('FitAddon', () => {
test.beforeEach(async function(): Promise<any> {
await ctx.page.evaluate(`
window.term.reset()
window.fit?.dispose();
window.fit = new FitAddon();
window.term.loadAddon(window.fit);
`);
});
// test.beforeEach(async function(): Promise<any> {
// await ctx.page.evaluate(`document.querySelector('#terminal-container').style.display=''`);
// await openTerminal(page);
// });
// after(async () => {
// await browser.close();
// });
// afterEach(async function(): Promise<any> {
// await ctx.proxy.dispose();
// });
test('no terminal', async function(): Promise<any> {
await ctx.page.evaluate(`window.fit = new FitAddon();`);
strictEqual(await ctx.page.evaluate(`window.fit.proposeDimensions()`), undefined);
});
test.describe('proposeDimensions', () => {
// test.afterEach(() => unloadFit());
test('default', async function(): Promise<any> {
await loadFit();
const dimensions: {cols: number, rows: number} = await ctx.page.evaluate(`window.fit.proposeDimensions()`);
ok(dimensions.cols > 85);
ok(dimensions.cols < 88);
ok(dimensions.rows > 24);
ok(dimensions.rows < 29);
});
test('width', async function(): Promise<any> {
await loadFit(1008);
const dimensions: {cols: number, rows: number} = await ctx.page.evaluate(`window.fit.proposeDimensions()`);
ok(dimensions.cols > 108);
ok(dimensions.cols < 111);
ok(dimensions.rows > 24);
ok(dimensions.rows < 29);
});
test('small', async function(): Promise<any> {
await loadFit(1, 1);
deepEqual(await ctx.page.evaluate(`window.fit.proposeDimensions()`), {
cols: 2,
rows: 1
});
});
test('hidden', async function(): Promise<any> {
await ctx.proxy.dispose();
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 loadFit();
const dimensions: { cols: number, rows: number } | undefined = await ctx.page.evaluate(`window.fit.proposeDimensions()`);
// The value of dims will be undefined if the char measure strategy falls back to the DOM
// method, so only assert if it's not undefined.
if (dimensions) {
ok(dimensions.cols > 85);
ok(dimensions.cols < 88);
ok(dimensions.rows > 24);
ok(dimensions.rows < 29);
}
await ctx.page.evaluate(`document.querySelector('#terminal-container').style.display='block'`);
});
});
test.describe('fit', () => {
test.afterEach(() => unloadFit());
test('default', async function(): Promise<any> {
await loadFit();
await ctx.page.evaluate(`window.fit.fit()`);
const cols: number = await ctx.proxy.cols;
const rows: number = await ctx.proxy.rows;
ok(cols > 85);
ok(cols < 88);
ok(rows > 24);
ok(rows < 29);
});
test('width', async function(): Promise<any> {
await loadFit(1008);
await ctx.page.evaluate(`window.fit.fit()`);
const cols: number = await ctx.proxy.cols;
const rows: number = await ctx.proxy.rows;
ok(cols > 108);
ok(cols < 111);
ok(rows > 24);
ok(rows < 29);
});
test('small', async function(): Promise<any> {
await loadFit(1, 1);
await ctx.page.evaluate(`window.fit.fit()`);
strictEqual(await ctx.proxy.cols, 2);
strictEqual(await ctx.proxy.rows, 1);
});
});
});
async function loadFit(width: number = 800, height: number = 450): Promise<void> {
await ctx.page.evaluate(`
window.fit = new FitAddon();
window.term.loadAddon(window.fit);
document.querySelector('#terminal-container').style.width='${width}px';
document.querySelector('#terminal-container').style.height='${height}px';
`);
}
async function unloadFit(): Promise<void> {
await ctx.page.evaluate(`window.fit.dispose();`);
}
@@ -0,0 +1,35 @@
import { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
testDir: '.',
timeout: 10000,
projects: [
{
name: 'ChromeStable',
use: {
browserName: 'chromium',
channel: 'chrome'
}
},
{
name: 'FirefoxStable',
use: {
browserName: 'firefox'
}
},
{
name: 'WebKit',
use: {
browserName: 'webkit'
}
}
],
reporter: 'list',
webServer: {
command: 'npm run start-server-only',
port: 3000,
timeout: 120000,
reuseExistingServer: !process.env.CI
}
};
export default config;
+20 -4
View File
@@ -1,23 +1,39 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2021",
"target": "ESNext",
"lib": [
"es2015"
"es2021",
],
"rootDir": ".",
"outDir": "../out-test",
"sourceMap": true,
"removeComments": true,
"baseUrl": ".",
"paths": {
"common/*": [
"../../../src/common/*"
],
"browser/*": [
"../../../src/browser/*"
]
},
"strict": true,
"types": [
"../../../node_modules/@types/mocha",
"../../../node_modules/@types/node",
"../../../out-test/api/TestUtils"
"../../../out-test/playwright/TestUtils"
]
},
"include": [
"./**/*",
"../../../typings/xterm.d.ts"
],
"references": [
{
"path": "../../../src/common"
},
{
"path": "../../../src/browser"
}
]
}
-321
View File
@@ -1,321 +0,0 @@
/**
* Copyright (c) 2020 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { openTerminal, launchBrowser, pollFor } from '../../../out-test/api/TestUtils';
import { Browser, Page } from '@playwright/test';
import { IImageAddonOptions } from '../src/Types';
import { FINALIZER, introducer, sixelEncode } from 'sixel';
import { readFileSync } from 'fs';
const APP = 'http://127.0.0.1:3001/test';
let browser: Browser;
let page: Page;
const width = 800;
const height = 600;
// eslint-disable-next-line
declare const ImageAddon: {
new(options?: Partial<IImageAddonOptions>): any;
};
interface ITestData {
width: number;
height: number;
bytes: Uint8Array;
palette: number[];
sixel: string;
}
interface IDimensions {
cellWidth: number;
cellHeight: number;
width: number;
height: number;
}
// image: 640 x 80, 512 color
const TESTDATA: ITestData = (() => {
const data8 = readFileSync('./addons/addon-image/fixture/palette.blob');
const data32 = new Uint32Array(data8.buffer);
const palette = new Set<number>();
for (let i = 0; i < data32.length; ++i) palette.add(data32[i]);
const sixel = sixelEncode(data8, 640, 80, [...palette]);
return {
width: 640,
height: 80,
bytes: data8,
palette: [...palette],
sixel
};
})();
const SIXEL_SEQ_0 = introducer(0) + TESTDATA.sixel + FINALIZER;
// const SIXEL_SEQ_1 = introducer(1) + TESTDATA.sixel + FINALIZER;
// const SIXEL_SEQ_2 = introducer(2) + TESTDATA.sixel + FINALIZER;
// NOTE: the data is loaded as string for easier transport through playwright
const TESTDATA_IIP: [string, [number, number]][] = [
[readFileSync('./addons/addon-image/fixture/iip/palette.iip', { encoding: 'utf-8' }), [640, 80]],
[readFileSync('./addons/addon-image/fixture/iip/spinfox.iip', { encoding: 'utf-8' }), [148, 148]],
[readFileSync('./addons/addon-image/fixture/iip/w3c_gif.iip', { encoding: 'utf-8' }), [72, 48]],
[readFileSync('./addons/addon-image/fixture/iip/w3c_jpg.iip', { encoding: 'utf-8' }), [72, 48]],
[readFileSync('./addons/addon-image/fixture/iip/w3c_png.iip', { encoding: 'utf-8' }), [72, 48]]
];
describe('ImageAddon', () => {
before(async () => {
browser = await launchBrowser();
page = await (await browser.newContext()).newPage();
await page.setViewportSize({ width, height });
});
after(async () => {
await browser.close();
});
beforeEach(async () => {
await page.goto(APP);
await openTerminal(page);
await page.evaluate(opts => {
(window as any).imageAddon = new ImageAddon(opts.opts);
(window as any).term.loadAddon((window as any).imageAddon);
}, { opts: { sixelPaletteLimit: 512 } });
});
it('test for private accessors', async () => {
// terminal privates
const accessors = [
'_core',
'_core._renderService',
'_core._inputHandler',
'_core._inputHandler._parser',
'_core._inputHandler._curAttrData',
'_core._inputHandler._dirtyRowTracker',
'_core._themeService.colors',
'_core._coreBrowserService'
];
for (const prop of accessors) {
assert.equal(
await page.evaluate('(() => { const v = window.term.' + prop + '; return v !== undefined && v !== null; })()'),
true, `problem at ${prop}`
);
}
// bufferline privates
assert.equal(await page.evaluate('window.term._core.buffer.lines.get(0)._data instanceof Uint32Array'), true);
assert.equal(await page.evaluate('window.term._core.buffer.lines.get(0)._extendedAttrs instanceof Object'), true);
// inputhandler privates
assert.equal(await page.evaluate('window.term._core._inputHandler._curAttrData.constructor.name'), 'AttributeData');
assert.equal(await page.evaluate('window.term._core._inputHandler._parser.constructor.name'), 'EscapeSequenceParser');
});
describe('ctor options', () => {
it('empty settings should load defaults', async () => {
const DEFAULT_OPTIONS: IImageAddonOptions = {
enableSizeReports: true,
pixelLimit: 16777216,
sixelSupport: true,
sixelScrolling: true,
sixelPaletteLimit: 512, // set to 512 to get example image working
sixelSizeLimit: 25000000,
storageLimit: 128,
showPlaceholder: true,
iipSupport: true,
iipSizeLimit: 20000000
};
assert.deepEqual(await page.evaluate(`window.imageAddon._opts`), DEFAULT_OPTIONS);
});
it('custom settings should overload defaults', async () => {
const customSettings: IImageAddonOptions = {
enableSizeReports: false,
pixelLimit: 5,
sixelSupport: false,
sixelScrolling: false,
sixelPaletteLimit: 1024,
sixelSizeLimit: 1000,
storageLimit: 10,
showPlaceholder: false,
iipSupport: false,
iipSizeLimit: 1000
};
await page.evaluate(opts => {
(window as any).imageAddonCustom = new ImageAddon(opts.opts);
(window as any).term.loadAddon((window as any).imageAddonCustom);
}, { opts: customSettings });
assert.deepEqual(await page.evaluate(`window.imageAddonCustom._opts`), customSettings);
});
});
describe('scrolling & cursor modes', () => {
it('testdata default (scrolling with VT240 cursor pos)', async () => {
const dim = await getDimensions();
await writeToTerminal(SIXEL_SEQ_0);
assert.deepEqual(await getCursor(), [0, Math.floor(TESTDATA.height/dim.cellHeight)]);
// moved to right by 10 cells
await writeToTerminal('#'.repeat(10) + SIXEL_SEQ_0);
assert.deepEqual(await getCursor(), [10, Math.floor(TESTDATA.height/dim.cellHeight) * 2]);
});
it('write testdata noScrolling', async () => {
await writeToTerminal('\x1b[?80h' + SIXEL_SEQ_0);
assert.deepEqual(await getCursor(), [0, 0]);
// second draw does not change anything
await writeToTerminal(SIXEL_SEQ_0);
assert.deepEqual(await getCursor(), [0, 0]);
});
it('testdata cursor always at VT240 pos', async () => {
const dim = await getDimensions();
// offset 0
await writeToTerminal(SIXEL_SEQ_0);
assert.deepEqual(await getCursor(), [0, Math.floor(TESTDATA.height/dim.cellHeight)]);
// moved to right by 10 cells
await writeToTerminal('#'.repeat(10) + SIXEL_SEQ_0);
assert.deepEqual(await getCursor(), [10, Math.floor(TESTDATA.height/dim.cellHeight) * 2]);
// moved by 30 cells (+10 prev)
await writeToTerminal('#'.repeat(30) + SIXEL_SEQ_0);
assert.deepEqual(await getCursor(), [10 + 30, Math.floor(TESTDATA.height/dim.cellHeight) * 3]);
});
});
describe('image lifecycle & eviction', () => {
it('delete image once scrolled off', async () => {
await writeToTerminal(SIXEL_SEQ_0);
pollFor(page, 'window.imageAddon._storage._images.size', 1);
// scroll to scrollback + rows - 1
await page.evaluate(
scrollback => new Promise(res => (window as any).term.write('\n'.repeat(scrollback), res)),
(await getScrollbackPlusRows() - 1)
);
// wait here, as we have to make sure, that eviction did not yet occur
await new Promise(r => setTimeout(r, 100));
pollFor(page, 'window.imageAddon._storage._images.size', 1);
// scroll one further should delete the image
await page.evaluate(() => new Promise(res => (window as any).term.write('\n', res)));
pollFor(page, 'window.imageAddon._storage._images.size', 0);
});
it('get storageUsage', async () => {
assert.equal(await page.evaluate('imageAddon.storageUsage'), 0);
await writeToTerminal(SIXEL_SEQ_0);
assert.closeTo(await page.evaluate('imageAddon.storageUsage'), 640 * 80 * 4 / 1000000, 0.05);
});
it('get/set storageLimit', async () => {
assert.equal(await page.evaluate('imageAddon.storageLimit'), 128);
assert.equal(await page.evaluate('imageAddon.storageLimit = 1'), 1);
assert.equal(await page.evaluate('imageAddon.storageLimit'), 1);
});
it('remove images by storage limit pressure', async () => {
assert.equal(await page.evaluate('imageAddon.storageLimit = 1'), 1);
// never go beyond storage limit
await writeToTerminal(SIXEL_SEQ_0);
await writeToTerminal(SIXEL_SEQ_0);
await writeToTerminal(SIXEL_SEQ_0);
await writeToTerminal(SIXEL_SEQ_0);
await new Promise(r => setTimeout(r, 50));
const usage = await page.evaluate('imageAddon.storageUsage');
await writeToTerminal(SIXEL_SEQ_0);
await writeToTerminal(SIXEL_SEQ_0);
await writeToTerminal(SIXEL_SEQ_0);
await writeToTerminal(SIXEL_SEQ_0);
await new Promise(r => setTimeout(r, 50));
assert.equal(await page.evaluate('imageAddon.storageUsage'), usage);
assert.equal(usage as number < 1, true);
});
it('set storageLimit removes images synchronously', async () => {
await writeToTerminal(SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0);
const usage: number = await page.evaluate('imageAddon.storageUsage');
const newUsage: number = await page.evaluate('imageAddon.storageLimit = 0.5; imageAddon.storageUsage');
assert.equal(newUsage < usage, true);
assert.equal(newUsage < 0.5, true);
});
it('clear alternate images on buffer change', async () => {
assert.equal(await page.evaluate('imageAddon.storageUsage'), 0);
await writeToTerminal('\x1b[?1049h' + SIXEL_SEQ_0);
assert.closeTo(await page.evaluate('imageAddon.storageUsage'), 640 * 80 * 4 / 1000000, 0.05);
await writeToTerminal('\x1b[?1049l');
assert.equal(await page.evaluate('imageAddon.storageUsage'), 0);
});
it('evict tiles by in-place overwrites (only full overwrite tested)', async () => {
await new Promise(r => setTimeout(r, 50));
await writeToTerminal('\x1b[H' + SIXEL_SEQ_0 + '\x1b[100;100H');
let usage = await page.evaluate('imageAddon.storageUsage');
while (usage === 0) {
await new Promise(r => setTimeout(r, 50));
usage = await page.evaluate('imageAddon.storageUsage');
}
await writeToTerminal('\x1b[H' + SIXEL_SEQ_0 + '\x1b[100;100H');
await new Promise(r => setTimeout(r, 200)); // wait some time and re-check
assert.equal(await page.evaluate('imageAddon.storageUsage'), usage);
});
it('manual eviction on alternate buffer must not miss images', async () => {
await writeToTerminal('\x1b[?1049h');
await writeToTerminal(SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0);
await new Promise(r => setTimeout(r, 50));
const usage: number = await page.evaluate('imageAddon.storageUsage');
await writeToTerminal(SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0);
await writeToTerminal(SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0);
await new Promise(r => setTimeout(r, 50));
const newUsage: number = await page.evaluate('imageAddon.storageUsage');
assert.equal(newUsage, usage);
});
});
describe('IIP support - testimages', () => {
it('palette.png', async () => {
await writeToTerminal(TESTDATA_IIP[0][0]);
assert.deepEqual(await getOrigSize(1), TESTDATA_IIP[0][1]);
});
it('spinfox.png', async () => {
await writeToTerminal(TESTDATA_IIP[1][0]);
assert.deepEqual(await getOrigSize(1), TESTDATA_IIP[1][1]);
});
it('w3c gif', async () => {
await writeToTerminal(TESTDATA_IIP[2][0]);
assert.deepEqual(await getOrigSize(1), TESTDATA_IIP[2][1]);
});
it('w3c jpeg', async () => {
await writeToTerminal(TESTDATA_IIP[3][0]);
assert.deepEqual(await getOrigSize(1), TESTDATA_IIP[3][1]);
});
it('w3c png', async () => {
await writeToTerminal(TESTDATA_IIP[4][0]);
assert.deepEqual(await getOrigSize(1), TESTDATA_IIP[4][1]);
});
});
});
/**
* terminal access helpers.
*/
async function getDimensions(): Promise<IDimensions> {
const dimensions: any = await page.evaluate(`term._core._renderService.dimensions`);
return {
cellWidth: Math.round(dimensions.css.cell.width),
cellHeight: Math.round(dimensions.css.cell.height),
width: Math.round(dimensions.css.canvas.width),
height: Math.round(dimensions.css.canvas.height)
};
}
async function getCursor(): Promise<[number, number]> {
return page.evaluate('[window.term.buffer.active.cursorX, window.term.buffer.active.cursorY]');
}
async function getImageStorageLength(): Promise<number> {
return page.evaluate('window.imageAddon._storage._images.size');
}
async function getScrollbackPlusRows(): Promise<number> {
return page.evaluate('window.term.options.scrollback + window.term.rows');
}
async function writeToTerminal(d: string): Promise<any> {
return page.evaluate(data => new Promise(res => (window as any).term.write(data, res)), d);
}
async function getOrigSize(id: number): Promise<[number, number]> {
return page.evaluate<any>(`[
window.imageAddon._storage._images.get(${id}).orig.width,
window.imageAddon._storage._images.get(${id}).orig.height
]`);
}
+313
View File
@@ -0,0 +1,313 @@
/**
* Copyright (c) 2020 The xterm.js authors. All rights reserved.
* @license MIT
*/
import test from '@playwright/test';
import { readFileSync } from 'fs';
import { FINALIZER, introducer, sixelEncode } from 'sixel';
import { ITestContext, createTestContext, openTerminal, pollFor } from '../../../out-test/playwright/TestUtils';
import { IImageAddonOptions } from '../src/Types';
import { deepStrictEqual, ok, strictEqual } from 'assert';
// eslint-disable-next-line
declare const ImageAddon: {
new(options?: Partial<IImageAddonOptions>): any;
};
interface ITestData {
width: number;
height: number;
bytes: Uint8Array;
palette: number[];
sixel: string;
}
interface IDimensions {
cellWidth: number;
cellHeight: number;
width: number;
height: number;
}
// image: 640 x 80, 512 color
const TESTDATA: ITestData = (() => {
const data8 = readFileSync('./addons/addon-image/fixture/palette.blob');
const data32 = new Uint32Array(data8.buffer);
const palette = new Set<number>();
for (let i = 0; i < data32.length; ++i) palette.add(data32[i]);
const sixel = sixelEncode(data8, 640, 80, [...palette]);
return {
width: 640,
height: 80,
bytes: data8,
palette: [...palette],
sixel
};
})();
const SIXEL_SEQ_0 = introducer(0) + TESTDATA.sixel + FINALIZER;
// const SIXEL_SEQ_1 = introducer(1) + TESTDATA.sixel + FINALIZER;
// const SIXEL_SEQ_2 = introducer(2) + TESTDATA.sixel + FINALIZER;
// NOTE: the data is loaded as string for easier transport through playwright
const TESTDATA_IIP: [string, [number, number]][] = [
[readFileSync('./addons/addon-image/fixture/iip/palette.iip', { encoding: 'utf-8' }), [640, 80]],
[readFileSync('./addons/addon-image/fixture/iip/spinfox.iip', { encoding: 'utf-8' }), [148, 148]],
[readFileSync('./addons/addon-image/fixture/iip/w3c_gif.iip', { encoding: 'utf-8' }), [72, 48]],
[readFileSync('./addons/addon-image/fixture/iip/w3c_jpg.iip', { encoding: 'utf-8' }), [72, 48]],
[readFileSync('./addons/addon-image/fixture/iip/w3c_png.iip', { encoding: 'utf-8' }), [72, 48]]
];
let ctx: ITestContext;
test.beforeAll(async ({ browser }) => {
ctx = await createTestContext(browser);
await openTerminal(ctx);
});
test.afterAll(async () => await ctx.page.close());
test.describe('ImageAddon', () => {
test.beforeEach(async ({}, testInfo) => {
// DEBT: This test never worked on webkit
if (ctx.browser.browserType().name() === 'webkit') {
testInfo.skip();
return;
}
await ctx.page.evaluate(`
window.term.reset()
window.imageAddon?.dispose();
window.imageAddon = new ImageAddon({ sixelPaletteLimit: 512 });
window.term.loadAddon(window.imageAddon);
`);
});
test('test for private accessors', async () => {
// terminal privates
const accessors = [
'_core',
'_core._renderService',
'_core._inputHandler',
'_core._inputHandler._parser',
'_core._inputHandler._curAttrData',
'_core._inputHandler._dirtyRowTracker',
'_core._themeService.colors',
'_core._coreBrowserService'
];
for (const prop of accessors) {
strictEqual(
await ctx.page.evaluate('(() => { const v = window.term.' + prop + '; return v !== undefined && v !== null; })()'),
true, `problem at ${prop}`
);
}
// bufferline privates
strictEqual(await ctx.page.evaluate('window.term._core.buffer.lines.get(0)._data instanceof Uint32Array'), true);
strictEqual(await ctx.page.evaluate('window.term._core.buffer.lines.get(0)._extendedAttrs instanceof Object'), true);
// inputhandler privates
strictEqual(await ctx.page.evaluate('window.term._core._inputHandler._curAttrData.constructor.name'), 'AttributeData');
strictEqual(await ctx.page.evaluate('window.term._core._inputHandler._parser.constructor.name'), 'EscapeSequenceParser');
});
test.describe('ctor options', () => {
test('empty settings should load defaults', async () => {
const DEFAULT_OPTIONS: IImageAddonOptions = {
enableSizeReports: true,
pixelLimit: 16777216,
sixelSupport: true,
sixelScrolling: true,
sixelPaletteLimit: 512, // set to 512 to get example image working
sixelSizeLimit: 25000000,
storageLimit: 128,
showPlaceholder: true,
iipSupport: true,
iipSizeLimit: 20000000
};
deepStrictEqual(await ctx.page.evaluate(`window.imageAddon._opts`), DEFAULT_OPTIONS);
});
test('custom settings should overload defaults', async () => {
const customSettings: IImageAddonOptions = {
enableSizeReports: false,
pixelLimit: 5,
sixelSupport: false,
sixelScrolling: false,
sixelPaletteLimit: 1024,
sixelSizeLimit: 1000,
storageLimit: 10,
showPlaceholder: false,
iipSupport: false,
iipSizeLimit: 1000
};
await ctx.page.evaluate(opts => {
(window as any).imageAddonCustom = new ImageAddon(opts.opts);
(window as any).term.loadAddon((window as any).imageAddonCustom);
}, { opts: customSettings });
deepStrictEqual(await ctx.page.evaluate(`window.imageAddonCustom._opts`), customSettings);
});
});
test.describe('scrolling & cursor modes', () => {
test('testdata default (scrolling with VT240 cursor pos)', async () => {
const dim = await getDimensions();
await ctx.proxy.write(SIXEL_SEQ_0);
deepStrictEqual(await getCursor(), [0, Math.floor(TESTDATA.height/dim.cellHeight)]);
// moved to right by 10 cells
await ctx.proxy.write('#'.repeat(10) + SIXEL_SEQ_0);
deepStrictEqual(await getCursor(), [10, Math.floor(TESTDATA.height/dim.cellHeight) * 2]);
});
test('write testdata noScrolling', async () => {
await ctx.proxy.write('\x1b[?80h' + SIXEL_SEQ_0);
deepStrictEqual(await getCursor(), [0, 0]);
// second draw does not change anything
await ctx.proxy.write(SIXEL_SEQ_0);
deepStrictEqual(await getCursor(), [0, 0]);
});
test('testdata cursor always at VT240 pos', async () => {
const dim = await getDimensions();
// offset 0
await ctx.proxy.write(SIXEL_SEQ_0);
deepStrictEqual(await getCursor(), [0, Math.floor(TESTDATA.height/dim.cellHeight)]);
// moved to right by 10 cells
await ctx.proxy.write('#'.repeat(10) + SIXEL_SEQ_0);
deepStrictEqual(await getCursor(), [10, Math.floor(TESTDATA.height/dim.cellHeight) * 2]);
// moved by 30 cells (+10 prev)
await ctx.proxy.write('#'.repeat(30) + SIXEL_SEQ_0);
deepStrictEqual(await getCursor(), [10 + 30, Math.floor(TESTDATA.height/dim.cellHeight) * 3]);
});
});
test.describe('image lifecycle & eviction', () => {
test('delete image once scrolled off', async () => {
await ctx.proxy.write(SIXEL_SEQ_0);
pollFor(ctx.page, 'window.imageAddon._storage._images.size', 1);
// scroll to scrollback + rows - 1
await ctx.page.evaluate(
scrollback => new Promise(res => (window as any).term.write('\n'.repeat(scrollback), res)),
(await getScrollbackPlusRows() - 1)
);
// wait here, as we have to make sure, that eviction did not yet occur
await new Promise(r => setTimeout(r, 100));
pollFor(ctx.page, 'window.imageAddon._storage._images.size', 1);
// scroll one further should delete the image
await ctx.page.evaluate(() => new Promise(res => (window as any).term.write('\n', res)));
pollFor(ctx.page, 'window.imageAddon._storage._images.size', 0);
});
test('get storageUsage', async () => {
strictEqual(await ctx.page.evaluate('window.imageAddon.storageUsage'), 0);
await ctx.proxy.write(SIXEL_SEQ_0);
ok(Math.abs((await ctx.page.evaluate<number>('window.imageAddon.storageUsage')) - 640 * 80 * 4 / 1000000) < 0.05);
});
test('get/set storageLimit', async () => {
strictEqual(await ctx.page.evaluate('window.imageAddon.storageLimit'), 128);
strictEqual(await ctx.page.evaluate('window.imageAddon.storageLimit = 1'), 1);
strictEqual(await ctx.page.evaluate('window.imageAddon.storageLimit'), 1);
});
test('remove images by storage limit pressure', async () => {
strictEqual(await ctx.page.evaluate('window.imageAddon.storageLimit = 1'), 1);
// never go beyond storage limit
await ctx.proxy.write(SIXEL_SEQ_0);
await ctx.proxy.write(SIXEL_SEQ_0);
await ctx.proxy.write(SIXEL_SEQ_0);
await ctx.proxy.write(SIXEL_SEQ_0);
await new Promise(r => setTimeout(r, 50));
const usage = await ctx.page.evaluate('window.imageAddon.storageUsage');
await ctx.proxy.write(SIXEL_SEQ_0);
await ctx.proxy.write(SIXEL_SEQ_0);
await ctx.proxy.write(SIXEL_SEQ_0);
await ctx.proxy.write(SIXEL_SEQ_0);
await new Promise(r => setTimeout(r, 50));
strictEqual(await ctx.page.evaluate('window.imageAddon.storageUsage'), usage);
strictEqual(usage as number < 1, true);
});
test('set storageLimit removes images synchronously', async () => {
await ctx.proxy.write(SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0);
const usage: number = await ctx.page.evaluate('window.imageAddon.storageUsage');
const newUsage: number = await ctx.page.evaluate('window.imageAddon.storageLimit = 0.5; window.imageAddon.storageUsage');
strictEqual(newUsage < usage, true);
strictEqual(newUsage < 0.5, true);
});
test('clear alternate images on buffer change', async () => {
strictEqual(await ctx.page.evaluate('window.imageAddon.storageUsage'), 0);
await ctx.proxy.write('\x1b[?1049h' + SIXEL_SEQ_0);
ok(Math.abs((await ctx.page.evaluate<number>('window.imageAddon.storageUsage')) - 640 * 80 * 4 / 1000000) < 0.05);
await ctx.proxy.write('\x1b[?1049l');
strictEqual(await ctx.page.evaluate('window.imageAddon.storageUsage'), 0);
});
test('evict tiles by in-place overwrites (only full overwrite tested)', async () => {
await new Promise(r => setTimeout(r, 50));
await ctx.proxy.write('\x1b[H' + SIXEL_SEQ_0 + '\x1b[100;100H');
let usage = await ctx.page.evaluate('window.imageAddon.storageUsage');
while (usage === 0) {
await new Promise(r => setTimeout(r, 50));
usage = await ctx.page.evaluate('window.imageAddon.storageUsage');
}
await ctx.proxy.write('\x1b[H' + SIXEL_SEQ_0 + '\x1b[100;100H');
await new Promise(r => setTimeout(r, 200)); // wait some time and re-check
strictEqual(await ctx.page.evaluate('window.imageAddon.storageUsage'), usage);
});
test('manual eviction on alternate buffer must not miss images', async () => {
await ctx.proxy.write('\x1b[?1049h');
await ctx.proxy.write(SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0);
await new Promise(r => setTimeout(r, 50));
const usage: number = await ctx.page.evaluate('window.imageAddon.storageUsage');
await ctx.proxy.write(SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0);
await ctx.proxy.write(SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0);
await new Promise(r => setTimeout(r, 50));
const newUsage: number = await ctx.page.evaluate('window.imageAddon.storageUsage');
strictEqual(newUsage, usage);
});
});
test.describe('IIP support - testimages', () => {
test('palette.png', async () => {
await ctx.proxy.write(TESTDATA_IIP[0][0]);
deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[0][1]);
});
test('spinfox.png', async () => {
await ctx.proxy.write(TESTDATA_IIP[1][0]);
deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[1][1]);
});
test('w3c gif', async () => {
await ctx.proxy.write(TESTDATA_IIP[2][0]);
deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[2][1]);
});
test('w3c jpeg', async () => {
await ctx.proxy.write(TESTDATA_IIP[3][0]);
deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[3][1]);
});
test('w3c png', async () => {
await ctx.proxy.write(TESTDATA_IIP[4][0]);
deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[4][1]);
});
});
});
/**
* terminal access helpers.
*/
async function getDimensions(): Promise<IDimensions> {
const dimensions: any = await ctx.page.evaluate(`term._core._renderService.dimensions`);
return {
cellWidth: Math.round(dimensions.css.cell.width),
cellHeight: Math.round(dimensions.css.cell.height),
width: Math.round(dimensions.css.canvas.width),
height: Math.round(dimensions.css.canvas.height)
};
}
async function getCursor(): Promise<[number, number]> {
return ctx.page.evaluate('[window.term.buffer.active.cursorX, window.term.buffer.active.cursorY]');
}
async function getImageStorageLength(): Promise<number> {
return ctx.page.evaluate('window.imageAddon._storage._images.size');
}
async function getScrollbackPlusRows(): Promise<number> {
return ctx.page.evaluate('window.term.options.scrollback + window.term.rows');
}
async function getOrigSize(id: number): Promise<[number, number]> {
return ctx.page.evaluate<any>(`[
window.imageAddon._storage._images.get(${id}).orig.width,
window.imageAddon._storage._images.get(${id}).orig.height
]`);
}
@@ -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 run start-server-only',
port: 3000,
timeout: 120000,
reuseExistingServer: !process.env.CI
}
};
export default config;
+18 -8
View File
@@ -1,21 +1,27 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2017",
"target": "ESNext",
"lib": [
"es2021",
],
"rootDir": ".",
"outDir": "../out-test",
"sourceMap": true,
"removeComments": true,
"strict": true,
"baseUrl": ".",
"paths": {
"browser/*": [ "../../../src/browser/*" ],
"common/*": [ "../../../src/common/*" ]
"common/*": [
"../../../src/common/*"
],
"browser/*": [
"../../../src/browser/*"
]
},
"strict": true,
"types": [
"../../../node_modules/@types/mocha",
"../../../node_modules/@types/node",
"../../../out-test/api/TestUtils"
"../../../out-test/playwright/TestUtils"
]
},
"include": [
@@ -23,7 +29,11 @@
"../../../typings/xterm.d.ts"
],
"references": [
{ "path": "../../../src/browser" },
{ "path": "../../../src/common" }
{
"path": "../../../src/common"
},
{
"path": "../../../src/browser"
}
]
}
-510
View File
@@ -1,510 +0,0 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
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/test';
const APP = 'http://127.0.0.1:3001/test';
let browser: Browser;
let page: Page;
const width = 800;
const height = 600;
describe('Search Tests', function (): void {
before(async function (): Promise<any> {
browser = await launchBrowser();
page = await (await browser.newContext()).newPage();
await page.setViewportSize({ width, height });
await page.goto(APP);
await openTerminal(page);
});
after(() => {
browser.close();
});
beforeEach(async () => {
await page.evaluate(`
window.term.reset()
window.search?.dispose();
window.search = new SearchAddon();
window.term.loadAddon(window.search);
`);
});
it('Simple Search', async () => {
await writeSync(page, 'dafhdjfldshafhldsahfkjhldhjkftestlhfdsakjfhdjhlfdsjkafhjdlk');
assert.deepEqual(await page.evaluate(`window.search.findNext('test')`), true);
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'test');
});
it('Scrolling Search', async () => {
let dataString = '';
for (let i = 0; i < 100; i++) {
if (i === 52) {
dataString += '$^1_3{}test$#';
}
dataString += makeData(50);
}
await writeSync(page, dataString);
assert.deepEqual(await page.evaluate(`window.search.findNext('$^1_3{}test$#')`), true);
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), '$^1_3{}test$#');
});
it('Incremental Find Previous', async () => {
await page.evaluate(`window.term.writeln('package.jsonc\\n')`);
await writeSync(page, 'package.json pack package.lock');
await page.evaluate(`window.search.findPrevious('pack', {incremental: true})`);
let line: string = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().start.y).translateToString()`);
let selectionPosition: { start: { x: number, y: number }, end: { x: number, y: number } } = await page.evaluate(`window.term.getSelectionPosition()`);
// We look further ahead in the line to ensure that pack was selected from package.lock
assert.deepEqual(line.substring(selectionPosition.start.x, selectionPosition.end.x + 8), 'package.lock');
await page.evaluate(`window.search.findPrevious('package.j', {incremental: true})`);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(line.substring(selectionPosition.start.x, selectionPosition.end.x + 3), 'package.json');
await page.evaluate(`window.search.findPrevious('package.jsonc', {incremental: true})`);
// We have to reevaluate line because it should have switched starting rows at this point
line = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().start.y).translateToString()`);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(line.substring(selectionPosition.start.x, selectionPosition.end.x), 'package.jsonc');
});
it('Incremental Find Next', async () => {
await page.evaluate(`window.term.writeln('package.lock pack package.json package.ups\\n')`);
await writeSync(page, 'package.jsonc');
await page.evaluate(`window.search.findNext('pack', {incremental: true})`);
let line: string = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().start.y).translateToString()`);
let selectionPosition: { start: { x: number, y: number }, end: { x: number, y: number } } = await page.evaluate(`window.term.getSelectionPosition()`);
// We look further ahead in the line to ensure that pack was selected from package.lock
assert.deepEqual(line.substring(selectionPosition.start.x, selectionPosition.end.x + 8), 'package.lock');
await page.evaluate(`window.search.findNext('package.j', {incremental: true})`);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(line.substring(selectionPosition.start.x, selectionPosition.end.x + 3), 'package.json');
await page.evaluate(`window.search.findNext('package.jsonc', {incremental: true})`);
// We have to reevaluate line because it should have switched starting rows at this point
line = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().start.y).translateToString()`);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(line.substring(selectionPosition.start.x, selectionPosition.end.x), 'package.jsonc');
});
it('Simple Regex', async () => {
await writeSync(page, 'abc123defABCD');
await page.evaluate(`window.search.findNext('[a-z]+', {regex: true})`);
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'abc');
await page.evaluate(`window.search.findNext('[A-Z]+', {regex: true, caseSensitive: true})`);
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'ABCD');
});
it('Search for single result twice should not unselect it', async () => {
await writeSync(page, 'abc def');
assert.deepEqual(await page.evaluate(`window.search.findNext('abc')`), true);
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'abc');
assert.deepEqual(await page.evaluate(`window.search.findNext('abc')`), true);
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'abc');
});
it('Search for result bounding with wide unicode chars', async () => {
await writeSync(page, '中文xx𝄞𝄞');
assert.deepEqual(await page.evaluate(`window.search.findNext('中')`), true);
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), '中');
assert.deepEqual(await page.evaluate(`window.search.findNext('xx')`), true);
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'xx');
assert.deepEqual(await page.evaluate(`window.search.findNext('𝄞')`), true);
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), '𝄞');
assert.deepEqual(await page.evaluate(`window.search.findNext('𝄞')`), true);
assert.deepEqual(await page.evaluate(`window.term.getSelectionPosition()`), {
start: {
x: 7,
y: 0
},
end: {
x: 8,
y: 0
}
});
});
describe('onDidChangeResults', async () => {
describe('findNext', () => {
it('should not fire unless the decorations option is set', async () => {
await page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await writeSync(page, 'abc');
assert.strictEqual(await page.evaluate(`window.search.findNext('a')`), true);
assert.strictEqual(await page.evaluate('window.calls.length'), 0);
assert.strictEqual(await page.evaluate(`window.search.findNext('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.strictEqual(await page.evaluate('window.calls.length'), 1);
});
it('should fire with correct event values', async () => {
await page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await writeSync(page, 'abc bc c');
assert.strictEqual(await page.evaluate(`window.search.findNext('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 }
]);
assert.strictEqual(await page.evaluate(`window.search.findNext('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 }
]);
assert.strictEqual(await page.evaluate(`window.search.findNext('d', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 0, resultIndex: -1 }
]);
assert.strictEqual(await page.evaluate(`window.search.findNext('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.strictEqual(await page.evaluate(`window.search.findNext('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.strictEqual(await page.evaluate(`window.search.findNext('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 0, resultIndex: -1 },
{ resultCount: 3, resultIndex: 0 },
{ resultCount: 3, resultIndex: 1 },
{ resultCount: 3, resultIndex: 2 }
]);
});
it('should fire with correct event values (incremental)', async () => {
await page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await writeSync(page, 'd abc aabc d');
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('a', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 0 }
]);
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('ab', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 }
]);
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('abc', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 }
]);
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('abc', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 1 }
]);
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('d', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 1 }
]);
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('abcd', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 0, resultIndex: -1 }
]);
});
it('should fire with more than 1k matches', async () => {
await page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
const data = ('a bc'.repeat(10) + '\\n\\r').repeat(150);
await writeSync(page, data);
assert.strictEqual(await page.evaluate(`window.search.findNext('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1000, resultIndex: 0 }
]);
assert.strictEqual(await page.evaluate(`window.search.findNext('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1000, resultIndex: 0 },
{ resultCount: 1000, resultIndex: 1 }
]);
assert.strictEqual(await page.evaluate(`window.search.findNext('bc', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1000, resultIndex: 0 },
{ resultCount: 1000, resultIndex: 1 },
{ resultCount: 1000, resultIndex: 1 }
]);
});
it('should fire when writing to terminal', async () => {
await page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await writeSync(page, 'abc bc c\\n\\r'.repeat(2));
assert.strictEqual(await page.evaluate(`window.search.findNext('abc', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 2, resultIndex: 0 }
]);
await writeSync(page, 'abc bc c\\n\\r');
await timeout(300);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 3, resultIndex: 0 }
]);
});
});
describe('findPrevious', () => {
it('should not fire unless the decorations option is set', async () => {
await page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await writeSync(page, 'abc');
assert.strictEqual(await page.evaluate(`window.search.findPrevious('a')`), true);
assert.strictEqual(await page.evaluate('window.calls.length'), 0);
assert.strictEqual(await page.evaluate(`window.search.findPrevious('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.strictEqual(await page.evaluate('window.calls.length'), 1);
});
it('should fire with correct event values', async () => {
await page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await writeSync(page, 'abc bc c');
assert.strictEqual(await page.evaluate(`window.search.findPrevious('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 }
]);
await page.evaluate(`window.term.clearSelection()`);
assert.strictEqual(await page.evaluate(`window.search.findPrevious('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 },
{ resultCount: 2, resultIndex: 1 }
]);
await timeout(2000);
assert.strictEqual(await page.evaluate(`debugger; window.search.findPrevious('d', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 0, resultIndex: -1 }
]);
assert.strictEqual(await page.evaluate(`window.search.findPrevious('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.strictEqual(await page.evaluate(`window.search.findPrevious('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.strictEqual(await page.evaluate(`window.search.findPrevious('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 0, resultIndex: -1 },
{ resultCount: 3, resultIndex: 2 },
{ resultCount: 3, resultIndex: 1 },
{ resultCount: 3, resultIndex: 0 }
]);
});
it('should fire with correct event values (incremental)', async () => {
await page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await writeSync(page, 'd abc aabc d');
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('a', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 2 }
]);
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('ab', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 2 },
{ resultCount: 2, resultIndex: 1 }
]);
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('abc', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 2 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 1 }
]);
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('abc', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 2 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 0 }
]);
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('d', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 2 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 1 }
]);
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('abcd', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 2 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 0, resultIndex: -1 }
]);
});
it('should fire with more than 1k matches', async () => {
await page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
const data = ('a bc'.repeat(10) + '\\n\\r').repeat(150);
await writeSync(page, data);
assert.strictEqual(await page.evaluate(`window.search.findPrevious('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1000, resultIndex: -1 }
]);
assert.strictEqual(await page.evaluate(`window.search.findPrevious('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1000, resultIndex: -1 },
{ resultCount: 1000, resultIndex: -1 }
]);
assert.strictEqual(await page.evaluate(`window.search.findPrevious('bc', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1000, resultIndex: -1 },
{ resultCount: 1000, resultIndex: -1 },
{ resultCount: 1000, resultIndex: -1 }
]);
});
it('should fire when writing to terminal', async () => {
await page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await writeSync(page, 'abc bc c\\n\\r'.repeat(2));
assert.strictEqual(await page.evaluate(`window.search.findPrevious('abc', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 2, resultIndex: 1 }
]);
await writeSync(page, 'abc bc c\\n\\r');
await timeout(300);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 3, resultIndex: 1 }
]);
});
});
});
describe('Regression tests', () => {
describe('#2444 wrapped line content not being found', () => {
let fixture: string;
before(async () => {
const rawFixture = await new Promise<Buffer>(r => readFile(resolve(__dirname, '../fixtures/issue-2444'), (err, data) => r(data)));
if (process.platform === 'win32') {
fixture = rawFixture.toString()
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r');
} else {
fixture = rawFixture.toString()
.replace(/\n/g, '\\n\\r');
}
fixture = fixture
.replace(/'/g, `\\'`);
});
it('should find all occurrences using findNext', async () => {
await writeSync(page, fixture);
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
let selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { start: { x: 24, y: 53 }, end: { x: 30, y: 53 } });
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { start: { x: 24, y: 76 }, end: { x: 30, y: 76 } });
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { start: { x: 24, y: 96 }, end: { x: 30, y: 96 } });
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { start: { x: 1, y: 114 }, end: { x: 7, y: 114 } });
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { start: { x: 11, y: 115 }, end: { x: 17, y: 115 } });
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { start: { x: 1, y: 126 }, end: { x: 7, y: 126 } });
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { start: { x: 11, y: 127 }, end: { x: 17, y: 127 } });
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { start: { x: 1, y: 135 }, end: { x: 7, y: 135 } });
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { start: { x: 11, y: 136 }, end: { x: 17, y: 136 } });
// Wrap around to first result
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { start: { x: 24, y: 53 }, end: { x: 30, y: 53 } });
});
it('should y all occurrences using findPrevious', async () => {
await writeSync(page, fixture);
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
let selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { start: { x: 11, y: 136 }, end: { x: 17, y: 136 } });
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { start: { x: 1, y: 135 }, end: { x: 7, y: 135 } });
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { start: { x: 11, y: 127 }, end: { x: 17, y: 127 } });
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { start: { x: 1, y: 126 }, end: { x: 7, y: 126 } });
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { start: { x: 11, y: 115 }, end: { x: 17, y: 115 } });
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { start: { x: 1, y: 114 }, end: { x: 7, y: 114 } });
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { start: { x: 24, y: 96 }, end: { x: 30, y: 96 } });
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { start: { x: 24, y: 76 }, end: { x: 30, y: 76 } });
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { start: { x: 24, y: 53 }, end: { x: 30, y: 53 } });
// Wrap around to first result
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { start: { x: 11, y: 136 }, end: { x: 17, y: 136 } });
});
});
});
describe('#3834 lines with null characters before search terms', () => {
// This case can be triggered by the prompt when using starship under conpty
it('should find all matches on a line containing null characters', async () => {
await page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
// Move cursor forward 1 time to create a null character, as opposed to regular whitespace
await writeSync(page, '\\x1b[CHi Hi');
assert.strictEqual(await page.evaluate(`window.search.findPrevious('h', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 2, resultIndex: 1 }
]);
});
});
});
function makeData(length: number): string {
let result = '';
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
return result;
}
@@ -0,0 +1,489 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import test from '@playwright/test';
import { deepStrictEqual, strictEqual } from 'assert';
import { readFile } from 'fs';
import { resolve } from 'path';
import { ITestContext, createTestContext, openTerminal, timeout } from '../../../out-test/playwright/TestUtils';
let ctx: ITestContext;
test.beforeAll(async ({ browser }) => {
ctx = await createTestContext(browser);
await openTerminal(ctx);
});
test.afterAll(async () => await ctx.page.close());
test.describe('Search Tests', () => {
test.beforeEach(async () => {
await ctx.page.evaluate(`
window.term.reset()
window.search?.dispose();
window.search = new SearchAddon();
window.term.loadAddon(window.search);
`);
});
test('Simple Search', async () => {
await ctx.proxy.write('dafhdjfldshafhldsahfkjhldhjkftestlhfdsakjfhdjhlfdsjkafhjdlk');
deepStrictEqual(await ctx.page.evaluate(`window.search.findNext('test')`), true);
deepStrictEqual(await ctx.proxy.getSelection(), 'test');
});
test('Scrolling Search', async () => {
let dataString = '';
for (let i = 0; i < 100; i++) {
if (i === 52) {
dataString += '$^1_3{}test$#';
}
dataString += makeData(50);
}
await ctx.proxy.write(dataString);
deepStrictEqual(await ctx.page.evaluate(`window.search.findNext('$^1_3{}test$#')`), true);
deepStrictEqual(await ctx.proxy.getSelection(), '$^1_3{}test$#');
});
test('Incremental Find Previous', async () => {
await ctx.proxy.writeln(`package.jsonc\n`);
await ctx.proxy.write('package.json pack package.lock');
await ctx.page.evaluate(`window.search.findPrevious('pack', {incremental: true})`);
let selectionPosition: { start: { x: number, y: number }, end: { x: number, y: number } } = (await ctx.proxy.getSelectionPosition())!;
let line: string = await (await ctx.proxy.buffer.active.getLine(selectionPosition.start.y))!.translateToString();
// We look further ahead in the line to ensure that pack was selected from package.lock
deepStrictEqual(line.substring(selectionPosition.start.x, selectionPosition.end.x + 8), 'package.lock');
await ctx.page.evaluate(`window.search.findPrevious('package.j', {incremental: true})`);
selectionPosition = (await ctx.proxy.getSelectionPosition())!;
deepStrictEqual(line.substring(selectionPosition.start.x, selectionPosition.end.x + 3), 'package.json');
await ctx.page.evaluate(`window.search.findPrevious('package.jsonc', {incremental: true})`);
// We have to reevaluate line because it should have switched starting rows at this point
selectionPosition = (await ctx.proxy.getSelectionPosition())!;
line = await (await ctx.proxy.buffer.active.getLine(selectionPosition.start.y))!.translateToString();
deepStrictEqual(line.substring(selectionPosition.start.x, selectionPosition.end.x), 'package.jsonc');
});
test('Incremental Find Next', async () => {
await ctx.proxy.writeln(`package.lock pack package.json package.ups\n`);
await ctx.proxy.write('package.jsonc');
await ctx.page.evaluate(`window.search.findNext('pack', {incremental: true})`);
let selectionPosition: { start: { x: number, y: number }, end: { x: number, y: number } } = (await ctx.proxy.getSelectionPosition())!;
let line: string = await (await ctx.proxy.buffer.active.getLine(selectionPosition.start.y))!.translateToString();
// We look further ahead in the line to ensure that pack was selected from package.lock
deepStrictEqual(line.substring(selectionPosition.start.x, selectionPosition.end.x + 8), 'package.lock');
await ctx.page.evaluate(`window.search.findNext('package.j', {incremental: true})`);
selectionPosition = (await ctx.proxy.getSelectionPosition())!;
deepStrictEqual(line.substring(selectionPosition.start.x, selectionPosition.end.x + 3), 'package.json');
await ctx.page.evaluate(`window.search.findNext('package.jsonc', {incremental: true})`);
// We have to reevaluate line because it should have switched starting rows at this point
selectionPosition = (await ctx.proxy.getSelectionPosition())!;
line = await (await ctx.proxy.buffer.active.getLine(selectionPosition.start.y))!.translateToString();
deepStrictEqual(line.substring(selectionPosition.start.x, selectionPosition.end.x), 'package.jsonc');
});
test('Simple Regex', async () => {
await ctx.proxy.write('abc123defABCD');
await ctx.page.evaluate(`window.search.findNext('[a-z]+', {regex: true})`);
deepStrictEqual(await ctx.proxy.getSelection(), 'abc');
await ctx.page.evaluate(`window.search.findNext('[A-Z]+', {regex: true, caseSensitive: true})`);
deepStrictEqual(await ctx.proxy.getSelection(), 'ABCD');
});
test('Search for single result twice should not unselect it', async () => {
await ctx.proxy.write('abc def');
deepStrictEqual(await ctx.page.evaluate(`window.search.findNext('abc')`), true);
deepStrictEqual(await ctx.proxy.getSelection(), 'abc');
deepStrictEqual(await ctx.page.evaluate(`window.search.findNext('abc')`), true);
deepStrictEqual(await ctx.proxy.getSelection(), 'abc');
});
test('Search for result bounding with wide unicode chars', async () => {
await ctx.proxy.write('中文xx𝄞𝄞');
deepStrictEqual(await ctx.page.evaluate(`window.search.findNext('中')`), true);
deepStrictEqual(await ctx.proxy.getSelection(), '中');
deepStrictEqual(await ctx.page.evaluate(`window.search.findNext('xx')`), true);
deepStrictEqual(await ctx.proxy.getSelection(), 'xx');
deepStrictEqual(await ctx.page.evaluate(`window.search.findNext('𝄞')`), true);
deepStrictEqual(await ctx.proxy.getSelection(), '𝄞');
deepStrictEqual(await ctx.page.evaluate(`window.search.findNext('𝄞')`), true);
deepStrictEqual(await ctx.proxy.getSelectionPosition(), {
start: {
x: 7,
y: 0
},
end: {
x: 8,
y: 0
}
});
});
test.describe('onDidChangeResults', async () => {
test.describe('findNext', () => {
test('should not fire unless the decorations option is set', async () => {
await ctx.page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await ctx.proxy.write('abc');
strictEqual(await ctx.page.evaluate(`window.search.findNext('a')`), true);
strictEqual(await ctx.page.evaluate('window.calls.length'), 0);
strictEqual(await ctx.page.evaluate(`window.search.findNext('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
strictEqual(await ctx.page.evaluate('window.calls.length'), 1);
});
test('should fire with correct event values', async () => {
await ctx.page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await ctx.proxy.write('abc bc c');
strictEqual(await ctx.page.evaluate(`window.search.findNext('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 }
]);
strictEqual(await ctx.page.evaluate(`window.search.findNext('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 }
]);
strictEqual(await ctx.page.evaluate(`window.search.findNext('d', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 0, resultIndex: -1 }
]);
strictEqual(await ctx.page.evaluate(`window.search.findNext('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
strictEqual(await ctx.page.evaluate(`window.search.findNext('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
strictEqual(await ctx.page.evaluate(`window.search.findNext('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 0, resultIndex: -1 },
{ resultCount: 3, resultIndex: 0 },
{ resultCount: 3, resultIndex: 1 },
{ resultCount: 3, resultIndex: 2 }
]);
});
test('should fire with correct event values (incremental)', async () => {
await ctx.page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await ctx.proxy.write('d abc aabc d');
deepStrictEqual(await ctx.page.evaluate(`window.search.findNext('a', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 0 }
]);
deepStrictEqual(await ctx.page.evaluate(`window.search.findNext('ab', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 }
]);
deepStrictEqual(await ctx.page.evaluate(`window.search.findNext('abc', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 }
]);
deepStrictEqual(await ctx.page.evaluate(`window.search.findNext('abc', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 1 }
]);
deepStrictEqual(await ctx.page.evaluate(`window.search.findNext('d', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 1 }
]);
deepStrictEqual(await ctx.page.evaluate(`window.search.findNext('abcd', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 0, resultIndex: -1 }
]);
});
test('should fire with more than 1k matches', async () => {
await ctx.page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
const data = ('a bc'.repeat(10) + '\\n\\r').repeat(150);
await ctx.proxy.write(data);
strictEqual(await ctx.page.evaluate(`window.search.findNext('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 1000, resultIndex: 0 }
]);
strictEqual(await ctx.page.evaluate(`window.search.findNext('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 1000, resultIndex: 0 },
{ resultCount: 1000, resultIndex: 1 }
]);
strictEqual(await ctx.page.evaluate(`window.search.findNext('bc', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 1000, resultIndex: 0 },
{ resultCount: 1000, resultIndex: 1 },
{ resultCount: 1000, resultIndex: 1 }
]);
});
test('should fire when writing to terminal', async () => {
await ctx.page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await ctx.proxy.write('abc bc c\\n\\r'.repeat(2));
strictEqual(await ctx.page.evaluate(`window.search.findNext('abc', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 2, resultIndex: 0 }
]);
await ctx.proxy.write('abc bc c\\n\\r');
await timeout(300);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 3, resultIndex: 0 }
]);
});
});
test.describe('findPrevious', () => {
test('should not fire unless the decorations option is set', async () => {
await ctx.page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await ctx.proxy.write('abc');
strictEqual(await ctx.page.evaluate(`window.search.findPrevious('a')`), true);
strictEqual(await ctx.page.evaluate('window.calls.length'), 0);
strictEqual(await ctx.page.evaluate(`window.search.findPrevious('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
strictEqual(await ctx.page.evaluate('window.calls.length'), 1);
});
test('should fire with correct event values', async () => {
await ctx.page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await ctx.proxy.write('abc bc c');
strictEqual(await ctx.page.evaluate(`window.search.findPrevious('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 }
]);
await ctx.page.evaluate(`window.term.clearSelection()`);
strictEqual(await ctx.page.evaluate(`window.search.findPrevious('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 },
{ resultCount: 2, resultIndex: 1 }
]);
await timeout(2000);
strictEqual(await ctx.page.evaluate(`debugger; window.search.findPrevious('d', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 0, resultIndex: -1 }
]);
strictEqual(await ctx.page.evaluate(`window.search.findPrevious('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
strictEqual(await ctx.page.evaluate(`window.search.findPrevious('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
strictEqual(await ctx.page.evaluate(`window.search.findPrevious('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 0, resultIndex: -1 },
{ resultCount: 3, resultIndex: 2 },
{ resultCount: 3, resultIndex: 1 },
{ resultCount: 3, resultIndex: 0 }
]);
});
test('should fire with correct event values (incremental)', async () => {
await ctx.page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await ctx.proxy.write('d abc aabc d');
deepStrictEqual(await ctx.page.evaluate(`window.search.findPrevious('a', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 2 }
]);
deepStrictEqual(await ctx.page.evaluate(`window.search.findPrevious('ab', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 2 },
{ resultCount: 2, resultIndex: 1 }
]);
deepStrictEqual(await ctx.page.evaluate(`window.search.findPrevious('abc', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 2 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 1 }
]);
deepStrictEqual(await ctx.page.evaluate(`window.search.findPrevious('abc', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 2 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 0 }
]);
deepStrictEqual(await ctx.page.evaluate(`window.search.findPrevious('d', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 2 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 1 }
]);
deepStrictEqual(await ctx.page.evaluate(`window.search.findPrevious('abcd', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 2 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 0, resultIndex: -1 }
]);
});
test('should fire with more than 1k matches', async () => {
await ctx.page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
const data = ('a bc'.repeat(10) + '\\n\\r').repeat(150);
await ctx.proxy.write(data);
strictEqual(await ctx.page.evaluate(`window.search.findPrevious('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 1000, resultIndex: -1 }
]);
strictEqual(await ctx.page.evaluate(`window.search.findPrevious('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 1000, resultIndex: -1 },
{ resultCount: 1000, resultIndex: -1 }
]);
strictEqual(await ctx.page.evaluate(`window.search.findPrevious('bc', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 1000, resultIndex: -1 },
{ resultCount: 1000, resultIndex: -1 },
{ resultCount: 1000, resultIndex: -1 }
]);
});
test('should fire when writing to terminal', async () => {
await ctx.page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await ctx.proxy.write('abc bc c\\n\\r'.repeat(2));
strictEqual(await ctx.page.evaluate(`window.search.findPrevious('abc', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 2, resultIndex: 1 }
]);
await ctx.proxy.write('abc bc c\\n\\r');
await timeout(300);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 3, resultIndex: 1 }
]);
});
});
});
test.describe('Regression tests', () => {
test.describe('#2444 wrapped line content not being found', () => {
let fixture: string;
test.beforeAll(async () => {
fixture = (await new Promise<Buffer>(r => readFile(resolve(__dirname, '../fixtures/issue-2444'), (err, data) => r(data)))).toString();
});
test('should find all occurrences using findNext', async () => {
await ctx.proxy.write(fixture);
deepStrictEqual(await ctx.page.evaluate(`window.search.findNext('opencv')`), true);
let selectionPosition = await ctx.proxy.getSelectionPosition();
deepStrictEqual(selectionPosition, { start: { x: 24, y: 53 }, end: { x: 30, y: 53 } });
deepStrictEqual(await ctx.page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await ctx.proxy.getSelectionPosition();
deepStrictEqual(selectionPosition, { start: { x: 24, y: 76 }, end: { x: 30, y: 76 } });
deepStrictEqual(await ctx.page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await ctx.proxy.getSelectionPosition();
deepStrictEqual(selectionPosition, { start: { x: 24, y: 96 }, end: { x: 30, y: 96 } });
deepStrictEqual(await ctx.page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await ctx.proxy.getSelectionPosition();
deepStrictEqual(selectionPosition, { start: { x: 1, y: 114 }, end: { x: 7, y: 114 } });
deepStrictEqual(await ctx.page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await ctx.proxy.getSelectionPosition();
deepStrictEqual(selectionPosition, { start: { x: 11, y: 115 }, end: { x: 17, y: 115 } });
deepStrictEqual(await ctx.page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await ctx.proxy.getSelectionPosition();
deepStrictEqual(selectionPosition, { start: { x: 1, y: 126 }, end: { x: 7, y: 126 } });
deepStrictEqual(await ctx.page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await ctx.proxy.getSelectionPosition();
deepStrictEqual(selectionPosition, { start: { x: 11, y: 127 }, end: { x: 17, y: 127 } });
deepStrictEqual(await ctx.page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await ctx.proxy.getSelectionPosition();
deepStrictEqual(selectionPosition, { start: { x: 1, y: 135 }, end: { x: 7, y: 135 } });
deepStrictEqual(await ctx.page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await ctx.proxy.getSelectionPosition();
deepStrictEqual(selectionPosition, { start: { x: 11, y: 136 }, end: { x: 17, y: 136 } });
// Wrap around to first result
deepStrictEqual(await ctx.page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await ctx.proxy.getSelectionPosition();
deepStrictEqual(selectionPosition, { start: { x: 24, y: 53 }, end: { x: 30, y: 53 } });
});
test('should y all occurrences using findPrevious', async () => {
await ctx.proxy.write(fixture);
deepStrictEqual(await ctx.page.evaluate(`window.search.findPrevious('opencv')`), true);
let selectionPosition = await ctx.proxy.getSelectionPosition();
deepStrictEqual(selectionPosition, { start: { x: 11, y: 136 }, end: { x: 17, y: 136 } });
deepStrictEqual(await ctx.page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await ctx.proxy.getSelectionPosition();
deepStrictEqual(selectionPosition, { start: { x: 1, y: 135 }, end: { x: 7, y: 135 } });
deepStrictEqual(await ctx.page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await ctx.proxy.getSelectionPosition();
deepStrictEqual(selectionPosition, { start: { x: 11, y: 127 }, end: { x: 17, y: 127 } });
deepStrictEqual(await ctx.page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await ctx.proxy.getSelectionPosition();
deepStrictEqual(selectionPosition, { start: { x: 1, y: 126 }, end: { x: 7, y: 126 } });
deepStrictEqual(await ctx.page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await ctx.proxy.getSelectionPosition();
deepStrictEqual(selectionPosition, { start: { x: 11, y: 115 }, end: { x: 17, y: 115 } });
deepStrictEqual(await ctx.page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await ctx.proxy.getSelectionPosition();
deepStrictEqual(selectionPosition, { start: { x: 1, y: 114 }, end: { x: 7, y: 114 } });
deepStrictEqual(await ctx.page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await ctx.proxy.getSelectionPosition();
deepStrictEqual(selectionPosition, { start: { x: 24, y: 96 }, end: { x: 30, y: 96 } });
deepStrictEqual(await ctx.page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await ctx.proxy.getSelectionPosition();
deepStrictEqual(selectionPosition, { start: { x: 24, y: 76 }, end: { x: 30, y: 76 } });
deepStrictEqual(await ctx.page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await ctx.proxy.getSelectionPosition();
deepStrictEqual(selectionPosition, { start: { x: 24, y: 53 }, end: { x: 30, y: 53 } });
// Wrap around to first result
deepStrictEqual(await ctx.page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await ctx.proxy.getSelectionPosition();
deepStrictEqual(selectionPosition, { start: { x: 11, y: 136 }, end: { x: 17, y: 136 } });
});
});
});
test.describe('#3834 lines with null characters before search terms', () => {
// This case can be triggered by the prompt when using starship under conpty
test('should find all matches on a line containing null characters', async () => {
await ctx.page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
// Move cursor forward 1 time to create a null character, as opposed to regular whitespace
await ctx.proxy.write('\\x1b[CHi Hi');
strictEqual(await ctx.page.evaluate(`window.search.findPrevious('h', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 2, resultIndex: 1 }
]);
});
});
});
function makeData(length: number): string {
let result = '';
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
return result;
}
@@ -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 run start-server-only',
port: 3000,
timeout: 120000,
reuseExistingServer: !process.env.CI
}
};
export default config;
+19 -3
View File
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2021",
"target": "ESNext",
"lib": [
"es2021",
],
@@ -9,15 +9,31 @@
"outDir": "../out-test",
"sourceMap": true,
"removeComments": true,
"baseUrl": ".",
"paths": {
"common/*": [
"../../../src/common/*"
],
"browser/*": [
"../../../src/browser/*"
]
},
"strict": true,
"types": [
"../../../node_modules/@types/mocha",
"../../../node_modules/@types/node",
"../../../out-test/api/TestUtils"
"../../../out-test/playwright/TestUtils"
]
},
"include": [
"./**/*",
"../../../typings/xterm.d.ts"
],
"references": [
{
"path": "../../../src/common"
},
{
"path": "../../../src/browser"
}
]
}
@@ -3,49 +3,47 @@
* @license MIT
*/
import { assert } from 'chai';
import { openTerminal, writeSync, launchBrowser } from '../../../out-test/api/TestUtils';
import { Browser, Page } from '@playwright/test';
import test from '@playwright/test';
import { deepStrictEqual, notDeepStrictEqual, strictEqual } from 'assert';
import { readFile } from 'fs';
import { resolve } from 'path';
import { ITestContext, createTestContext, openTerminal, timeout, writeSync } from '../../../out-test/playwright/TestUtils';
const APP = 'http://127.0.0.1:3001/test';
let browser: Browser;
let page: Page;
const width = 800;
const height = 600;
const writeRawSync = (page: any, str: string): Promise<void> => writeSync(page, `' +` + JSON.stringify(str) + `+ '`);
const writeRawSync = (page: any, str: string): Promise<void> => writeSync(ctx.page, `' +` + JSON.stringify(str) + `+ '`);
const testNormalScreenEqual = async (page: any, str: string): Promise<void> => {
await writeRawSync(page, str);
const originalBuffer = await page.evaluate(`inspectBuffer(term.buffer.normal);`);
await writeRawSync(ctx.page, str);
const originalBuffer = await ctx.page.evaluate(`inspectBuffer(term.buffer.normal);`);
const result = await page.evaluate(`serializeAddon.serialize();`) as string;
await page.evaluate(`term.reset();`);
await writeRawSync(page, result);
const newBuffer = await page.evaluate(`inspectBuffer(term.buffer.normal);`);
const result = await ctx.page.evaluate(`window.serialize.serialize();`) as string;
await ctx.page.evaluate(`term.reset();`);
await writeRawSync(ctx.page, result);
const newBuffer = await ctx.page.evaluate(`inspectBuffer(term.buffer.normal);`);
// chai decides -0 and 0 are different number...
// and firefox have a bug that output -0 for unknown reason
assert.equal(JSON.stringify(originalBuffer), JSON.stringify(newBuffer));
deepStrictEqual(JSON.stringify(originalBuffer), JSON.stringify(newBuffer));
};
async function testSerializeEquals(writeContent: string, expectedSerialized: string): Promise<void> {
await writeRawSync(page, writeContent);
const result = await page.evaluate(`serializeAddon.serialize();`) as string;
assert.strictEqual(result, expectedSerialized);
await writeRawSync(ctx.page, writeContent);
const result = await ctx.page.evaluate(`window.serialize.serialize();`) as string;
strictEqual(result, expectedSerialized);
}
describe('SerializeAddon', () => {
before(async function(): Promise<any> {
browser = await launchBrowser();
page = await (await browser.newContext()).newPage();
await page.setViewportSize({ width, height });
await page.goto(APP);
await openTerminal(page, { rows: 10, cols: 10 });
await page.evaluate(`
window.serializeAddon = new SerializeAddon();
window.term.loadAddon(window.serializeAddon);
let ctx: ITestContext;
test.beforeAll(async ({ browser }) => {
ctx = await createTestContext(browser);
await openTerminal(ctx, { rows: 10, cols: 10 });
});
test.afterAll(async () => await ctx.page.close());
test.describe('SerializeAddon', () => {
test.beforeEach(async () => {
await ctx.page.evaluate(`
window.term.reset()
window.serialize?.dispose();
window.serialize = new SerializeAddon();
window.term.loadAddon(window.serialize);
window.inspectBuffer = (buffer) => {
const lines = [];
for (let i = 0; i < buffer.length; i++) {
@@ -62,56 +60,53 @@ describe('SerializeAddon', () => {
`);
});
after(async () => await browser.close());
beforeEach(async () => await page.evaluate(`window.term.reset()`));
it('produce different output when we call test util with different text', async function(): Promise<any> {
await writeRawSync(page, '12345');
const buffer1 = await page.evaluate(`inspectBuffer(term.buffer.normal);`);
await page.evaluate(`term.reset();`);
await writeRawSync(page, '67890');
const buffer2 = await page.evaluate(`inspectBuffer(term.buffer.normal);`);
assert.throw(() => {
assert.equal(JSON.stringify(buffer1), JSON.stringify(buffer2));
});
test.beforeEach(async () => {
await ctx.proxy.reset();
});
it('produce different output when we call test util with different line wrap', async function(): Promise<any> {
await writeRawSync(page, '1234567890\r\n12345');
const buffer3 = await page.evaluate(`inspectBuffer(term.buffer.normal);`);
test('produce different output when we call test util with different text', async function(): Promise<any> {
await writeRawSync(ctx.page, '12345');
const buffer1 = await ctx.page.evaluate(`inspectBuffer(term.buffer.normal);`);
await page.evaluate(`term.reset();`);
await writeRawSync(page, '123456789012345');
const buffer4 = await page.evaluate(`inspectBuffer(term.buffer.normal);`);
await ctx.page.evaluate(`term.reset();`);
await writeRawSync(ctx.page, '67890');
const buffer2 = await ctx.page.evaluate(`inspectBuffer(term.buffer.normal);`);
assert.throw(() => {
assert.equal(JSON.stringify(buffer3), JSON.stringify(buffer4));
});
notDeepStrictEqual(JSON.stringify(buffer1), JSON.stringify(buffer2));
});
it('empty content', async function(): Promise<any> {
assert.equal(await page.evaluate(`serializeAddon.serialize();`), '');
test('produce different output when we call test util with different line wrap', async function(): Promise<any> {
await writeRawSync(ctx.page, '1234567890\r\n12345');
const buffer3 = await ctx.page.evaluate(`inspectBuffer(term.buffer.normal);`);
await ctx.page.evaluate(`term.reset();`);
await writeRawSync(ctx.page, '123456789012345');
const buffer4 = await ctx.page.evaluate(`inspectBuffer(term.buffer.normal);`);
notDeepStrictEqual(JSON.stringify(buffer3), JSON.stringify(buffer4));
});
it('unwrap wrapped line', async function(): Promise<any> {
test('empty content', async function(): Promise<any> {
strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), '');
});
test('unwrap wrapped line', async function(): Promise<any> {
const lines = ['123456789123456789'];
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
await ctx.proxy.write(lines.join('\r\n'));
strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n'));
});
it('does not unwrap non-wrapped line', async function(): Promise<any> {
test('does not unwrap non-wrapped line', async function(): Promise<any> {
const lines = [
'123456789',
'123456789'
];
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
await ctx.proxy.write(lines.join('\r\n'));
strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n'));
});
it('preserve last empty lines', async function(): Promise<any> {
test('preserve last empty lines', async function(): Promise<any> {
const cols = 10;
const lines = [
'',
@@ -126,50 +121,50 @@ describe('SerializeAddon', () => {
'',
''
];
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
await ctx.proxy.write(lines.join('\r\n'));
strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n'));
});
it('digits content', async function(): Promise<any> {
test('digits content', async function(): Promise<any> {
const rows = 10;
const cols = 10;
const digitsLine = digitsString(cols);
const lines = newArray<string>(digitsLine, rows);
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
await ctx.proxy.write(lines.join('\r\n'));
strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n'));
});
it('serialize with half of scrollback', async function(): Promise<any> {
test('serialize with half of scrollback', async function(): Promise<any> {
const rows = 20;
const scrollback = rows - 10;
const halfScrollback = scrollback / 2;
const cols = 10;
const lines = newArray<string>((index: number) => digitsString(cols, index), rows);
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize({ scrollback: ${halfScrollback} });`), lines.slice(halfScrollback, rows).join('\r\n'));
await ctx.proxy.write(lines.join('\r\n'));
strictEqual(await ctx.page.evaluate(`window.serialize.serialize({ scrollback: ${halfScrollback} });`), lines.slice(halfScrollback, rows).join('\r\n'));
});
it('serialize 0 rows of scrollback', async function(): Promise<any> {
test('serialize 0 rows of scrollback', async function(): Promise<any> {
const rows = 20;
const cols = 10;
const lines = newArray<string>((index: number) => digitsString(cols, index), rows);
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize({ scrollback: 0 });`), lines.slice(rows - 10, rows).join('\r\n'));
await ctx.proxy.write(lines.join('\r\n'));
strictEqual(await ctx.page.evaluate(`window.serialize.serialize({ scrollback: 0 });`), lines.slice(rows - 10, rows).join('\r\n'));
});
it('serialize exclude modes', async () => {
await writeSync(page, 'before\\x1b[?1hafter');
assert.equal(await page.evaluate(`serializeAddon.serialize();`), 'beforeafter\x1b[?1h');
assert.equal(await page.evaluate(`serializeAddon.serialize({ excludeModes: true });`), 'beforeafter');
test('serialize exclude modes', async () => {
await ctx.proxy.write('before\x1b[?1hafter');
strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), 'beforeafter\x1b[?1h');
strictEqual(await ctx.page.evaluate(`window.serialize.serialize({ excludeModes: true });`), 'beforeafter');
});
it('serialize exclude alt buffer', async () => {
await writeSync(page, 'normal\\x1b[?1049h\\x1b[Halt');
assert.equal(await page.evaluate(`serializeAddon.serialize();`), 'normal\x1b[?1049h\x1b[Halt');
assert.equal(await page.evaluate(`serializeAddon.serialize({ excludeAltBuffer: true });`), 'normal');
test('serialize exclude alt buffer', async () => {
await ctx.proxy.write('normal\x1b[?1049h\x1b[Halt');
strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), 'normal\x1b[?1049h\x1b[Halt');
strictEqual(await ctx.page.evaluate(`window.serialize.serialize({ excludeAltBuffer: true });`), 'normal');
});
it('serialize all rows of content with color16', async function(): Promise<any> {
test('serialize all rows of content with color16', async function(): Promise<any> {
const cols = 10;
const color16 = [
30, 31, 32, 33, 34, 35, 36, 37, // Set foreground color
@@ -182,11 +177,11 @@ describe('SerializeAddon', () => {
(index: number) => digitsString(cols, index, `\x1b[${color16[index % color16.length]}m`),
rows
);
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
await ctx.proxy.write(lines.join('\r\n'));
strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n'));
});
it('serialize all rows of content with fg/bg flags', async function(): Promise<any> {
test('serialize all rows of content with fg/bg flags', async function(): Promise<any> {
const cols = 10;
const line = '+'.repeat(cols);
const lines: string[] = [
@@ -204,22 +199,22 @@ describe('SerializeAddon', () => {
sgr(NO_INVISIBLE) + line,
sgr(NO_STRIKETHROUGH) + line
];
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
await ctx.proxy.write(lines.join('\r\n'));
strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n'));
});
it('serialize all rows of content with color256', async function(): Promise<any> {
test('serialize all rows of content with color256', async function(): Promise<any> {
const rows = 32;
const cols = 10;
const lines = newArray<string>(
(index: number) => digitsString(cols, index, `\x1b[38;5;${16 + index}m`),
rows
);
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
await ctx.proxy.write(lines.join('\r\n'));
strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n'));
});
it('serialize all rows of content with overline', async () => {
test('serialize all rows of content with overline', async () => {
const cols = 10;
const line = '+'.repeat(cols);
const lines: string[] = [
@@ -227,11 +222,11 @@ describe('SerializeAddon', () => {
sgr(UNDERLINED) + line, // Overlined, Underlined
sgr(NORMAL) + line // Normal
];
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
await ctx.proxy.write(lines.join('\r\n'));
strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n'));
});
it('serialize all rows of content with color16 and style separately', async function(): Promise<any> {
test('serialize all rows of content with color16 and style separately', async function(): Promise<any> {
const cols = 10;
const line = '+'.repeat(cols);
const lines: string[] = [
@@ -246,11 +241,11 @@ describe('SerializeAddon', () => {
sgr(BG_RESET) + line, // Underlined, Inverse
sgr(NORMAL) + line // Back to normal
];
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
await ctx.proxy.write(lines.join('\r\n'));
strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n'));
});
it('serialize all rows of content with color16 and style together', async function(): Promise<any> {
test('serialize all rows of content with color16 and style together', async function(): Promise<any> {
const cols = 10;
const line = '+'.repeat(cols);
const lines: string[] = [
@@ -268,11 +263,11 @@ describe('SerializeAddon', () => {
sgr(FG_RESET, ITALIC) + line, // bg Yellow, Italic
sgr(BG_RESET) + line // Italic
];
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
await ctx.proxy.write(lines.join('\r\n'));
strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n'));
});
it('serialize all rows of content with color256 and style separately', async function(): Promise<any> {
test('serialize all rows of content with color256 and style separately', async function(): Promise<any> {
const cols = 10;
const line = '+'.repeat(cols);
const lines: string[] = [
@@ -287,11 +282,11 @@ describe('SerializeAddon', () => {
sgr(BG_RESET) + line, // Underlined, Inverse
sgr(NORMAL) + line // Back to normal
];
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
await ctx.proxy.write(lines.join('\r\n'));
strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n'));
});
it('serialize all rows of content with color256 and style together', async function(): Promise<any> {
test('serialize all rows of content with color256 and style together', async function(): Promise<any> {
const cols = 10;
const line = '+'.repeat(cols);
const lines: string[] = [
@@ -309,11 +304,11 @@ describe('SerializeAddon', () => {
sgr(FG_RESET, ITALIC) + line, // bg Yellow 256, Italic
sgr(BG_RESET) + line // Italic
];
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
await ctx.proxy.write(lines.join('\r\n'));
strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n'));
});
it('serialize all rows of content with colorRGB and style separately', async function(): Promise<any> {
test('serialize all rows of content with colorRGB and style separately', async function(): Promise<any> {
const cols = 10;
const line = '+'.repeat(cols);
const lines: string[] = [
@@ -328,11 +323,11 @@ describe('SerializeAddon', () => {
sgr(BG_RESET) + line, // Underlined, Inverse
sgr(NORMAL) + line // Back to normal
];
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
await ctx.proxy.write(lines.join('\r\n'));
strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n'));
});
it('serialize all rows of content with colorRGB and style together', async function(): Promise<any> {
test('serialize all rows of content with colorRGB and style together', async function(): Promise<any> {
const cols = 10;
const line = '+'.repeat(cols);
const lines: string[] = [
@@ -350,11 +345,11 @@ describe('SerializeAddon', () => {
sgr(FG_RESET, ITALIC) + line, // bg Yellow RGB, Italic
sgr(BG_RESET) + line // Italic
];
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
await ctx.proxy.write(lines.join('\r\n'));
strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n'));
});
it('serialize tabs correctly', async () => {
test('serialize tabs correctly', async () => {
const lines = [
'a\tb',
'aa\tc',
@@ -365,11 +360,11 @@ describe('SerializeAddon', () => {
'aa\x1b[6Cc',
'aaa\x1b[5Cd'
];
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), expected.join('\r\n'));
await ctx.proxy.write(lines.join('\r\n'));
strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), expected.join('\r\n'));
});
it('serialize CJK correctly', async () => {
test('serialize CJK correctly', async () => {
const lines = [
'中文中文',
'12中文',
@@ -381,22 +376,22 @@ describe('SerializeAddon', () => {
// see also #3097
'1中文中文中'
];
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
await ctx.proxy.write(lines.join('\r\n'));
strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n'));
});
it('serialize CJK Mixed with tab correctly', async () => {
test('serialize CJK Mixed with tab correctly', async () => {
const lines = [
'中文\t12' // CJK mixed with tab
];
const expected = [
'中文\x1b[4C12'
];
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), expected.join('\r\n'));
await ctx.proxy.write(lines.join('\r\n'));
strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), expected.join('\r\n'));
});
it('serialize with alt screen correctly', async () => {
test('serialize with alt screen correctly', async () => {
const SMCUP = '\u001b[?1049h';
const CUP = '\u001b[H';
@@ -407,12 +402,12 @@ describe('SerializeAddon', () => {
`1${SMCUP}${CUP}2`
];
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`window.term.buffer.active.type`), 'alternate');
assert.equal(JSON.stringify(await page.evaluate(`serializeAddon.serialize();`)), JSON.stringify(expected.join('\r\n')));
await ctx.proxy.write(lines.join('\r\n'));
strictEqual(await ctx.page.evaluate(`window.term.buffer.active.type`), 'alternate');
strictEqual(JSON.stringify(await ctx.page.evaluate(`window.serialize.serialize();`)), JSON.stringify(expected.join('\r\n')));
});
it('serialize without alt screen correctly', async () => {
test('serialize without alt screen correctly', async () => {
const SMCUP = '\u001b[?1049h';
const RMCUP = '\u001b[?1049l';
@@ -423,12 +418,12 @@ describe('SerializeAddon', () => {
`1`
];
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`window.term.buffer.active.type`), 'normal');
assert.equal(JSON.stringify(await page.evaluate(`serializeAddon.serialize();`)), JSON.stringify(expected.join('\r\n')));
await ctx.proxy.write(lines.join('\r\n'));
strictEqual(await ctx.page.evaluate(`window.term.buffer.active.type`), 'normal');
strictEqual(JSON.stringify(await ctx.page.evaluate(`window.serialize.serialize();`)), JSON.stringify(expected.join('\r\n')));
});
it('serialize with background', async () => {
test('serialize with background', async () => {
const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`;
const lines = [
@@ -436,10 +431,10 @@ describe('SerializeAddon', () => {
`2${CLEAR_RIGHT(9)}`
];
await testNormalScreenEqual(page, lines.join('\r\n'));
await testNormalScreenEqual(ctx.page, lines.join('\r\n'));
});
it('cause the BCE on scroll', async () => {
test('cause the BCE on scroll', async () => {
const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`;
const padLines = newArray<string>(
@@ -452,10 +447,10 @@ describe('SerializeAddon', () => {
`\u001b[44m${CLEAR_RIGHT(5)}1111111111111111`
];
await testNormalScreenEqual(page, lines.join('\r\n'));
await testNormalScreenEqual(ctx.page, lines.join('\r\n'));
});
it('handle invalid wrap before scroll', async () => {
test('handle invalid wrap before scroll', async () => {
const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`;
const MOVE_UP = (l: number): string => `\u001b[${l}A`;
const MOVE_DOWN = (l: number): string => `\u001b[${l}B`;
@@ -475,10 +470,10 @@ describe('SerializeAddon', () => {
'1'
];
await testNormalScreenEqual(page, segments.join(''));
await testNormalScreenEqual(ctx.page, segments.join(''));
});
it('handle invalid wrap after scroll', async () => {
test('handle invalid wrap after scroll', async () => {
const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`;
const MOVE_UP = (l: number): string => `\u001b[${l}A`;
const MOVE_DOWN = (l: number): string => `\u001b[${l}B`;
@@ -505,27 +500,27 @@ describe('SerializeAddon', () => {
'1'
];
await testNormalScreenEqual(page, lines.join(''));
await testNormalScreenEqual(ctx.page, lines.join(''));
});
describe('handle modes', () => {
it('applicationCursorKeysMode', async () => {
test.describe('handle modes', () => {
test('applicationCursorKeysMode', async () => {
await testSerializeEquals('test\u001b[?1h', 'test\u001b[?1h');
await testSerializeEquals('\u001b[?1l', 'test');
});
it('applicationKeypadMode', async () => {
test('applicationKeypadMode', async () => {
await testSerializeEquals('test\u001b[?66h', 'test\u001b[?66h');
await testSerializeEquals('\u001b[?66l', 'test');
});
it('bracketedPasteMode', async () => {
test('bracketedPasteMode', async () => {
await testSerializeEquals('test\u001b[?2004h', 'test\u001b[?2004h');
await testSerializeEquals('\u001b[?2004l', 'test');
});
it('insertMode', async () => {
test('insertMode', async () => {
await testSerializeEquals('test\u001b[4h', 'test\u001b[4h');
await testSerializeEquals('\u001b[4l', 'test');
});
it('mouseTrackingMode', async () => {
test('mouseTrackingMode', async () => {
await testSerializeEquals('test\u001b[?9h', 'test\u001b[?9h');
await testSerializeEquals('\u001b[?9l', 'test');
await testSerializeEquals('\u001b[?1000h', 'test\u001b[?1000h');
@@ -535,20 +530,20 @@ describe('SerializeAddon', () => {
await testSerializeEquals('\u001b[?1003h', 'test\u001b[?1003h');
await testSerializeEquals('\u001b[?1003l', 'test');
});
it('originMode', async () => {
test('originMode', async () => {
// origin mode moves cursor to (0,0)
await testSerializeEquals('test\u001b[?6h', 'test\u001b[4D\u001b[?6h');
await testSerializeEquals('\u001b[?6l', 'test\u001b[4D');
});
it('reverseWraparoundMode', async () => {
test('reverseWraparoundMode', async () => {
await testSerializeEquals('test\u001b[?45h', 'test\u001b[?45h');
await testSerializeEquals('\u001b[?45l', 'test');
});
it('sendFocusMode', async () => {
test('sendFocusMode', async () => {
await testSerializeEquals('test\u001b[?1004h', 'test\u001b[?1004h');
await testSerializeEquals('\u001b[?1004l', 'test');
});
it('wraparoundMode', async () => {
test('wraparoundMode', async () => {
await testSerializeEquals('test\u001b[?7l', 'test\u001b[?7l');
await testSerializeEquals('\u001b[?7h', 'test');
});
@@ -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 run start-server-only',
port: 3000,
timeout: 120000,
reuseExistingServer: !process.env.CI
}
};
export default config;
+9 -4
View File
@@ -1,9 +1,9 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2021",
"target": "ESNext",
"lib": [
"es2015"
"es2021",
],
"rootDir": ".",
"outDir": "../out-test",
@@ -13,13 +13,15 @@
"paths": {
"common/*": [
"../../../src/common/*"
],
"browser/*": [
"../../../src/browser/*"
]
},
"strict": true,
"types": [
"../../../node_modules/@types/mocha",
"../../../node_modules/@types/node",
"../../../out-test/api/TestUtils"
"../../../out-test/playwright/TestUtils"
]
},
"include": [
@@ -29,6 +31,9 @@
"references": [
{
"path": "../../../src/common"
},
{
"path": "../../../src/browser"
}
]
}
@@ -1,73 +0,0 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { openTerminal, launchBrowser } from '../../../out-test/api/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('UnicodeGraphemesAddon', () => {
before(async function(): Promise<any> {
browser = await launchBrowser();
page = await (await browser.newContext()).newPage();
await page.setViewportSize({ width, height });
});
after(async () => {
await browser.close();
});
beforeEach(async function(): Promise<any> {
await page.goto(APP);
await openTerminal(page);
});
async function evalWidth(str: string): Promise<number> {
return page.evaluate(`window.term._core.unicodeService.getStringCellWidth('${str}')`);
}
const ourVersion = '15-graphemes';
it('wcwidth V15 emoji test', async () => {
await page.evaluate(`
window.unicode = new UnicodeGraphemesAddon();
window.term.loadAddon(window.unicode);
`);
// should have loaded '15-graphemes'
assert.deepEqual(await page.evaluate(`window.term.unicode.versions`), ['6', '15', '15-graphemes']);
// switch should not throw
await page.evaluate(`window.term.unicode.activeVersion = '${ourVersion}';`);
assert.equal(await page.evaluate(`window.term.unicode.activeVersion`), ourVersion);
assert.equal(await evalWidth('🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣'), 20,
'10 emoji - width 10 in V6; 20 in V11 or later');
assert.equal(await evalWidth('\u{1F476}\u{1F3FF}\u{1F476}'), 4,
'baby with emoji modifier fitzpatrick type-6; baby');
assert.equal(await evalWidth('\u{1F469}\u200d\u{1f469}\u200d\u{1f466}'), 2,
'woman+zwj+woman+zwj+boy');
assert.equal(await evalWidth('=\u{1F3CB}\u{FE0F}=\u{F3CB}\u{1F3FE}\u200D\u2640='), 7,
'person lifting weights (plain, emoji); woman lighting weights, medium dark');
assert.equal(await evalWidth('\u{1F469}\u{1F469}\u{200D}\u{1F393}\u{1F468}\u{1F3FF}\u{200D}\u{1F393}'), 6,
'woman; woman student; man student dark');
assert.equal(await evalWidth('\u{1f1f3}\u{1f1f4}/'), 3,
'regional indicator symbol letters N and O, cluster');
assert.equal(await evalWidth('\u{1f1f3}/\u{1f1f4}'), 3,
'regional indicator symbol letters N and O, separated');
assert.equal(await evalWidth('\u0061\u0301'), 1,
'letter a with acute accent');
assert.equal(await evalWidth('{\u1100\u1161\u11a8\u1100\u1161}'), 6,
'Korean Jamo');
assert.equal(await evalWidth('\uAC00=\uD685='), 6,
'Hangul syllables (pre-composed)');
assert.equal(await evalWidth('(\u26b0\ufe0e)'), 3,
'coffin with text presentation');
assert.equal(await evalWidth('(\u26b0\ufe0f)'), 4,
'coffin with emoji presentation');
assert.equal(await evalWidth('<E\u0301\ufe0fg\ufe0fa\ufe0fl\ufe0fi\ufe0f\ufe0ft\ufe0fe\u0301\ufe0f>'), 16,
'Égalité (using separate acute) emoij_presentation');
});
});
@@ -0,0 +1,65 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import test from '@playwright/test';
import { deepStrictEqual, strictEqual } from 'assert';
import { ITestContext, createTestContext, openTerminal } from '../../../out-test/playwright/TestUtils';
let ctx: ITestContext;
test.beforeAll(async ({ browser }) => {
ctx = await createTestContext(browser);
await openTerminal(ctx);
});
test.afterAll(async () => await ctx.page.close());
test.describe('UnicodeGraphemesAddon', () => {
test.beforeEach(async () => {
await ctx.page.evaluate(`
window.term.reset()
window.unicode?.dispose();
window.unicode = new UnicodeGraphemesAddon();
window.term.loadAddon(window.unicode);
`);
});
async function evalWidth(str: string): Promise<number> {
return ctx.page.evaluate(`window.term._core.unicodeService.getStringCellWidth('${str}')`);
}
const ourVersion = '15-graphemes';
test('wcwidth V15 emoji test', async () => {
// should have loaded '15-graphemes'
deepStrictEqual(await ctx.page.evaluate(`window.term.unicode.versions`), ['6', '15', '15-graphemes']);
// switch should not throw
await ctx.page.evaluate(`window.term.unicode.activeVersion = '${ourVersion}';`);
strictEqual(await ctx.page.evaluate(`window.term.unicode.activeVersion`), ourVersion);
strictEqual(await evalWidth('🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣'), 20,
'10 emoji - width 10 in V6; 20 in V11 or later');
strictEqual(await evalWidth('\u{1F476}\u{1F3FF}\u{1F476}'), 4,
'baby with emoji modifier fitzpatrick type-6; baby');
strictEqual(await evalWidth('\u{1F469}\u200d\u{1f469}\u200d\u{1f466}'), 2,
'woman+zwj+woman+zwj+boy');
strictEqual(await evalWidth('=\u{1F3CB}\u{FE0F}=\u{F3CB}\u{1F3FE}\u200D\u2640='), 7,
'person lifting weights (plain, emoji); woman lighting weights, medium dark');
strictEqual(await evalWidth('\u{1F469}\u{1F469}\u{200D}\u{1F393}\u{1F468}\u{1F3FF}\u{200D}\u{1F393}'), 6,
'woman; woman student; man student dark');
strictEqual(await evalWidth('\u{1f1f3}\u{1f1f4}/'), 3,
'regional indicator symbol letters N and O, cluster');
strictEqual(await evalWidth('\u{1f1f3}/\u{1f1f4}'), 3,
'regional indicator symbol letters N and O, separated');
strictEqual(await evalWidth('\u0061\u0301'), 1,
'letter a with acute accent');
strictEqual(await evalWidth('{\u1100\u1161\u11a8\u1100\u1161}'), 6,
'Korean Jamo');
strictEqual(await evalWidth('\uAC00=\uD685='), 6,
'Hangul syllables (pre-composed)');
strictEqual(await evalWidth('(\u26b0\ufe0e)'), 3,
'coffin with text presentation');
strictEqual(await evalWidth('(\u26b0\ufe0f)'), 4,
'coffin with emoji presentation');
strictEqual(await evalWidth('<E\u0301\ufe0fg\ufe0fa\ufe0fl\ufe0fi\ufe0f\ufe0ft\ufe0fe\u0301\ufe0f>'), 16,
'Égalité (using separate acute) emoij_presentation');
});
});
@@ -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 run start-server-only',
port: 3000,
timeout: 120000,
reuseExistingServer: !process.env.CI
}
};
export default config;
@@ -1,26 +1,27 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2015",
"target": "ESNext",
"lib": [
"dom",
"es2015"
"es2021",
],
"rootDir": ".",
"outDir": "../out-test",
"sourceMap": true,
"removeComments": true,
"strict": true,
"baseUrl": ".",
"paths": {
"common/*": [
"../../../src/common/*"
],
"browser/*": [
"../../../src/browser/*"
]
},
"strict": true,
"types": [
"../../../node_modules/@types/mocha",
"../../../node_modules/@types/node",
"../../../out-test/api/TestUtils"
"../../../out-test/playwright/TestUtils"
]
},
"include": [
@@ -30,6 +31,9 @@
"references": [
{
"path": "../../../src/common"
},
{
"path": "../../../src/browser"
}
]
}
@@ -1,46 +0,0 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { openTerminal, launchBrowser } from '../../../out-test/api/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('Unicode11Addon', () => {
before(async function(): Promise<any> {
browser = await launchBrowser();
page = await (await browser.newContext()).newPage();
await page.setViewportSize({ width, height });
});
after(async () => {
await browser.close();
});
beforeEach(async function(): Promise<any> {
await page.goto(APP);
await openTerminal(page);
});
it('wcwidth V11 emoji test', async () => {
await page.evaluate(`
window.unicode11 = new Unicode11Addon();
window.term.loadAddon(window.unicode11);
`);
// should have loaded '11'
assert.deepEqual((await page.evaluate(`window.term.unicode.versions`) as string[]).includes('11'), true);
// switch should not throw
await page.evaluate(`window.term.unicode.activeVersion = '11';`);
assert.deepEqual(await page.evaluate(`window.term.unicode.activeVersion`), '11');
// v6: 10, V11: 20
assert.deepEqual(await page.evaluate(`window.term._core.unicodeService.getStringCellWidth('🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣')`), 20);
});
});
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import test from '@playwright/test';
import { deepStrictEqual } from 'assert';
import { ITestContext, createTestContext, openTerminal } from '../../../out-test/playwright/TestUtils';
let ctx: ITestContext;
test.beforeAll(async ({ browser }) => {
ctx = await createTestContext(browser);
await openTerminal(ctx);
});
test.afterAll(async () => await ctx.page.close());
test.describe('Unicode11Addon', () => {
test.beforeEach(async () => {
await ctx.page.evaluate(`
window.term.reset()
window.unicode11?.dispose();
window.unicode11 = new Unicode11Addon();
window.term.loadAddon(window.unicode11);
`);
});
test('wcwidth V11 emoji test', async () => {
// should have loaded '11'
deepStrictEqual((await ctx.page.evaluate(`window.term.unicode.versions`) as string[]).includes('11'), true);
// switch should not throw
await ctx.page.evaluate(`window.term.unicode.activeVersion = '11';`);
deepStrictEqual(await ctx.page.evaluate(`window.term.unicode.activeVersion`), '11');
// v6: 10, V11: 20
deepStrictEqual(await ctx.page.evaluate(`window.term._core.unicodeService.getStringCellWidth('🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣')`), 20);
});
});
@@ -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 run start-server-only',
port: 3000,
timeout: 120000,
reuseExistingServer: !process.env.CI
}
};
export default config;
+10 -6
View File
@@ -1,26 +1,27 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2021",
"target": "ESNext",
"lib": [
"dom",
"es2015"
"es2021",
],
"rootDir": ".",
"outDir": "../out-test",
"sourceMap": true,
"removeComments": true,
"strict": true,
"baseUrl": ".",
"paths": {
"common/*": [
"../../../src/common/*"
],
"browser/*": [
"../../../src/browser/*"
]
},
"strict": true,
"types": [
"../../../node_modules/@types/mocha",
"../../../node_modules/@types/node",
"../../../out-test/api/TestUtils"
"../../../out-test/playwright/TestUtils"
]
},
"include": [
@@ -30,6 +31,9 @@
"references": [
{
"path": "../../../src/common"
},
{
"path": "../../../src/browser"
}
]
}
@@ -2,18 +2,11 @@
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { openTerminal, pollFor, writeSync, launchBrowser } from '../../../out-test/api/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;
import test from '@playwright/test';
import { deepStrictEqual, strictEqual } from 'assert';
import { readFile } from 'fs';
import { resolve } from 'path';
import { ITestContext, createTestContext, openTerminal, pollFor, timeout } from '../../../out-test/playwright/TestUtils';
interface ILinkStateData {
uri?: string;
@@ -30,21 +23,20 @@ interface ILinkStateData {
}
describe('WebLinksAddon', () => {
before(async function(): Promise<any> {
browser = await launchBrowser();
page = await (await browser.newContext()).newPage();
await page.setViewportSize({ width, height });
await page.goto(APP);
await openTerminal(page, { cols: 40 });
});
let ctx: ITestContext;
test.beforeAll(async ({ browser }) => {
ctx = await createTestContext(browser);
await openTerminal(ctx, { cols: 40 });
});
test.afterAll(async () => await ctx.page.close());
after(async () => await browser.close());
beforeEach(async () => {
await page.evaluate(`
test.describe('WebLinksAddon', () => {
test.beforeEach(async () => {
await ctx.page.evaluate(`
window.term.reset()
window._linkaddon?.dispose();
window.term.reset();
window._linkaddon = new window.WebLinksAddon();
window._linkaddon = new WebLinksAddon();
window.term.loadAddon(window._linkaddon);
`);
});
@@ -71,36 +63,36 @@ describe('WebLinksAddon', () => {
'.vc', '.ve', '.vg', '.vi', '.vn', '.vu', '.wf', '.ws', '.ye', '.yt', '.za', '.zm', '.zw'
];
for (const tld of countryTlds) {
it(tld, async () => await testHostName(`foo${tld}`));
test(tld, async () => await testHostName(`foo${tld}`));
}
it(`.com`, async () => await testHostName(`foo.com`));
test(`.com`, async () => await testHostName(`foo.com`));
for (const tld of countryTlds) {
it(`.com${tld}`, async () => await testHostName(`foo.com${tld}`));
test(`.com${tld}`, async () => await testHostName(`foo.com${tld}`));
}
describe('correct buffer offsets & uri', () => {
beforeEach(async () => {
await page.evaluate(`
test.describe('correct buffer offsets & uri', () => {
test.beforeEach(async () => {
await ctx.page.evaluate(`
window._linkStateData = {uri:''};
window._linkaddon._options.hover = (event, uri, range) => { window._linkStateData = { uri, range }; };
`);
});
it('all half width', async () => {
await writeSync(page, 'aaa http://example.com aaa http://example.com aaa');
test('all half width', async () => {
await ctx.proxy.write('aaa http://example.com aaa http://example.com aaa');
await resetAndHover(5, 0);
await evalLinkStateData('http://example.com', { start: { x: 5, y: 1 }, end: { x: 22, y: 1 } });
await resetAndHover(1, 1);
await evalLinkStateData('http://example.com', { start: { x: 28, y: 1 }, end: { x: 5, y: 2 } });
});
it('url after full width', async () => {
await writeSync(page, '¥¥¥ http://example.com ¥¥¥ http://example.com aaa');
test('url after full width', async () => {
await ctx.proxy.write('¥¥¥ http://example.com ¥¥¥ http://example.com aaa');
await resetAndHover(8, 0);
await evalLinkStateData('http://example.com', { start: { x: 8, y: 1 }, end: { x: 25, y: 1 } });
await resetAndHover(1, 1);
await evalLinkStateData('http://example.com', { start: { x: 34, y: 1 }, end: { x: 11, y: 2 } });
});
it('full width within url and before', async () => {
await writeSync(page, '¥¥¥ https://ko.wikipedia.org/wiki/위키백과:대문 aaa https://ko.wikipedia.org/wiki/위키백과:대문 ¥¥¥');
test('full width within url and before', async () => {
await ctx.proxy.write('¥¥¥ https://ko.wikipedia.org/wiki/위키백과:대문 aaa https://ko.wikipedia.org/wiki/위키백과:대문 ¥¥¥');
await resetAndHover(8, 0);
await evalLinkStateData('https://ko.wikipedia.org/wiki/위키백과:대문', { start: { x: 8, y: 1 }, end: { x: 11, y: 2 } });
await resetAndHover(1, 1);
@@ -108,15 +100,15 @@ describe('WebLinksAddon', () => {
await resetAndHover(17, 1);
await evalLinkStateData('https://ko.wikipedia.org/wiki/위키백과:대문', { start: { x: 17, y: 2 }, end: { x: 19, y: 3 } });
});
it('name + password url after full width and combining', async () => {
await writeSync(page, '¥¥¥cafe\u0301 http://test:password@example.com/some_path');
test('name + password url after full width and combining', async () => {
await ctx.proxy.write('¥¥¥cafe\u0301 http://test:password@example.com/some_path');
await resetAndHover(12, 0);
await evalLinkStateData('http://test:password@example.com/some_path', { start: { x: 12, y: 1 }, end: { x: 13, y: 2 } });
await resetAndHover(5, 1);
await evalLinkStateData('http://test:password@example.com/some_path', { start: { x: 12, y: 1 }, end: { x: 13, y: 2 } });
});
it('url encoded params work properly', async () => {
await writeSync(page, '¥¥¥cafe\u0301 http://test:password@example.com/some_path?param=1%202%3');
test('url encoded params work properly', async () => {
await ctx.proxy.write('¥¥¥cafe\u0301 http://test:password@example.com/some_path?param=1%202%3');
await resetAndHover(12, 0);
await evalLinkStateData('http://test:password@example.com/some_path?param=1%202%3', { start: { x: 12, y: 1 }, end: { x: 27, y: 2 } });
await resetAndHover(5, 1);
@@ -125,13 +117,14 @@ describe('WebLinksAddon', () => {
});
// issue #4964
it('uppercase in protocol and host, default ports', async () => {
const data = ` HTTP://EXAMPLE.COM \\r\\n` +
` HTTPS://Example.com \\r\\n` +
` HTTP://Example.com:80 \\r\\n` +
` HTTP://Example.com:80/staysUpper \\r\\n` +
` HTTP://Ab:xY@abc.com:80/staysUpper \\r\\n`;
await writeSync(page, data);
test('uppercase in protocol and host, default ports', async () => {
await ctx.proxy.write(
` HTTP://EXAMPLE.COM \r\n` +
` HTTPS://Example.com \r\n` +
` HTTP://Example.com:80 \r\n` +
` HTTP://Example.com:80/staysUpper \r\n` +
` HTTP://Ab:xY@abc.com:80/staysUpper \r\n`
);
await pollForLinkAtCell(3, 0, `HTTP://EXAMPLE.COM`);
await pollForLinkAtCell(3, 1, `HTTPS://Example.com`);
await pollForLinkAtCell(3, 2, `HTTP://Example.com:80`);
@@ -141,14 +134,15 @@ describe('WebLinksAddon', () => {
});
async function testHostName(hostname: string): Promise<void> {
const data = ` http://${hostname} \\r\\n` +
` http://${hostname}/a~b#c~d?e~f \\r\\n` +
` http://${hostname}/colon:test \\r\\n` +
` http://${hostname}/colon:test: \\r\\n` +
`"http://${hostname}/"\\r\\n` +
`\\'http://${hostname}/\\'\\r\\n` +
`http://${hostname}/subpath/+/id`;
await writeSync(page, data);
await ctx.proxy.write(
` http://${hostname} \r\n` +
` http://${hostname}/a~b#c~d?e~f \r\n` +
` http://${hostname}/colon:test \r\n` +
` http://${hostname}/colon:test: \r\n` +
`"http://${hostname}/"\r\n` +
`\'http://${hostname}/\'\r\n` +
`http://${hostname}/subpath/+/id`
);
await pollForLinkAtCell(3, 0, `http://${hostname}`);
await pollForLinkAtCell(3, 1, `http://${hostname}/a~b#c~d?e~f`);
await pollForLinkAtCell(3, 2, `http://${hostname}/colon:test`);
@@ -159,28 +153,28 @@ async function testHostName(hostname: string): Promise<void> {
}
async function pollForLinkAtCell(col: number, row: number, value: string): Promise<void> {
await page.mouse.move(...(await cellPos(col, row)));
await pollFor(page, `!!Array.from(document.querySelectorAll('.xterm-rows > :nth-child(${row+1}) > span[style]')).filter(el => el.style.textDecoration == 'underline').length`, true);
const text = await page.evaluate(`Array.from(document.querySelectorAll('.xterm-rows > :nth-child(${row+1}) > span[style]')).filter(el => el.style.textDecoration == 'underline').map(el => el.textContent).join('');`);
assert.deepEqual(text, value);
await ctx.page.mouse.move(...(await cellPos(col, row)));
await pollFor(ctx.page, `!!Array.from(document.querySelectorAll('.xterm-rows > :nth-child(${row+1}) > span[style]')).filter(el => el.style.textDecoration == 'underline').length`, true);
const text = await ctx.page.evaluate(`Array.from(document.querySelectorAll('.xterm-rows > :nth-child(${row+1}) > span[style]')).filter(el => el.style.textDecoration == 'underline').map(el => el.textContent).join('');`);
deepStrictEqual(text, value);
}
async function resetAndHover(col: number, row: number): Promise<void> {
await page.mouse.move(0, 0);
await page.evaluate(`window._linkStateData = {uri:''};`);
await ctx.page.mouse.move(0, 0);
await ctx.page.evaluate(`window._linkStateData = {uri:''};`);
await new Promise(r => setTimeout(r, 200));
await page.mouse.move(...(await cellPos(col, row)));
await pollFor(page, `!!window._linkStateData.uri.length`, true);
await ctx.page.mouse.move(...(await cellPos(col, row)));
await pollFor(ctx.page, `!!window._linkStateData.uri.length`, true);
}
async function evalLinkStateData(uri: string, range: any): Promise<void> {
const data: ILinkStateData = await page.evaluate(`window._linkStateData`);
assert.equal(data.uri, uri);
assert.deepEqual(data.range, range);
const data: ILinkStateData = await ctx.page.evaluate(`window._linkStateData`);
strictEqual(data.uri, uri);
deepStrictEqual(data.range, range);
}
async function cellPos(col: number, row: number): Promise<[number, number]> {
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;
@@ -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 run start-server-only',
port: 3000,
timeout: 120000,
reuseExistingServer: !process.env.CI
}
};
export default config;
+21 -3
View File
@@ -3,20 +3,38 @@
"module": "commonjs",
"target": "es2021",
"lib": [
"es2015"
"es2021",
],
// "downlevelIteration": true,
"rootDir": ".",
"outDir": "../out-test",
"sourceMap": true,
"removeComments": true,
"baseUrl": ".",
"paths": {
"common/*": [
"../../../src/common/*"
],
"browser/*": [
"../../../src/browser/*"
]
},
"strict": true,
"types": [
"../../../node_modules/@types/mocha",
"../../../node_modules/@types/node"
"../../../node_modules/@types/node",
"../../../out-test/playwright/TestUtils"
]
},
"include": [
"./**/*",
"../../../typings/xterm.d.ts"
],
"references": [
{
"path": "../../../src/common"
},
{
"path": "../../../src/browser"
}
]
}
+2 -2
View File
@@ -5,14 +5,14 @@ const config: PlaywrightTestConfig = {
timeout: 10000,
projects: [
{
name: 'Chrome Stable',
name: 'ChromeStable',
use: {
browserName: 'chromium',
channel: 'chrome'
}
},
{
name: 'Firefox Stable',
name: 'FirefoxStable',
use: {
browserName: 'firefox'
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2021",
"target": "ESNext",
"lib": [
"es2021",
],
+7 -3
View File
@@ -6,7 +6,7 @@
const cp = require('child_process');
const path = require('path');
const COVERAGE_LINES_THRESHOLD = 60;
const COVERAGE_LINES_THRESHOLD = 40;
// Add `out` to the NODE_PATH so absolute paths can be resolved.
const env = { ...process.env };
@@ -31,7 +31,6 @@ if (process.argv.length > 2) {
}
const checkCoverage = flagArgs.indexOf('--coverage') >= 0;
if (checkCoverage) {
flagArgs.splice(flagArgs.indexOf('--coverage'), 1);
const executable = npmBinScript('nyc');
@@ -44,6 +43,7 @@ if (checkCoverage) {
{
cwd: path.resolve(__dirname, '..'),
env,
shell: true,
stdio: 'inherit'
}
);
@@ -56,6 +56,7 @@ const run = cp.spawnSync(
{
cwd: path.resolve(__dirname, '..'),
env,
shell: true,
stdio: 'inherit'
}
);
@@ -64,4 +65,7 @@ function npmBinScript(script) {
return path.resolve(__dirname, `../node_modules/.bin/` + (process.platform === 'win32' ? `${script}.cmd` : script));
}
process.exit(run.status);
if (run.error) {
console.error(run.error);
}
process.exit(run.status ?? -1);
-70
View File
@@ -1,70 +0,0 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
const cp = require('child_process');
const path = require('path');
// Add `out` to the NODE_PATH so absolute paths can be resolved.
const env = { ...process.env };
env.NODE_PATH = path.resolve(__dirname, '../out');
let testFiles = [
'./addons/**/out-test/*api.js',
'./out-test/**/*api.js',
];
let flagArgs = [];
if (process.argv.length > 2) {
const args = process.argv.slice(2);
flagArgs = args.filter(e => e.startsWith('--')).map(arg => arg.split('=')).reduce((arr, val) => arr.concat(val.slice(), []));
console.info(flagArgs);
// ability to inject particular test files via
// yarn test [testFileA testFileB ...]
files = args.filter(e => !e.startsWith('--'));
if (files.length) {
testFiles = files;
}
}
env.DEBUG = flagArgs.indexOf('--debug') >= 0 ? 'debug' : '';
env.PORT = 3001;
const server = cp.spawn('node', ['demo/start'], {
cwd: path.resolve(__dirname, '..'),
env,
stdio: 'pipe'
})
server.stdout.on('data', (data) => {
// await for the server to fully start
if (data.includes("successfully")) {
const run = cp.spawnSync(
npmBinScript('mocha'),
[...testFiles, ...flagArgs], {
cwd: path.resolve(__dirname, '..'),
env,
stdio: 'inherit'
}
);
function npmBinScript(script) {
return path.resolve(__dirname, `../node_modules/.bin/` + (process.platform === 'win32' ?
`${script}.cmd` : script));
}
server.kill();
process.exit(run.status);
}
});
server.stderr.on('data', (data) => {
console.error(data.toString());
});
+17 -5
View File
@@ -18,9 +18,18 @@ while (argv.some(e => e.startsWith('--suite='))) {
}
let configs = [
{ name: 'core', path: 'out-test/playwright/playwright.config.js' },
{ name: 'addon-canvas', path: 'addons/addon-canvas/out-test/playwright.config.js' },
{ name: 'addon-webgl', path: 'addons/addon-webgl/out-test/playwright.config.js' }
{ name: 'core', path: 'out-test/playwright/playwright.config.js' },
{ name: 'addon-attach', path: 'addons/addon-attach/out-test/playwright.config.js' },
{ name: 'addon-canvas', path: 'addons/addon-canvas/out-test/playwright.config.js' },
{ name: 'addon-clipboard', path: 'addons/addon-clipboard/out-test/playwright.config.js' },
{ name: 'addon-fit', path: 'addons/addon-fit/out-test/playwright.config.js' },
{ name: 'addon-image', path: 'addons/addon-image/out-test/playwright.config.js' },
{ name: 'addon-search', path: 'addons/addon-search/out-test/playwright.config.js' },
{ name: 'addon-serialize', path: 'addons/addon-serialize/out-test/playwright.config.js' },
{ name: 'addon-unicode-graphemes', path: 'addons/addon-unicode-graphemes/out-test/playwright.config.js' },
{ name: 'addon-unicode11', path: 'addons/addon-unicode11/out-test/playwright.config.js' },
{ name: 'addon-web-links', path: 'addons/addon-web-links/out-test/playwright.config.js' },
{ name: 'addon-webgl', path: 'addons/addon-webgl/out-test/playwright.config.js' }
];
if (suiteFilter) {
@@ -40,12 +49,15 @@ async function run() {
console.log(`\n\x1b[32m${command}\x1b[0m`, args);
const run = cp.spawnSync(command, args, {
cwd: path.resolve(__dirname, '..'),
shell: true,
stdio: 'inherit'
}
);
if (run.status) {
process.exit(run.status);
if (run.error) {
console.error(run.error);
}
process.exit(run.status ?? -1);
}
}
run();
+6
View File
@@ -157,10 +157,16 @@
}
.xterm .xterm-accessibility-tree {
font-family: monospace;
user-select: text;
white-space: pre;
}
.xterm .xterm-accessibility-tree > div {
transform-origin: left;
width: fit-content;
}
.xterm .live-region {
position: absolute;
left: -9999px;
+7 -10
View File
@@ -34,15 +34,11 @@
"lint-api": "eslint --no-eslintrc -c .eslintrc.json.typings --max-warnings 0 --no-ignore --ext .d.ts typings/",
"test": "npm run test-unit",
"posttest": "npm run lint",
"test-api": "npm run test-api-chromium",
"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=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-integration": "node ./bin/test_playwright.js --workers=75%",
"test-integration-chromium": "node ./bin/test_playwright.js --workers=75% \"--project=ChromeStable\"",
"test-integration-firefox": "node ./bin/test_playwright.js --workers=75% \"--project=FirefoxStable\"",
"test-integration-webkit": "node ./bin/test_playwright.js --workers=75% \"--project=WebKit\"",
"test-integration-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",
@@ -61,6 +57,7 @@
"devDependencies": {
"@lunapaint/png-codec": "^0.2.0",
"@playwright/test": "^1.37.1",
"@stylistic/eslint-plugin": "^2.3.0",
"@types/chai": "^4.2.22",
"@types/debug": "^4.1.7",
"@types/deep-equal": "^1.0.1",
@@ -91,7 +88,7 @@
"source-map-loader": "^3.0.0",
"source-map-support": "^0.5.20",
"ts-loader": "^9.3.1",
"typescript": "^5.1.6",
"typescript": "5.5",
"utf8": "^3.0.0",
"webpack": "^5.61.0",
"webpack-cli": "^4.9.1",
+38 -10
View File
@@ -58,10 +58,11 @@ export class AccessibilityManager extends Disposable {
@IRenderService private readonly _renderService: IRenderService
) {
super();
this._accessibilityContainer = this._coreBrowserService.mainDocument.createElement('div');
const doc = this._coreBrowserService.mainDocument;
this._accessibilityContainer = doc.createElement('div');
this._accessibilityContainer.classList.add('xterm-accessibility');
this._rowContainer = this._coreBrowserService.mainDocument.createElement('div');
this._rowContainer = doc.createElement('div');
this._rowContainer.setAttribute('role', 'list');
this._rowContainer.classList.add('xterm-accessibility-tree');
this._rowElements = [];
@@ -75,10 +76,9 @@ export class AccessibilityManager extends Disposable {
this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
this._refreshRowsDimensions();
this._accessibilityContainer.appendChild(this._rowContainer);
this._liveRegion = this._coreBrowserService.mainDocument.createElement('div');
this._liveRegion = doc.createElement('div');
this._liveRegion.classList.add('live-region');
this._liveRegion.setAttribute('aria-live', 'assertive');
this._accessibilityContainer.appendChild(this._liveRegion);
@@ -93,12 +93,12 @@ export class AccessibilityManager extends Disposable {
this._rowContainer.classList.add('debug');
// Use a `<div class="xterm">` container so that the css will still apply.
this._debugRootContainer = document.createElement('div');
this._debugRootContainer = doc.createElement('div');
this._debugRootContainer.classList.add('xterm');
this._debugRootContainer.appendChild(document.createTextNode('------start a11y------'));
this._debugRootContainer.appendChild(doc.createTextNode('------start a11y------'));
this._debugRootContainer.appendChild(this._accessibilityContainer);
this._debugRootContainer.appendChild(document.createTextNode('------end a11y------'));
this._debugRootContainer.appendChild(doc.createTextNode('------end a11y------'));
this._terminal.element.insertAdjacentElement('afterend', this._debugRootContainer);
} else {
@@ -115,9 +115,10 @@ export class AccessibilityManager extends Disposable {
this.register(this._terminal.onKey(e => this._handleKey(e.key)));
this.register(this._terminal.onBlur(() => this._clearLiveRegion()));
this.register(this._renderService.onDimensionsChange(() => this._refreshRowsDimensions()));
this.register(addDisposableDomListener(document, 'selectionchange', () => this._handleSelectionChange()));
this.register(addDisposableDomListener(doc, 'selectionchange', () => this._handleSelectionChange()));
this.register(this._coreBrowserService.onDprChange(() => this._refreshRowsDimensions()));
this._refreshRowsDimensions();
this._refreshRows();
this.register(toDisposable(() => {
if (DEBUG) {
@@ -192,6 +193,7 @@ export class AccessibilityManager extends Disposable {
}
element.setAttribute('aria-posinset', posInSet);
element.setAttribute('aria-setsize', setSize);
this._alignRowWidth(element);
}
}
this._announceCharacters();
@@ -270,7 +272,7 @@ export class AccessibilityManager extends Disposable {
return;
}
const selection = document.getSelection();
const selection = this._coreBrowserService.mainDocument.getSelection();
if (!selection) {
return;
}
@@ -389,19 +391,45 @@ export class AccessibilityManager extends Disposable {
this._refreshRowDimensions(element);
return element;
}
private _refreshRowsDimensions(): void {
if (!this._renderService.dimensions.css.cell.height) {
return;
}
this._accessibilityContainer.style.width = `${this._renderService.dimensions.css.canvas.width}px`;
Object.assign(this._accessibilityContainer.style, {
width: `${this._renderService.dimensions.css.canvas.width}px`,
fontSize: `${this._terminal.options.fontSize}px`
});
if (this._rowElements.length !== this._terminal.rows) {
this._handleResize(this._terminal.rows);
}
for (let i = 0; i < this._terminal.rows; i++) {
this._refreshRowDimensions(this._rowElements[i]);
this._alignRowWidth(this._rowElements[i]);
}
}
private _refreshRowDimensions(element: HTMLElement): void {
element.style.height = `${this._renderService.dimensions.css.cell.height}px`;
}
/**
* Scale the width of a row so that each of the character is (mostly) aligned
* with the actual rendering. This will allow the screen reader to draw
* selection outline at the correct position.
*
* On top of using the "monospace" font and correct font size, the scaling
* here is necessary to handle characters that are not covered by the font
* (e.g. CJK).
*/
private _alignRowWidth(element: HTMLElement): void {
element.style.transform = '';
const width = element.getBoundingClientRect().width;
const lastColumn = this._rowColumns.get(element)?.slice(-1)?.[0];
if (!lastColumn) {
return;
}
const targetWidth = lastColumn * this._renderService.dimensions.css.cell.width;
element.style.transform = `scaleX(${targetWidth / width})`;
}
}
@@ -355,6 +355,12 @@ const enum VectorType {
* Original symbols defined in https://github.com/powerline/fontpatcher
*/
export const powerlineDefinitions: { [index: string]: IVectorShape } = {
// Git branch
'\u{E0A0}': { d: 'M.3,1 L.03,1 L.03,.88 C.03,.82,.06,.78,.11,.73 C.15,.7,.2,.68,.28,.65 L.43,.6 C.49,.58,.53,.56,.56,.53 C.59,.5,.6,.47,.6,.43 L.6,.27 L.4,.27 L.69,.1 L.98,.27 L.78,.27 L.78,.46 C.78,.52,.76,.56,.72,.61 C.68,.66,.63,.67,.56,.7 L.48,.72 C.42,.74,.38,.76,.35,.78 C.32,.8,.31,.84,.31,.88 L.31,1 M.3,.5 L.03,.59 L.03,.09 L.3,.09 L.3,.655', type: VectorType.FILL },
// L N
'\u{E0A1}': { d: 'M.7,.4 L.7,.47 L.2,.47 L.2,.03 L.355,.03 L.355,.4 L.705,.4 M.7,.5 L.86,.5 L.86,.95 L.69,.95 L.44,.66 L.46,.86 L.46,.95 L.3,.95 L.3,.49 L.46,.49 L.71,.78 L.69,.565 L.69,.5', type: VectorType.FILL },
// Lock
'\u{E0A2}': { d: 'M.25,.94 C.16,.94,.11,.92,.11,.87 L.11,.53 C.11,.48,.15,.455,.23,.45 L.23,.3 C.23,.25,.26,.22,.31,.19 C.36,.16,.43,.15,.51,.15 C.59,.15,.66,.16,.71,.19 C.77,.22,.79,.26,.79,.3 L.79,.45 C.87,.45,.91,.48,.91,.53 L.91,.87 C.91,.92,.86,.94,.77,.94 L.24,.94 M.53,.2 C.49,.2,.45,.21,.42,.23 C.39,.25,.38,.27,.38,.3 L.38,.45 L.68,.45 L.68,.3 C.68,.27,.67,.25,.64,.23 C.61,.21,.58,.2,.53,.2 M.58,.82 L.58,.66 C.63,.65,.65,.63,.65,.6 C.65,.58,.64,.57,.61,.56 C.58,.55,.56,.54,.52,.54 C.48,.54,.46,.55,.43,.56 C.4,.57,.39,.59,.39,.6 C.39,.63,.41,.64,.46,.66 L.46,.82 L.57,.82', type: VectorType.FILL },
// Right triangle solid
'\u{E0B0}': { d: 'M0,0 L1,.5 L0,1', type: VectorType.FILL, rightPadding: 2 },
// Right triangle line
+6 -11
View File
@@ -20,23 +20,18 @@ export interface IEventEmitter<T, U = void> {
}
export class EventEmitter<T, U = void> implements IEventEmitter<T, U> {
private _listeners: IListener<T, U>[] = [];
private _listeners: Set<IListener<T, U>> = new Set();
private _event?: IEvent<T, U>;
private _disposed: boolean = false;
public get event(): IEvent<T, U> {
if (!this._event) {
this._event = (listener: (arg1: T, arg2: U) => any) => {
this._listeners.push(listener);
this._listeners.add(listener);
const disposable = {
dispose: () => {
if (!this._disposed) {
for (let i = 0; i < this._listeners.length; i++) {
if (this._listeners[i] === listener) {
this._listeners.splice(i, 1);
return;
}
}
this._listeners.delete(listener);
}
}
};
@@ -48,8 +43,8 @@ export class EventEmitter<T, U = void> implements IEventEmitter<T, U> {
public fire(arg1: T, arg2: U): void {
const queue: IListener<T, U>[] = [];
for (let i = 0; i < this._listeners.length; i++) {
queue.push(this._listeners[i]);
for (const l of this._listeners.values()) {
queue.push(l);
}
for (let i = 0; i < queue.length; i++) {
queue[i].call(undefined, arg1, arg2);
@@ -63,7 +58,7 @@ export class EventEmitter<T, U = void> implements IEventEmitter<T, U> {
public clearListeners(): void {
if (this._listeners) {
this._listeners.length = 0;
this._listeners.clear();
}
}
}
+1 -1
View File
@@ -611,8 +611,8 @@ export class Buffer implements IBuffer {
this._isClearing = true;
for (let i = 0; i < this.markers.length; i++) {
this.markers[i].dispose();
this.markers.splice(i--, 1);
}
this.markers.length = 0;
this._isClearing = false;
}
-1
View File
@@ -1 +0,0 @@
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.
-90
View File
@@ -1,90 +0,0 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import * as playwright from '@playwright/test';
import deepEqual = require('deep-equal');
import { ITerminalInitOnlyOptions, ITerminalOptions } from '@xterm/xterm';
import { deepStrictEqual } from 'assert';
export async function pollFor<T>(page: playwright.Page, evalOrFn: string | (() => Promise<T>), val: T, preFn?: () => Promise<void>, maxDuration?: number): Promise<void> {
if (preFn) {
await preFn();
}
const result = typeof evalOrFn === 'string' ? await page.evaluate(evalOrFn) : await evalOrFn();
if (process.env.DEBUG) {
console.log('pollFor result: ', result);
}
if (!deepEqual(result, val)) {
if (maxDuration === undefined) {
maxDuration = 2000;
}
if (maxDuration <= 0) {
deepStrictEqual(result, val, 'pollFor max duration exceeded');
}
return new Promise<void>(r => {
setTimeout(() => r(pollFor(page, evalOrFn, val, preFn, maxDuration! - 10)), 10);
});
}
}
export async function writeSync(page: playwright.Page, data: string): Promise<void> {
await page.evaluate(`
window.ready = false;
window.term.write('${data}', () => window.ready = true);
`);
await pollFor(page, 'window.ready', true);
}
export async function timeout(ms: number): Promise<void> {
return new Promise<void>(r => setTimeout(r, ms));
}
export async function openTerminal(page: playwright.Page, options: ITerminalOptions & ITerminalInitOnlyOptions = {}, testOptions: { loadUnicodeGraphemesAddon: boolean } = { loadUnicodeGraphemesAddon: true }): Promise<void> {
await page.evaluate(`window.term = new Terminal(${JSON.stringify({ allowProposedApi: true, ...options })})`);
await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
// HACK: This is a soft layer breaker that's temporarily included until unicode graphemes have
// more complete integration tests. See https://github.com/xtermjs/xterm.js/pull/4519#discussion_r1285234453
if (testOptions.loadUnicodeGraphemesAddon) {
await page.evaluate(`
window.unicode = new UnicodeGraphemesAddon();
window.term.loadAddon(window.unicode);
window.term.unicode.activeVersion = '15-graphemes';
`);
}
await page.waitForSelector('.xterm-rows');
}
export function getBrowserType(): playwright.BrowserType<playwright.WebKitBrowser> | playwright.BrowserType<playwright.ChromiumBrowser> | playwright.BrowserType<playwright.FirefoxBrowser> {
// Default to chromium
let browserType: playwright.BrowserType<playwright.WebKitBrowser> | playwright.BrowserType<playwright.ChromiumBrowser> | playwright.BrowserType<playwright.FirefoxBrowser> = playwright['chromium'];
const index = process.argv.indexOf('--browser');
if (index !== -1 && process.argv.length > index + 1 && typeof process.argv[index + 1] === 'string') {
const string = process.argv[index + 1];
if (string === 'firefox' || string === 'webkit') {
browserType = playwright[string];
}
}
return browserType;
}
export function launchBrowser(opts?: playwright.LaunchOptions): Promise<playwright.Browser> {
const browserType = getBrowserType();
const options: playwright.LaunchOptions = {
...opts,
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);
}
-34
View File
@@ -1,34 +0,0 @@
{
"compilerOptions": {
"lib": [
"dom",
"es2021"
],
"rootDir": ".",
"outDir": "../../out-test/api",
"types": [
"../../node_modules/@types/mocha",
"../../node_modules/@types/node"
],
"sourceMap": true,
"removeComments": true,
"pretty": true,
"strict": true,
"declaration": true,
"baseUrl": ".",
"paths": {
"browser/*": [
"../../src/browser/*"
]
}
},
"include": [
"./**/*",
"../../typings/xterm.d.ts"
],
"references": [
{
"path": "../../src/browser"
}
]
}
+2 -2
View File
@@ -5,14 +5,14 @@ const config: PlaywrightTestConfig = {
timeout: 10000,
projects: [
{
name: 'Chrome Stable',
name: 'ChromeStable',
use: {
browserName: 'chromium',
channel: 'chrome'
}
},
{
name: 'Firefox Stable',
name: 'FirefoxStable',
use: {
browserName: 'firefox'
}
+1 -1
View File
@@ -24,7 +24,7 @@
"../../src/common/*"
]
},
"target": "es2016",
"target": "ESNext",
"module": "commonjs",
},
"include": [
-1
View File
@@ -4,7 +4,6 @@
"references": [
{ "path": "./src/browser" },
{ "path": "./src/headless" },
{ "path": "./test/api" },
{ "path": "./test/benchmark" },
{ "path": "./test/playwright" },
{ "path": "./addons/addon-attach" },
+151 -9
View File
@@ -429,6 +429,54 @@
optionalDependencies:
fsevents "2.3.2"
"@stylistic/eslint-plugin-js@2.3.0", "@stylistic/eslint-plugin-js@^2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin-js/-/eslint-plugin-js-2.3.0.tgz#a3faee05863c50c0bb6f879db72b7ee895bfa74e"
integrity sha512-lQwoiYb0Fs6Yc5QS3uT8+T9CPKK2Eoxc3H8EnYJgM26v/DgtW+1lvy2WNgyBflU+ThShZaHm3a6CdD9QeKx23w==
dependencies:
"@types/eslint" "^8.56.10"
acorn "^8.11.3"
eslint-visitor-keys "^4.0.0"
espree "^10.0.1"
"@stylistic/eslint-plugin-jsx@2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin-jsx/-/eslint-plugin-jsx-2.3.0.tgz#f1a01b6dcdf3d6159727eef6ae298107facdb098"
integrity sha512-tsQ0IEKB195H6X9A4iUSgLLLKBc8gUBWkBIU8tp1/3g2l8stu+PtMQVV/VmK1+3bem5FJCyvfcZIQ/WF1fsizA==
dependencies:
"@stylistic/eslint-plugin-js" "^2.3.0"
"@types/eslint" "^8.56.10"
estraverse "^5.3.0"
picomatch "^4.0.2"
"@stylistic/eslint-plugin-plus@2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin-plus/-/eslint-plugin-plus-2.3.0.tgz#0ccadea6cb52c7ecb9af61b6f27077ba885ba145"
integrity sha512-xboPWGUU5yaPlR+WR57GwXEuY4PSlPqA0C3IdNA/+1o2MuBi95XgDJcZiJ9N+aXsqBXAPIpFFb+WQ7QEHo4f7g==
dependencies:
"@types/eslint" "^8.56.10"
"@typescript-eslint/utils" "^7.12.0"
"@stylistic/eslint-plugin-ts@2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin-ts/-/eslint-plugin-ts-2.3.0.tgz#2c9e047304df2094124a638b273ac02410cc98f1"
integrity sha512-wqOR38/uz/0XPnHX68ftp8sNMSAqnYGjovOTN7w00xnjS6Lxr3Sk7q6AaxWWqbMvOj7V2fQiMC5HWAbTruJsCg==
dependencies:
"@stylistic/eslint-plugin-js" "2.3.0"
"@types/eslint" "^8.56.10"
"@typescript-eslint/utils" "^7.12.0"
"@stylistic/eslint-plugin@^2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@stylistic/eslint-plugin/-/eslint-plugin-2.3.0.tgz#e9b411524d94a120dc757c2bc79919088f7385f6"
integrity sha512-rtiz6u5gRyyEZp36FcF1/gHJbsbT3qAgXZ1qkad6Nr/xJ9wrSJkiSFFQhpYVTIZ7FJNRJurEcumZDCwN9dEI4g==
dependencies:
"@stylistic/eslint-plugin-js" "2.3.0"
"@stylistic/eslint-plugin-jsx" "2.3.0"
"@stylistic/eslint-plugin-plus" "2.3.0"
"@stylistic/eslint-plugin-ts" "2.3.0"
"@types/eslint" "^8.56.10"
"@tootallnate/once@2":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
@@ -492,6 +540,14 @@
"@types/estree" "*"
"@types/json-schema" "*"
"@types/eslint@^8.56.10":
version "8.56.10"
resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.56.10.tgz#eb2370a73bf04a901eeba8f22595c7ee0f7eb58d"
integrity sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==
dependencies:
"@types/estree" "*"
"@types/json-schema" "*"
"@types/estree@*", "@types/estree@^1.0.0":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194"
@@ -712,6 +768,14 @@
"@typescript-eslint/types" "6.2.0"
"@typescript-eslint/visitor-keys" "6.2.0"
"@typescript-eslint/scope-manager@7.15.0":
version "7.15.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.15.0.tgz#201b34b0720be8b1447df17b963941bf044999b2"
integrity sha512-Q/1yrF/XbxOTvttNVPihxh1b9fxamjEoz2Os/Pe38OHwxC24CyCqXxGTOdpb4lt6HYtqw9HetA/Rf6gDGaMPlw==
dependencies:
"@typescript-eslint/types" "7.15.0"
"@typescript-eslint/visitor-keys" "7.15.0"
"@typescript-eslint/type-utils@6.2.0":
version "6.2.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.2.0.tgz#02b27a3eeb41aa5460d6275d12cce5dd72e1c9fc"
@@ -727,6 +791,11 @@
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.2.0.tgz#b341a4e6d5f609267306b07afc6f62bcf92b1495"
integrity sha512-1nRRaDlp/XYJQLvkQJG5F3uBTno5SHPT7XVcJ5n1/k2WfNI28nJsvLakxwZRNY5spuatEKO7d5nZWsQpkqXwBA==
"@typescript-eslint/types@7.15.0":
version "7.15.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.15.0.tgz#fb894373a6e3882cbb37671ffddce44f934f62fc"
integrity sha512-aV1+B1+ySXbQH0pLK0rx66I3IkiZNidYobyfn0WFsdGhSXw+P3YOqeTq5GED458SfB24tg+ux3S+9g118hjlTw==
"@typescript-eslint/typescript-estree@6.2.0":
version "6.2.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.2.0.tgz#4969944b831b481996aa4fbd73c7164ca683b8ef"
@@ -740,6 +809,20 @@
semver "^7.5.4"
ts-api-utils "^1.0.1"
"@typescript-eslint/typescript-estree@7.15.0":
version "7.15.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.15.0.tgz#e323bfa3966e1485b638ce751f219fc1f31eba37"
integrity sha512-gjyB/rHAopL/XxfmYThQbXbzRMGhZzGw6KpcMbfe8Q3nNQKStpxnUKeXb0KiN/fFDR42Z43szs6rY7eHk0zdGQ==
dependencies:
"@typescript-eslint/types" "7.15.0"
"@typescript-eslint/visitor-keys" "7.15.0"
debug "^4.3.4"
globby "^11.1.0"
is-glob "^4.0.3"
minimatch "^9.0.4"
semver "^7.6.0"
ts-api-utils "^1.3.0"
"@typescript-eslint/utils@6.2.0":
version "6.2.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.2.0.tgz#606a20e5c13883c2d2bd0538ddc4b96b8d410979"
@@ -753,6 +836,16 @@
"@typescript-eslint/typescript-estree" "6.2.0"
semver "^7.5.4"
"@typescript-eslint/utils@^7.12.0":
version "7.15.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.15.0.tgz#9e6253c4599b6e7da2fb64ba3f549c73eb8c1960"
integrity sha512-hfDMDqaqOqsUVGiEPSMLR/AjTSCsmJwjpKkYQRo1FNbmW4tBwBspYDwO9eh7sKSTwMQgBw9/T4DHudPaqshRWA==
dependencies:
"@eslint-community/eslint-utils" "^4.4.0"
"@typescript-eslint/scope-manager" "7.15.0"
"@typescript-eslint/types" "7.15.0"
"@typescript-eslint/typescript-estree" "7.15.0"
"@typescript-eslint/visitor-keys@6.2.0":
version "6.2.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.2.0.tgz#71943f42fdaa2ec86dc3222091f41761a49ae71a"
@@ -761,6 +854,14 @@
"@typescript-eslint/types" "6.2.0"
eslint-visitor-keys "^3.4.1"
"@typescript-eslint/visitor-keys@7.15.0":
version "7.15.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.15.0.tgz#1da0726201a859343fe6a05742a7c1792fff5b66"
integrity sha512-Hqgy/ETgpt2L5xueA/zHHIl4fJI2O4XUE9l4+OIfbJIRSnTJb/QscncdqqZzofQegIJugRIF57OJea1khw2SDw==
dependencies:
"@typescript-eslint/types" "7.15.0"
eslint-visitor-keys "^3.4.3"
"@ungap/structured-clone@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
@@ -955,6 +1056,11 @@ acorn@^7.1.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
acorn@^8.11.3, acorn@^8.12.0:
version "8.12.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248"
integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==
acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.2, acorn@^8.9.0:
version "8.10.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5"
@@ -1727,6 +1833,11 @@ eslint-visitor-keys@^3.4.3:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
eslint-visitor-keys@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz#e3adc021aa038a2a8e0b2f8b0ce8f66b9483b1fb"
integrity sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==
eslint@^8.56.0:
version "8.56.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.56.0.tgz#4957ce8da409dc0809f99ab07a1b94832ab74b15"
@@ -1771,6 +1882,15 @@ eslint@^8.56.0:
strip-ansi "^6.0.1"
text-table "^0.2.0"
espree@^10.0.1:
version "10.1.0"
resolved "https://registry.yarnpkg.com/espree/-/espree-10.1.0.tgz#8788dae611574c0f070691f522e4116c5a11fc56"
integrity sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==
dependencies:
acorn "^8.12.0"
acorn-jsx "^5.3.2"
eslint-visitor-keys "^4.0.0"
espree@^9.6.0, espree@^9.6.1:
version "9.6.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f"
@@ -1804,7 +1924,7 @@ estraverse@^4.1.1:
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
estraverse@^5.1.0, estraverse@^5.2.0:
estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
@@ -2861,6 +2981,13 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
dependencies:
brace-expansion "^1.1.7"
minimatch@^9.0.4:
version "9.0.5"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
dependencies:
brace-expansion "^2.0.1"
mocha@^10.1.0:
version "10.2.0"
resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.2.0.tgz#1fd4a7c32ba5ac372e03a17eef435bd00e5c68b8"
@@ -3171,6 +3298,11 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
picomatch@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab"
integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==
pkg-dir@^4.1.0, pkg-dir@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
@@ -3390,6 +3522,11 @@ semver@^7.3.4, semver@^7.5.3, semver@^7.5.4:
dependencies:
lru-cache "^6.0.0"
semver@^7.6.0:
version "7.6.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13"
integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==
send@0.18.0:
version "0.18.0"
resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be"
@@ -3696,6 +3833,11 @@ ts-api-utils@^1.0.1:
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.1.tgz#8144e811d44c749cd65b2da305a032510774452d"
integrity sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==
ts-api-utils@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1"
integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==
ts-loader@^9.3.1:
version "9.4.4"
resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-9.4.4.tgz#6ceaf4d58dcc6979f84125335904920884b7cee4"
@@ -3748,16 +3890,16 @@ typedarray-to-buffer@^3.1.5:
dependencies:
is-typedarray "^1.0.0"
typescript@5.5:
version "5.5.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.3.tgz#e1b0a3c394190838a0b168e771b0ad56a0af0faa"
integrity sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==
typescript@^4.2.3:
version "4.9.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
typescript@^5.1.6:
version "5.1.6"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274"
integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==
universalify@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0"
@@ -4027,9 +4169,9 @@ ws@^7.4.6:
integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
ws@^8.2.3:
version "8.13.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"
integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==
version "8.17.1"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b"
integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==
xml-name-validator@^4.0.0:
version "4.0.0"