From 1624854bd7ebc572d6c467ec1ca4d8c9f4d01d49 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 2 Sep 2023 08:18:02 -0700 Subject: [PATCH] Migrate Terminal api tests to playwright --- src/browser/Viewport.ts | 1 + test/api/Terminal.api.ts | 1062 -------------------------- test/playwright/InputHandler.test.ts | 5 +- test/playwright/Terminal.test.ts | 1053 +++++++++++++++++++++++++ test/playwright/TestUtils.ts | 19 +- 5 files changed, 1069 insertions(+), 1071 deletions(-) delete mode 100644 test/api/Terminal.api.ts create mode 100644 test/playwright/Terminal.test.ts diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index c6e8732f..1611f54b 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -118,6 +118,7 @@ export class Viewport extends Disposable implements IViewport { } private _innerRefresh(): void { + debugger; if (this._charSizeService.height > 0) { this._currentRowHeight = this._renderService.dimensions.device.cell.height / this._coreBrowserService.dpr; this._currentDeviceCellHeight = this._renderService.dimensions.device.cell.height; diff --git a/test/api/Terminal.api.ts b/test/api/Terminal.api.ts deleted file mode 100644 index d0ba1dd6..00000000 --- a/test/api/Terminal.api.ts +++ /dev/null @@ -1,1062 +0,0 @@ -/** - * Copyright (c) 2019 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { assert } from 'chai'; -import { pollFor, timeout, writeSync, openTerminal, launchBrowser } from './TestUtils'; -import { Browser, Page } from '@playwright/test'; -import { fail } from 'assert'; -import { IRenderDimensions } from 'browser/renderer/shared/Types'; - -const APP = 'http://127.0.0.1:3001/test'; - -let browser: Browser; -let page: Page; -const width = 800; -const height = 600; - -describe('API Integration Tests', function(): void { - before(async () => { - browser = await launchBrowser(); - page = await (await browser.newContext()).newPage(); - await page.setViewportSize({ width, height }); - }); - - after(async () => browser.close()); - beforeEach(async () => page.goto(APP)); - - it('Default options', async () => { - await openTerminal(page); - assert.equal(await page.evaluate(`window.term.cols`), 80); - assert.equal(await page.evaluate(`window.term.rows`), 24); - }); - - it('Proposed API check', async () => { - await openTerminal(page, { allowProposedApi: false }); - await page.evaluate(` - try { - window.term.markers; - } catch (e) { - window.throwMessage = e.message; - } - `); - await pollFor(page, 'window.throwMessage', 'You must set the allowProposedApi option to true to use proposed API'); - }); - - it('write', async () => { - await openTerminal(page); - await page.evaluate(` - window.term.write('foo'); - window.term.write('bar'); - window.term.write('文'); - `); - await pollFor(page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foobar文'); - }); - - it('write with callback', async () => { - await openTerminal(page); - await page.evaluate(` - window.term.write('foo', () => { window.__x = 'a'; }); - window.term.write('bar', () => { window.__x += 'b'; }); - window.term.write('文', () => { window.__x += 'c'; }); - `); - await pollFor(page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foobar文'); - await pollFor(page, `window.__x`, 'abc'); - }); - - it('write - bytes (UTF8)', async () => { - await openTerminal(page); - await page.evaluate(` - window.term.write(new Uint8Array([102, 111, 111])); // foo - window.term.write(new Uint8Array([98, 97, 114])); // bar - window.term.write(new Uint8Array([230, 150, 135])); // 文 - `); - await pollFor(page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foobar文'); - }); - - it('write - bytes (UTF8) with callback', async () => { - await openTerminal(page); - await page.evaluate(` - window.term.write(new Uint8Array([102, 111, 111]), () => { window.__x = 'A'; }); // foo - window.term.write(new Uint8Array([98, 97, 114]), () => { window.__x += 'B'; }); // bar - window.term.write(new Uint8Array([230, 150, 135]), () => { window.__x += 'C'; }); // 文 - `); - await pollFor(page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foobar文'); - await pollFor(page, `window.__x`, 'ABC'); - }); - - it('writeln', async () => { - await openTerminal(page); - await page.evaluate(` - window.term.writeln('foo'); - window.term.writeln('bar'); - window.term.writeln('文'); - `); - await pollFor(page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foo'); - await pollFor(page, `window.term.buffer.active.getLine(1).translateToString(true)`, 'bar'); - await pollFor(page, `window.term.buffer.active.getLine(2).translateToString(true)`, '文'); - }); - - it('writeln with callback', async () => { - await openTerminal(page); - await page.evaluate(` - window.term.writeln('foo', () => { window.__x = '1'; }); - window.term.writeln('bar', () => { window.__x += '2'; }); - window.term.writeln('文', () => { window.__x += '3'; }); - `); - await pollFor(page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foo'); - await pollFor(page, `window.term.buffer.active.getLine(1).translateToString(true)`, 'bar'); - await pollFor(page, `window.term.buffer.active.getLine(2).translateToString(true)`, '文'); - await pollFor(page, `window.__x`, '123'); - }); - - it('writeln - bytes (UTF8)', async () => { - await openTerminal(page); - await page.evaluate(` - window.term.writeln(new Uint8Array([102, 111, 111])); - window.term.writeln(new Uint8Array([98, 97, 114])); - window.term.writeln(new Uint8Array([230, 150, 135])); - `); - await pollFor(page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foo'); - await pollFor(page, `window.term.buffer.active.getLine(1).translateToString(true)`, 'bar'); - await pollFor(page, `window.term.buffer.active.getLine(2).translateToString(true)`, '文'); - }); - - it('paste', async () => { - await openTerminal(page); - await page.evaluate(` - window.calls = []; - window.term.onData(e => calls.push(e)); - window.term.paste('foo'); - window.term.paste('\\r\\nfoo\\nbar\\r'); - window.term.write('\\x1b[?2004h', () => { - window.term.paste('foo'); - - window.term.options.ignoreBracketedPasteMode = true; - window.term.paste('check_mode'); - }); - `); - await pollFor(page, `window.calls`, ['foo', '\rfoo\rbar\r', '\x1b[200~foo\x1b[201~', 'check_mode']); - }); - - it('clear', async () => { - await openTerminal(page, { rows: 5 }); - await page.evaluate(` - window.term.write('test0'); - window.parsed = 0; - for (let i = 1; i < 10; i++) { - window.term.write('\\n\\rtest' + i, () => window.parsed++); - } - `); - await pollFor(page, `window.parsed`, 9); - await page.evaluate(`window.term.clear()`); - await pollFor(page, `window.term.buffer.active.length`, 5); - await pollFor(page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'test9'); - for (let i = 1; i < 5; i++) { - await pollFor(page, `window.term.buffer.active.getLine(${i}).translateToString(true)`, ''); - } - }); - - describe('options', () => { - it('getter', async () => { - await openTerminal(page); - assert.equal(await page.evaluate(`window.term.options.cols`), 80); - assert.equal(await page.evaluate(`window.term.options.rows`), 24); - }); - it('setter', async () => { - await openTerminal(page); - try { - await page.evaluate('window.term.options.cols = 40'); - fail(); - } catch {} - try { - await page.evaluate('window.term.options.rows = 20'); - fail(); - } catch {} - await page.evaluate('window.term.options.scrollback = 1'); - assert.equal(await page.evaluate(`window.term.options.scrollback`), 1); - await page.evaluate(` - window.term.options = { - fontSize: 30, - fontFamily: 'Arial' - }; - `); - assert.equal(await page.evaluate(`window.term.options.fontSize`), 30); - assert.equal(await page.evaluate(`window.term.options.fontFamily`), 'Arial'); - }); - it('object.keys return the correct number of options', async () => { - await openTerminal(page); - assert.notEqual(await page.evaluate(`Object.keys(window.term.options).length`), 0); - }); - }); - - describe('renderer', () => { - it('foreground', async () => { - await openTerminal(page); - await writeSync(page, '\\x1b[30m0\\x1b[31m1\\x1b[32m2\\x1b[33m3\\x1b[34m4\\x1b[35m5\\x1b[36m6\\x1b[37m7'); - await pollFor(page, `document.querySelectorAll('.xterm-rows > :nth-child(1) > *').length`, 9); - assert.deepEqual(await page.evaluate(` - [ - document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(1)').className, - document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(2)').className, - document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(3)').className, - document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(4)').className, - document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(5)').className, - document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(6)').className, - document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(7)').className - ] - `), [ - 'xterm-fg-0', - 'xterm-fg-1', - 'xterm-fg-2', - 'xterm-fg-3', - 'xterm-fg-4', - 'xterm-fg-5', - 'xterm-fg-6' - ]); - }); - - it('background', async () => { - await openTerminal(page); - await writeSync(page, '\\x1b[40m0\\x1b[41m1\\x1b[42m2\\x1b[43m3\\x1b[44m4\\x1b[45m5\\x1b[46m6\\x1b[47m7'); - await pollFor(page, `document.querySelectorAll('.xterm-rows > :nth-child(1) > *').length`, 9); - assert.deepEqual(await page.evaluate(` - [ - document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(1)').className, - document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(2)').className, - document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(3)').className, - document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(4)').className, - document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(5)').className, - document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(6)').className, - document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(7)').className - ] - `), [ - 'xterm-bg-0', - 'xterm-bg-1', - 'xterm-bg-2', - 'xterm-bg-3', - 'xterm-bg-4', - 'xterm-bg-5', - 'xterm-bg-6' - ]); - }); - }); - - it('selection', async () => { - await openTerminal(page, { rows: 5, cols: 5 }); - await writeSync(page, `\\n\\nfoo\\n\\n\\rbar\\n\\n\\rbaz`); - assert.equal(await page.evaluate(`window.term.hasSelection()`), false); - assert.equal(await page.evaluate(`window.term.getSelection()`), ''); - assert.deepEqual(await page.evaluate(`window.term.getSelectionPosition()`), undefined); - await page.evaluate(`window.term.selectAll()`); - assert.equal(await page.evaluate(`window.term.hasSelection()`), true); - if (process.platform === 'win32') { - assert.equal(await page.evaluate(`window.term.getSelection()`), '\r\n\r\nfoo\r\n\r\nbar\r\n\r\nbaz'); - } else { - assert.equal(await page.evaluate(`window.term.getSelection()`), '\n\nfoo\n\nbar\n\nbaz'); - } - assert.deepEqual(await page.evaluate(`window.term.getSelectionPosition()`), { start: { x: 0, y: 0 }, end: { x: 5, y: 6 } }); - await page.evaluate(`window.term.clearSelection()`); - assert.equal(await page.evaluate(`window.term.hasSelection()`), false); - assert.equal(await page.evaluate(`window.term.getSelection()`), ''); - assert.deepEqual(await page.evaluate(`window.term.getSelectionPosition()`), undefined); - await page.evaluate(`window.term.select(1, 2, 2)`); - assert.equal(await page.evaluate(`window.term.hasSelection()`), true); - assert.equal(await page.evaluate(`window.term.getSelection()`), 'oo'); - assert.deepEqual(await page.evaluate(`window.term.getSelectionPosition()`), { start: { x: 1, y: 2 }, end: { x: 3, y: 2 } }); - }); - - it('focus, blur', async () => { - await openTerminal(page); - assert.equal(await page.evaluate(`document.activeElement.className`), ''); - await page.evaluate(`window.term.focus()`); - assert.equal(await page.evaluate(`document.activeElement.className`), 'xterm-helper-textarea'); - await page.evaluate(`window.term.blur()`); - assert.equal(await page.evaluate(`document.activeElement.className`), ''); - }); - - describe('loadAddon', () => { - it('constructor', async () => { - await openTerminal(page, { cols: 5 }); - await page.evaluate(` - window.cols = 0; - window.term.loadAddon({ - activate: (t) => window.cols = t.cols, - dispose: () => {} - }); - `); - assert.equal(await page.evaluate(`window.cols`), 5); - }); - - it('dispose (addon)', async () => { - await openTerminal(page); - await page.evaluate(` - window.disposeCalled = false - window.addon = { - activate: () => {}, - dispose: () => window.disposeCalled = true - }; - window.term.loadAddon(window.addon); - `); - assert.equal(await page.evaluate(`window.disposeCalled`), false); - await page.evaluate(`window.addon.dispose()`); - assert.equal(await page.evaluate(`window.disposeCalled`), true); - }); - - it('dispose (terminal)', async () => { - await openTerminal(page); - await page.evaluate(` - window.disposeCalled = false - window.term.loadAddon({ - activate: () => {}, - dispose: () => window.disposeCalled = true - }); - `); - assert.equal(await page.evaluate(`window.disposeCalled`), false); - await page.evaluate(`window.term.dispose()`); - assert.equal(await page.evaluate(`window.disposeCalled`), true); - }); - }); - - describe('Events', () => { - it('onCursorMove', async () => { - await openTerminal(page); - await page.evaluate(` - window.callCount = 0; - window.term.onCursorMove(e => window.callCount++); - window.term.write('foo'); - `); - await pollFor(page, `window.callCount`, 1); - await page.evaluate(`window.term.write('bar')`); - await pollFor(page, `window.callCount`, 2); - }); - - it('onData', async () => { - await openTerminal(page); - await page.evaluate(` - window.calls = []; - window.term.onData(e => calls.push(e)); - `); - await page.type('.xterm-helper-textarea', 'foo'); - assert.deepEqual(await page.evaluate(`window.calls`), ['f', 'o', 'o']); - }); - - it('onKey', async () => { - await openTerminal(page); - await page.evaluate(` - window.calls = []; - window.term.onKey(e => calls.push(e.key)); - `); - await page.type('.xterm-helper-textarea', 'foo'); - assert.deepEqual(await page.evaluate(`window.calls`), ['f', 'o', 'o']); - }); - - it('onLineFeed', async () => { - await openTerminal(page); - await page.evaluate(` - window.callCount = 0; - window.term.onLineFeed(() => callCount++); - window.term.writeln('foo'); - `); - await pollFor(page, `window.callCount`, 1); - await page.evaluate(`window.term.writeln('bar')`); - await pollFor(page, `window.callCount`, 2); - }); - - it('onScroll', async () => { - await openTerminal(page, { rows: 5 }); - await page.evaluate(` - window.calls = []; - window.term.onScroll(e => window.calls.push(e)); - for (let i = 0; i < 4; i++) { - window.term.writeln('foo'); - } - `); - await pollFor(page, `window.calls`, []); - await page.evaluate(`window.term.writeln('bar')`); - await pollFor(page, `window.calls`, [1]); - await page.evaluate(`window.term.writeln('baz')`); - await pollFor(page, `window.calls`, [1, 2]); - }); - - it('onSelectionChange', async () => { - await openTerminal(page); - await page.evaluate(` - window.callCount = 0; - window.term.onSelectionChange(() => window.callCount++); - `); - await pollFor(page, `window.callCount`, 0); - await page.evaluate(`window.term.selectAll()`); - await pollFor(page, `window.callCount`, 1); - await page.evaluate(`window.term.clearSelection()`); - await pollFor(page, `window.callCount`, 2); - }); - - it('onRender', async function(): Promise { - this.retries(3); - await openTerminal(page); - await timeout(20); // Ensure all init events are fired - await page.evaluate(` - window.calls = []; - window.term.onRender(e => window.calls.push([e.start, e.end])); - `); - await pollFor(page, `window.calls`, []); - await page.evaluate(`window.term.write('foo')`); - await pollFor(page, `window.calls`, [[0, 0]]); - await page.evaluate(`window.term.write('bar\\n\\nbaz')`); - await pollFor(page, `window.calls`, [[0, 0], [0, 2]]); - }); - - it('onResize', async () => { - await openTerminal(page); - await timeout(20); // Ensure all init events are fired - await page.evaluate(` - window.calls = []; - window.term.onResize(e => window.calls.push([e.cols, e.rows])); - `); - await pollFor(page, `window.calls`, []); - await page.evaluate(`window.term.resize(10, 5)`); - await pollFor(page, `window.calls`, [[10, 5]]); - await page.evaluate(`window.term.resize(20, 15)`); - await pollFor(page, `window.calls`, [[10, 5], [20, 15]]); - }); - - it('onTitleChange', async () => { - await openTerminal(page); - await page.evaluate(` - window.calls = []; - window.term.onTitleChange(e => window.calls.push(e)); - `); - await pollFor(page, `window.calls`, []); - await page.evaluate(`window.term.write('\\x1b]2;foo\\x9c')`); - await pollFor(page, `window.calls`, ['foo']); - }); - it('onBell', async () => { - await openTerminal(page); - await page.evaluate(` - window.calls = []; - window.term.onBell(() => window.calls.push(true)); - `); - await pollFor(page, `window.calls`, []); - await page.evaluate(`window.term.write('\\x07')`); - await pollFor(page, `window.calls`, [true]); - }); - }); - - describe('buffer', () => { - it('cursorX, cursorY', async () => { - await openTerminal(page, { rows: 5, cols: 5 }); - assert.equal(await page.evaluate(`window.term.buffer.active.cursorX`), 0); - assert.equal(await page.evaluate(`window.term.buffer.active.cursorY`), 0); - await writeSync(page, 'foo'); - assert.equal(await page.evaluate(`window.term.buffer.active.cursorX`), 3); - assert.equal(await page.evaluate(`window.term.buffer.active.cursorY`), 0); - await writeSync(page, '\\n'); - assert.equal(await page.evaluate(`window.term.buffer.active.cursorX`), 3); - assert.equal(await page.evaluate(`window.term.buffer.active.cursorY`), 1); - await writeSync(page, '\\r'); - assert.equal(await page.evaluate(`window.term.buffer.active.cursorX`), 0); - assert.equal(await page.evaluate(`window.term.buffer.active.cursorY`), 1); - await writeSync(page, 'abcde'); - assert.equal(await page.evaluate(`window.term.buffer.active.cursorX`), 5); - assert.equal(await page.evaluate(`window.term.buffer.active.cursorY`), 1); - await writeSync(page, '\\n\\r\\n\\n\\n\\n\\n'); - assert.equal(await page.evaluate(`window.term.buffer.active.cursorX`), 0); - assert.equal(await page.evaluate(`window.term.buffer.active.cursorY`), 4); - }); - - it('viewportY', async () => { - await openTerminal(page, { rows: 5 }); - assert.equal(await page.evaluate(`window.term.buffer.active.viewportY`), 0); - await writeSync(page, '\\n\\n\\n\\n'); - assert.equal(await page.evaluate(`window.term.buffer.active.viewportY`), 0); - await writeSync(page, '\\n'); - assert.equal(await page.evaluate(`window.term.buffer.active.viewportY`), 1); - await writeSync(page, '\\n\\n\\n\\n'); - assert.equal(await page.evaluate(`window.term.buffer.active.viewportY`), 5); - await page.evaluate(`window.term.scrollLines(-1)`); - assert.equal(await page.evaluate(`window.term.buffer.active.viewportY`), 4); - await page.evaluate(`window.term.scrollToTop()`); - assert.equal(await page.evaluate(`window.term.buffer.active.viewportY`), 0); - }); - - it('baseY', async () => { - await openTerminal(page, { rows: 5 }); - assert.equal(await page.evaluate(`window.term.buffer.active.baseY`), 0); - await writeSync(page, '\\n\\n\\n\\n'); - assert.equal(await page.evaluate(`window.term.buffer.active.baseY`), 0); - await writeSync(page, '\\n'); - assert.equal(await page.evaluate(`window.term.buffer.active.baseY`), 1); - await writeSync(page, '\\n\\n\\n\\n'); - assert.equal(await page.evaluate(`window.term.buffer.active.baseY`), 5); - await page.evaluate(`window.term.scrollLines(-1)`); - assert.equal(await page.evaluate(`window.term.buffer.active.baseY`), 5); - await page.evaluate(`window.term.scrollToTop()`); - assert.equal(await page.evaluate(`window.term.buffer.active.baseY`), 5); - }); - - it('length', async () => { - await openTerminal(page, { rows: 5 }); - assert.equal(await page.evaluate(`window.term.buffer.active.length`), 5); - await writeSync(page, '\\n\\n\\n\\n'); - assert.equal(await page.evaluate(`window.term.buffer.active.length`), 5); - await writeSync(page, '\\n'); - assert.equal(await page.evaluate(`window.term.buffer.active.length`), 6); - await writeSync(page, '\\n\\n\\n\\n'); - assert.equal(await page.evaluate(`window.term.buffer.active.length`), 10); - }); - - describe('getLine', () => { - it('invalid index', async () => { - await openTerminal(page, { rows: 5 }); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(-1)`), undefined); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(5)`), undefined); - }); - - it('isWrapped', async () => { - await openTerminal(page, { cols: 5 }); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).isWrapped`), false); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(1).isWrapped`), false); - await writeSync(page, 'abcde'); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).isWrapped`), false); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(1).isWrapped`), false); - await writeSync(page, 'f'); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).isWrapped`), false); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(1).isWrapped`), true); - }); - - it('translateToString', async () => { - await openTerminal(page, { cols: 5 }); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).translateToString()`), ' '); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).translateToString(true)`), ''); - await writeSync(page, 'foo'); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).translateToString()`), 'foo '); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).translateToString(true)`), 'foo'); - await writeSync(page, 'bar'); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).translateToString()`), 'fooba'); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).translateToString(true)`), 'fooba'); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(1).translateToString(true)`), 'r'); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).translateToString(false, 1)`), 'ooba'); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).translateToString(false, 1, 3)`), 'oo'); - }); - - it('getCell', async () => { - await openTerminal(page, { cols: 5 }); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).getCell(-1)`), undefined); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).getCell(5)`), undefined); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).getCell(0).getChars()`), ''); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).getCell(0).getWidth()`), 1); - await writeSync(page, 'a文'); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).getCell(0).getChars()`), 'a'); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).getCell(0).getWidth()`), 1); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).getCell(1).getChars()`), '文'); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).getCell(1).getWidth()`), 2); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).getCell(2).getChars()`), ''); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).getCell(2).getWidth()`), 0); - }); - - it('clearMarkers', async () => { - await openTerminal(page, { cols: 5 }); - await page.evaluate(` - window.disposeStack = []; - `); - await writeSync(page, '\\n\\n\\n\\n'); - await writeSync(page, '\\n\\n\\n\\n'); - await writeSync(page, '\\n\\n\\n\\n'); - await writeSync(page, '\\n\\n\\n\\n'); - await page.evaluate(`window.term.registerMarker(1)`); - await page.evaluate(`window.term.registerMarker(2)`); - await page.evaluate(`window.term.scrollLines(10)`); - await page.evaluate(`window.term.registerMarker(3)`); - await page.evaluate(`window.term.registerMarker(4)`); - await page.evaluate(` - for (let i = 0; i < window.term.markers.length; ++i) { - const marker = window.term.markers[i]; - marker.onDispose(() => window.disposeStack.push(marker)); - }`); - await page.evaluate(`window.term.clear()`); - assert.equal(await page.evaluate(`window.disposeStack.length`), 4); - }); - }); - - it('active, normal, alternate', async () => { - await openTerminal(page, { cols: 5 }); - assert.equal(await page.evaluate(`window.term.buffer.active.type`), 'normal'); - assert.equal(await page.evaluate(`window.term.buffer.normal.type`), 'normal'); - assert.equal(await page.evaluate(`window.term.buffer.alternate.type`), 'alternate'); - - await writeSync(page, 'norm '); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).translateToString()`), 'norm '); - assert.equal(await page.evaluate(`window.term.buffer.normal.getLine(0).translateToString()`), 'norm '); - assert.equal(await page.evaluate(`window.term.buffer.alternate.getLine(0)`), undefined); - - await writeSync(page, '\\x1b[?47h\\r'); // use alternate screen buffer - assert.equal(await page.evaluate(`window.term.buffer.active.type`), 'alternate'); - assert.equal(await page.evaluate(`window.term.buffer.normal.type`), 'normal'); - assert.equal(await page.evaluate(`window.term.buffer.alternate.type`), 'alternate'); - - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).translateToString()`), ' '); - await writeSync(page, 'alt '); - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).translateToString()`), 'alt '); - assert.equal(await page.evaluate(`window.term.buffer.normal.getLine(0).translateToString()`), 'norm '); - assert.equal(await page.evaluate(`window.term.buffer.alternate.getLine(0).translateToString()`), 'alt '); - - await writeSync(page, '\\x1b[?47l\\r'); // use normal screen buffer - assert.equal(await page.evaluate(`window.term.buffer.active.type`), 'normal'); - assert.equal(await page.evaluate(`window.term.buffer.normal.type`), 'normal'); - assert.equal(await page.evaluate(`window.term.buffer.alternate.type`), 'alternate'); - - assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).translateToString()`), 'norm '); - assert.equal(await page.evaluate(`window.term.buffer.normal.getLine(0).translateToString()`), 'norm '); - assert.equal(await page.evaluate(`window.term.buffer.alternate.getLine(0)`), undefined); - }); - }); - - describe('modes', () => { - it('defaults', async () => { - await openTerminal(page); - assert.deepStrictEqual(await page.evaluate(`window.term.modes`), { - applicationCursorKeysMode: false, - applicationKeypadMode: false, - bracketedPasteMode: false, - insertMode: false, - mouseTrackingMode: 'none', - originMode: false, - reverseWraparoundMode: false, - sendFocusMode: false, - wraparoundMode: true - }); - }); - it('applicationCursorKeysMode', async () => { - await openTerminal(page); - await writeSync(page, '\\x1b[?1h'); - assert.strictEqual(await page.evaluate(`window.term.modes.applicationCursorKeysMode`), true); - await writeSync(page, '\\x1b[?1l'); - assert.strictEqual(await page.evaluate(`window.term.modes.applicationCursorKeysMode`), false); - }); - it('applicationKeypadMode', async () => { - await openTerminal(page); - await writeSync(page, '\\x1b[?66h'); - assert.strictEqual(await page.evaluate(`window.term.modes.applicationKeypadMode`), true); - await writeSync(page, '\\x1b[?66l'); - assert.strictEqual(await page.evaluate(`window.term.modes.applicationKeypadMode`), false); - }); - it('bracketedPasteMode', async () => { - await openTerminal(page); - await writeSync(page, '\\x1b[?2004h'); - assert.strictEqual(await page.evaluate(`window.term.modes.bracketedPasteMode`), true); - await writeSync(page, '\\x1b[?2004l'); - assert.strictEqual(await page.evaluate(`window.term.modes.bracketedPasteMode`), false); - }); - it('insertMode', async () => { - await openTerminal(page); - await writeSync(page, '\\x1b[4h'); - assert.strictEqual(await page.evaluate(`window.term.modes.insertMode`), true); - await writeSync(page, '\\x1b[4l'); - assert.strictEqual(await page.evaluate(`window.term.modes.insertMode`), false); - }); - it('mouseTrackingMode', async () => { - await openTerminal(page); - await writeSync(page, '\\x1b[?9h'); - assert.strictEqual(await page.evaluate(`window.term.modes.mouseTrackingMode`), 'x10'); - await writeSync(page, '\\x1b[?9l'); - assert.strictEqual(await page.evaluate(`window.term.modes.mouseTrackingMode`), 'none'); - await writeSync(page, '\\x1b[?1000h'); - assert.strictEqual(await page.evaluate(`window.term.modes.mouseTrackingMode`), 'vt200'); - await writeSync(page, '\\x1b[?1000l'); - assert.strictEqual(await page.evaluate(`window.term.modes.mouseTrackingMode`), 'none'); - await writeSync(page, '\\x1b[?1002h'); - assert.strictEqual(await page.evaluate(`window.term.modes.mouseTrackingMode`), 'drag'); - await writeSync(page, '\\x1b[?1002l'); - assert.strictEqual(await page.evaluate(`window.term.modes.mouseTrackingMode`), 'none'); - await writeSync(page, '\\x1b[?1003h'); - assert.strictEqual(await page.evaluate(`window.term.modes.mouseTrackingMode`), 'any'); - await writeSync(page, '\\x1b[?1003l'); - assert.strictEqual(await page.evaluate(`window.term.modes.mouseTrackingMode`), 'none'); - }); - it('originMode', async () => { - await openTerminal(page); - await writeSync(page, '\\x1b[?6h'); - assert.strictEqual(await page.evaluate(`window.term.modes.originMode`), true); - await writeSync(page, '\\x1b[?6l'); - assert.strictEqual(await page.evaluate(`window.term.modes.originMode`), false); - }); - it('reverseWraparoundMode', async () => { - await openTerminal(page); - await writeSync(page, '\\x1b[?45h'); - assert.strictEqual(await page.evaluate(`window.term.modes.reverseWraparoundMode`), true); - await writeSync(page, '\\x1b[?45l'); - assert.strictEqual(await page.evaluate(`window.term.modes.reverseWraparoundMode`), false); - }); - it('sendFocusMode', async () => { - await openTerminal(page); - await writeSync(page, '\\x1b[?1004h'); - assert.strictEqual(await page.evaluate(`window.term.modes.sendFocusMode`), true); - await writeSync(page, '\\x1b[?1004l'); - assert.strictEqual(await page.evaluate(`window.term.modes.sendFocusMode`), false); - }); - it('wraparoundMode', async () => { - await openTerminal(page); - await writeSync(page, '\\x1b[?7h'); - assert.strictEqual(await page.evaluate(`window.term.modes.wraparoundMode`), true); - await writeSync(page, '\\x1b[?7l'); - assert.strictEqual(await page.evaluate(`window.term.modes.wraparoundMode`), false); - }); - }); - - it('dispose', async () => { - await page.evaluate(` - window.term = new Terminal(); - window.term.dispose(); - `); - assert.equal(await page.evaluate(`window.term._core._isDisposed`), true); - }); - - it('dispose (opened)', async () => { - await openTerminal(page); - await page.evaluate(`window.term.dispose()`); - assert.equal(await page.evaluate(`window.term._core._isDisposed`), true); - }); - - it('render when visible after hidden', async () => { - await page.evaluate(`document.querySelector('#terminal-container').style.display='none'`); - await page.evaluate(`window.term = new Terminal()`); - await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`); - await page.evaluate(`document.querySelector('#terminal-container').style.display=''`); - await pollFor(page, `window.term._core._renderService.dimensions.css.cell.width > 0`, true); - }); - - describe('registerDecoration', () => { - describe('bufferDecorations', () => { - it('should register decorations and render them when terminal open is called', async () => { - await openTerminal(page); - await page.evaluate(`window.marker1 = window.term.registerMarker(1)`); - await page.evaluate(`window.marker2 = window.term.registerMarker(2)`); - await page.evaluate(`window.term.registerDecoration({ marker: window.marker1 })`); - await page.evaluate(`window.term.registerDecoration({ marker: window.marker2 })`); - await openTerminal(page); - await pollFor(page, `document.querySelectorAll('.xterm-screen .xterm-decoration').length`, 2); - }); - it('should return undefined when the marker has already been disposed of', async () => { - await openTerminal(page); - await page.evaluate(`window.marker = window.term.registerMarker(1)`); - await page.evaluate(`window.marker.dispose()`); - await pollFor(page, `window.decoration = window.term.registerDecoration({ marker: window.marker });`, undefined); - }); - it('should throw when a negative x offset is provided', async () => { - await openTerminal(page); - await page.evaluate(`window.marker = window.term.registerMarker(1)`); - await page.evaluate(` - try { - window.decoration = window.term.registerDecoration({ marker: window.marker, x: -2 }); - } catch (e) { - window.throwMessage = e.message; - } - `); - await pollFor(page, 'window.throwMessage', 'This API only accepts positive integers'); - }); - }); - describe('overviewRulerDecorations', () => { - it('should not add an overview ruler when width is not set', async () => { - await openTerminal(page); - await page.evaluate(`window.marker1 = window.term.registerMarker(1)`); - await page.evaluate(`window.marker2 = window.term.registerMarker(2)`); - await page.evaluate(`window.term.registerDecoration({ marker: window.marker1, overviewRulerOptions: { color: 'red', position: 'full' } })`); - await page.evaluate(`window.term.registerDecoration({ marker: window.marker2, overviewRulerOptions: { color: 'blue', position: 'full' } })`); - await openTerminal(page); - await pollFor(page, `document.querySelectorAll('.xterm-decoration-overview-ruler').length`, 0); - }); - it('should add an overview ruler when width is set', async () => { - await openTerminal(page, { overviewRulerWidth: 15 }); - await page.evaluate(`window.marker1 = window.term.registerMarker(1)`); - await page.evaluate(`window.marker2 = window.term.registerMarker(2)`); - await page.evaluate(`window.term.registerDecoration({ marker: window.marker1, overviewRulerOptions: { color: 'red', position: 'full' } })`); - await page.evaluate(`window.term.registerDecoration({ marker: window.marker2, overviewRulerOptions: { color: 'blue', position: 'full' } })`); - await openTerminal(page); - await pollFor(page, `document.querySelectorAll('.xterm-decoration-overview-ruler').length`, 1); - }); - }); - }); - - describe('registerLinkProvider', () => { - it('should fire provideLinks when hovering cells', async () => { - await openTerminal(page); - // Focus the terminal as the cursor will show and trigger a rerender, which can clear the - // active link - await page.evaluate('window.term.focus()'); - await page.evaluate(` - window.calls = []; - window.disposable = window.term.registerLinkProvider({ - provideLinks: (position, cb) => { - calls.push(position); - cb(undefined); - } - }); - `); - const dims = await getDimensions(); - await moveMouseCell(page, dims, 1, 1); - await moveMouseCell(page, dims, 2, 2); - await moveMouseCell(page, dims, 10, 4); - await pollFor(page, `window.calls`, [1, 2, 4]); - await page.evaluate(`window.disposable.dispose()`); - }); - - it('should fire hover and leave events on the link', async () => { - await openTerminal(page); - // Focus the terminal as the cursor will show and trigger a rerender, which can clear the - // active link - await page.evaluate('window.term.focus()'); - await writeSync(page, 'foo bar baz'); - // Wait for renderer to catch up as links are cleared on render - await pollFor(page, `document.querySelector('.xterm-rows').textContent`, 'foo bar baz '); - await page.evaluate(` - window.calls = []; - window.disposable = window.term.registerLinkProvider({ - provideLinks: (position, cb) => { - window.calls.push('provide ' + position); - if (position === 1) { - window.calls.push('match'); - cb([{ - range: { start: { x: 5, y: 1 }, end: { x: 7, y: 1 } }, - text: 'bar', - activate: () => window.calls.push('activate'), - hover: () => window.calls.push('hover'), - leave: () => window.calls.push('leave') - }]); - } - } - }); - `); - const dims = await getDimensions(); - await moveMouseCell(page, dims, 5, 1); - await timeout(100); - await moveMouseCell(page, dims, 4, 1); - await pollFor(page, `window.calls`, ['provide 1', 'match', 'hover', 'leave' ]); - await moveMouseCell(page, dims, 7, 1); - await timeout(100); - await moveMouseCell(page, dims, 8, 1); - await pollFor(page, `window.calls`, ['provide 1', 'match', 'hover', 'leave', 'hover', 'leave']); - await page.evaluate(`window.disposable.dispose()`); - }); - - it('should work fine when hover and leave callbacks are not provided', async () => { - await openTerminal(page); - // Focus the terminal as the cursor will show and trigger a rerender, which can clear the - // active link - await page.evaluate('window.term.focus()'); - await writeSync(page, 'foo bar baz'); - // Wait for renderer to catch up as links are cleared on render - await pollFor(page, `document.querySelector('.xterm-rows').textContent`, 'foo bar baz '); - await page.evaluate(` - window.calls = []; - window.disposable = window.term.registerLinkProvider({ - provideLinks: (position, cb) => { - window.calls.push('provide ' + position); - if (position === 1) { - window.calls.push('match 1'); - cb([{ - range: { start: { x: 5, y: 1 }, end: { x: 7, y: 1 } }, - text: 'bar', - activate: () => window.calls.push('activate') - }]); - } else if (position === 2) { - window.calls.push('match 2'); - cb([{ - range: { start: { x: 5, y: 2 }, end: { x: 7, y: 2 } }, - text: 'bar', - activate: () => window.calls.push('activate') - }]); - } - } - }); - `); - const dims = await getDimensions(); - await moveMouseCell(page, dims, 5, 1); - await pollFor(page, `window.calls`, ['provide 1', 'match 1']); - await moveMouseCell(page, dims, 4, 2); - await pollFor(page, `window.calls`, ['provide 1', 'match 1', 'provide 2', 'match 2']); - await moveMouseCell(page, dims, 7, 1); - await pollFor(page, `window.calls`, ['provide 1', 'match 1', 'provide 2', 'match 2', 'provide 1', 'match 1']); - await moveMouseCell(page, dims, 6, 2); - await pollFor(page, `window.calls`, ['provide 1', 'match 1', 'provide 2', 'match 2', 'provide 1', 'match 1', 'provide 2', 'match 2']); - await page.evaluate(`window.disposable.dispose()`); - }); - - it('should fire activate events when clicking the link', async () => { - await openTerminal(page); - // Focus the terminal as the cursor will show and trigger a rerender, which can clear the - // active link - await page.evaluate('window.term.focus()'); - await writeSync(page, 'a b c'); - // Wait for renderer to catch up as links are cleared on render - await pollFor(page, `document.querySelector('.xterm-rows').textContent`, 'a b c '); - await page.evaluate(` - window.calls = []; - window.disposable = window.term.registerLinkProvider({ - provideLinks: (y, cb) => { - window.calls.push('provide ' + y); - cb([{ - range: { start: { x: 1, y }, end: { x: 80, y } }, - text: window.term.buffer.active.getLine(y - 1).translateToString(), - activate: (_, text) => window.calls.push('activate ' + y), - hover: () => window.calls.push('hover ' + y), - leave: () => window.calls.push('leave ' + y) - }]); - } - }); - `); - const dims = await getDimensions(); - await moveMouseCell(page, dims, 3, 1); - await pollFor(page, `window.calls`, ['provide 1', 'hover 1']); - await page.mouse.down(); - await page.mouse.up(); - await pollFor(page, `window.calls`, ['provide 1', 'hover 1', 'activate 1']); - await moveMouseCell(page, dims, 1, 2); - await pollFor(page, `window.calls`, ['provide 1', 'hover 1', 'activate 1', 'leave 1', 'provide 2', 'hover 2']); - await page.mouse.down(); - await page.mouse.up(); - await pollFor(page, `window.calls`, ['provide 1', 'hover 1', 'activate 1', 'leave 1', 'provide 2', 'hover 2', 'activate 2']); - await moveMouseCell(page, dims, 5, 1); - await pollFor(page, `window.calls`, ['provide 1', 'hover 1', 'activate 1', 'leave 1', 'provide 2', 'hover 2', 'activate 2', 'leave 2', 'provide 1', 'hover 1']); - await page.mouse.down(); - await page.mouse.up(); - await pollFor(page, `window.calls`, ['provide 1', 'hover 1', 'activate 1', 'leave 1', 'provide 2', 'hover 2', 'activate 2', 'leave 2', 'provide 1', 'hover 1', 'activate 1']); - await page.evaluate(`window.disposable.dispose()`); - }); - - it('should work when multiple links are provided on the same line', async () => { - await openTerminal(page); - // Focus the terminal as the cursor will show and trigger a rerender, which can clear the - // active link - await page.evaluate('window.term.focus()'); - await writeSync(page, 'foo bar baz'); - // Wait for renderer to catch up as links are cleared on render - await pollFor(page, `document.querySelector('.xterm-rows').textContent`, 'foo bar baz '); - await page.evaluate(` - window.calls = []; - window.disposable = window.term.registerLinkProvider({ - provideLinks: (position, cb) => { - window.calls.push('provide ' + position); - if (position === 1) { - cb([{ - range: { start: { x: 1, y: 1 }, end: { x: 3, y: 1 } }, - text: '', - activate: () => window.calls.push('activate'), - hover: () => window.calls.push('hover 1-3'), - leave: () => window.calls.push('leave 1-3') - }, { - range: { start: { x: 5, y: 1 }, end: { x: 7, y: 1 } }, - text: '', - activate: () => window.calls.push('activate'), - hover: () => window.calls.push('hover 5-7'), - leave: () => window.calls.push('leave 5-7') - }, { - range: { start: { x: 9, y: 1 }, end: { x: 11, y: 1 } }, - text: '', - activate: () => window.calls.push('activate'), - hover: () => window.calls.push('hover 9-11'), - leave: () => window.calls.push('leave 9-11') - }]); - } - } - }); - `); - const dims = await getDimensions(); - await moveMouseCell(page, dims, 2, 1); - await pollFor(page, `window.calls`, ['provide 1', 'hover 1-3']); - await moveMouseCell(page, dims, 6, 1); - await pollFor(page, `window.calls`, ['provide 1', 'hover 1-3', 'leave 1-3', 'hover 5-7']); - await moveMouseCell(page, dims, 6, 2); - await pollFor(page, `window.calls`, ['provide 1', 'hover 1-3', 'leave 1-3', 'hover 5-7', 'leave 5-7', 'provide 2']); - await moveMouseCell(page, dims, 10, 1); - await pollFor(page, `window.calls`, ['provide 1', 'hover 1-3', 'leave 1-3', 'hover 5-7', 'leave 5-7', 'provide 2', 'provide 1', 'hover 9-11']); - await page.evaluate(`window.disposable.dispose()`); - }); - - it('should dispose links when hovering away', async () => { - await openTerminal(page); - // Focus the terminal as the cursor will show and trigger a rerender, which can clear the - // active link - await page.evaluate('window.term.focus()'); - await writeSync(page, 'foo bar baz'); - // Wait for renderer to catch up as links are cleared on render - await pollFor(page, `document.querySelector('.xterm-rows').textContent`, 'foo bar baz '); - await page.evaluate(` - window.calls = []; - window.disposable = window.term.registerLinkProvider({ - provideLinks: (position, cb) => { - window.calls.push('provide ' + position); - if (position === 1) { - cb([{ - range: { start: { x: 1, y: 1 }, end: { x: 3, y: 1 } }, - text: '', - activate: () => window.calls.push('activate'), - dispose: () => window.calls.push('dispose 1-3'), - hover: () => window.calls.push('hover 1-3'), - leave: () => window.calls.push('leave 1-3') - }, { - range: { start: { x: 5, y: 1 }, end: { x: 7, y: 1 } }, - text: '', - activate: () => window.calls.push('activate'), - dispose: () => window.calls.push('dispose 5-7'), - hover: () => window.calls.push('hover 5-7'), - leave: () => window.calls.push('leave 5-7') - }, { - range: { start: { x: 9, y: 1 }, end: { x: 11, y: 1 } }, - text: '', - activate: () => window.calls.push('activate'), - dispose: () => window.calls.push('dispose 9-11'), - hover: () => window.calls.push('hover 9-11'), - leave: () => window.calls.push('leave 9-11') - }]); - } - } - }); - `); - const dims = await getDimensions(); - await moveMouseCell(page, dims, 2, 1); - await pollFor(page, `window.calls`, ['provide 1', 'hover 1-3']); - await moveMouseCell(page, dims, 6, 1); - await pollFor(page, `window.calls`, ['provide 1', 'hover 1-3', 'leave 1-3', 'hover 5-7']); - await moveMouseCell(page, dims, 6, 2); - await pollFor(page, `window.calls`, ['provide 1', 'hover 1-3', 'leave 1-3', 'hover 5-7', 'leave 5-7', 'dispose 1-3', 'dispose 5-7', 'dispose 9-11', 'provide 2']); - await moveMouseCell(page, dims, 10, 1); - await pollFor(page, `window.calls`, ['provide 1', 'hover 1-3', 'leave 1-3', 'hover 5-7', 'leave 5-7', 'dispose 1-3', 'dispose 5-7', 'dispose 9-11', 'provide 2', 'provide 1', 'hover 9-11']); - await moveMouseCell(page, dims, 10, 2); - await pollFor(page, `window.calls`, ['provide 1', 'hover 1-3', 'leave 1-3', 'hover 5-7', 'leave 5-7', 'dispose 1-3', 'dispose 5-7', 'dispose 9-11', 'provide 2', 'provide 1', 'hover 9-11', 'leave 9-11', 'dispose 1-3', 'dispose 5-7', 'dispose 9-11', 'provide 2']); - await page.evaluate(`window.disposable.dispose()`); - }); - }); -}); - -interface IDimensions { - top: number; - left: number; - renderDimensions: IRenderDimensions; -} - -async function getDimensions(): Promise { - return await page.evaluate(` - (function() { - const rect = document.querySelector('.xterm-rows').getBoundingClientRect(); - return { - top: rect.top, - left: rect.left, - renderDimensions: window.term._core._renderService.dimensions - }; - })(); - `); -} - -async function getCellCoordinates(dimensions: IDimensions, col: number, row: number): Promise<{ x: number, y: number }> { - return { - x: dimensions.left + dimensions.renderDimensions.device.cell.width * (col - 0.5), - y: dimensions.top + dimensions.renderDimensions.device.cell.height * (row - 0.5) - }; -} - -async function moveMouseCell(page: Page, dimensions: IDimensions, col: number, row: number): Promise { - const coords = await getCellCoordinates(dimensions, col, row); - await page.mouse.move(coords.x, coords.y); -} diff --git a/test/playwright/InputHandler.test.ts b/test/playwright/InputHandler.test.ts index 1b5f225a..42b2916a 100644 --- a/test/playwright/InputHandler.test.ts +++ b/test/playwright/InputHandler.test.ts @@ -3,10 +3,9 @@ * @license MIT */ import { test } from '@playwright/test'; -import { deepStrictEqual, ok, strictEqual } from 'assert'; -import type { IDisposable } from 'xterm'; -import { createTestContext, ITestContext, openTerminal, pollFor } from './TestUtils'; +import { deepStrictEqual, ok } from 'assert'; import { IRenderDimensions } from 'browser/renderer/shared/Types'; +import { ITestContext, createTestContext, openTerminal, pollFor } from './TestUtils'; let ctx: ITestContext; test.beforeAll(async ({ browser }) => { diff --git a/test/playwright/Terminal.test.ts b/test/playwright/Terminal.test.ts new file mode 100644 index 00000000..6fe2f803 --- /dev/null +++ b/test/playwright/Terminal.test.ts @@ -0,0 +1,1053 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ +import { test } from '@playwright/test'; +import { deepStrictEqual, notStrictEqual, strictEqual } from 'assert'; +import { ITestContext, createTestContext, openTerminal, pollFor, timeout } from './TestUtils'; +import { IRenderDimensions } from 'browser/renderer/shared/Types'; + +let ctx: ITestContext; +test.beforeAll(async ({ browser }) => { + ctx = await createTestContext(browser); + await openTerminal(ctx); +}); +test.afterAll(async () => await ctx.page.close()); + + +test.describe.only('API Integration Tests', () => { + test('Default options', async () => { + strictEqual(await ctx.proxy.cols, 80); + strictEqual(await ctx.proxy.rows, 24); + }); + + test('Proposed API check', async () => { + await openTerminal(ctx, { allowProposedApi: false }); + await ctx.page.evaluate(` + try { + window.term.markers; + } catch (e) { + window.throwMessage = e.message; + } + `); + await pollFor(ctx.page, 'window.throwMessage', 'You must set the allowProposedApi option to true to use proposed API'); + }); + + test('write', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + window.term.write('foo'); + window.term.write('bar'); + window.term.write('文'); + `); + await pollFor(ctx.page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foobar文'); + }); + + test('write with callback', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + window.term.write('foo', () => { window.__x = 'a'; }); + window.term.write('bar', () => { window.__x += 'b'; }); + window.term.write('文', () => { window.__x += 'c'; }); + `); + await pollFor(ctx.page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foobar文'); + await pollFor(ctx.page, `window.__x`, 'abc'); + }); + + test('write - bytes (UTF8)', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + window.term.write(new Uint8Array([102, 111, 111])); // foo + window.term.write(new Uint8Array([98, 97, 114])); // bar + window.term.write(new Uint8Array([230, 150, 135])); // 文 + `); + await pollFor(ctx.page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foobar文'); + }); + + test('write - bytes (UTF8) with callback', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + window.term.write(new Uint8Array([102, 111, 111]), () => { window.__x = 'A'; }); // foo + window.term.write(new Uint8Array([98, 97, 114]), () => { window.__x += 'B'; }); // bar + window.term.write(new Uint8Array([230, 150, 135]), () => { window.__x += 'C'; }); // 文 + `); + await pollFor(ctx.page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foobar文'); + await pollFor(ctx.page, `window.__x`, 'ABC'); + }); + + test('writeln', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + window.term.writeln('foo'); + window.term.writeln('bar'); + window.term.writeln('文'); + `); + await pollFor(ctx.page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foo'); + await pollFor(ctx.page, `window.term.buffer.active.getLine(1).translateToString(true)`, 'bar'); + await pollFor(ctx.page, `window.term.buffer.active.getLine(2).translateToString(true)`, '文'); + }); + + test('writeln with callback', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + window.term.writeln('foo', () => { window.__x = '1'; }); + window.term.writeln('bar', () => { window.__x += '2'; }); + window.term.writeln('文', () => { window.__x += '3'; }); + `); + await pollFor(ctx.page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foo'); + await pollFor(ctx.page, `window.term.buffer.active.getLine(1).translateToString(true)`, 'bar'); + await pollFor(ctx.page, `window.term.buffer.active.getLine(2).translateToString(true)`, '文'); + await pollFor(ctx.page, `window.__x`, '123'); + }); + + test('writeln - bytes (UTF8)', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + window.term.writeln(new Uint8Array([102, 111, 111])); + window.term.writeln(new Uint8Array([98, 97, 114])); + window.term.writeln(new Uint8Array([230, 150, 135])); + `); + await pollFor(ctx.page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foo'); + await pollFor(ctx.page, `window.term.buffer.active.getLine(1).translateToString(true)`, 'bar'); + await pollFor(ctx.page, `window.term.buffer.active.getLine(2).translateToString(true)`, '文'); + }); + + test('paste', async () => { + await openTerminal(ctx); + const calls: string[] = []; + ctx.proxy.onData(e => calls.push(e)); + await ctx.proxy.paste('foo'); + await ctx.proxy.paste('\r\nfoo\nbar\r'); + await ctx.proxy.write('\x1b[?2004h'); + await ctx.proxy.paste('foo'); + await ctx.page.evaluate(`window.term.options.ignoreBracketedPasteMode = true;`); + await ctx.proxy.paste('check_mode'); + deepStrictEqual(calls, ['foo', '\rfoo\rbar\r', '\x1b[200~foo\x1b[201~', 'check_mode']); + }); + + test('clear', async () => { + await openTerminal(ctx, { rows: 5 }); + await ctx.page.evaluate(` + window.term.write('test0'); + window.parsed = 0; + for (let i = 1; i < 10; i++) { + window.term.write('\\n\\rtest' + i, () => window.parsed++); + } + `); + await pollFor(ctx.page, `window.parsed`, 9); + await ctx.page.evaluate(`window.term.clear()`); + await pollFor(ctx.page, `window.term.buffer.active.length`, 5); + await pollFor(ctx.page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'test9'); + for (let i = 1; i < 5; i++) { + await pollFor(ctx.page, `window.term.buffer.active.getLine(${i}).translateToString(true)`, ''); + } + }); + + test.describe('options', () => { + test('getter', async () => { + await openTerminal(ctx); + strictEqual(await ctx.page.evaluate(`window.term.options.cols`), 80); + strictEqual(await ctx.page.evaluate(`window.term.options.rows`), 24); + }); + test('setter', async () => { + await openTerminal(ctx); + try { + await ctx.page.evaluate('window.term.options.cols = 40'); + test.fail(); + } catch {} + try { + await ctx.page.evaluate('window.term.options.rows = 20'); + test.fail(); + } catch {} + await ctx.page.evaluate('window.term.options.scrollback = 1'); + strictEqual(await ctx.page.evaluate(`window.term.options.scrollback`), 1); + await ctx.page.evaluate(` + window.term.options = { + fontSize: 30, + fontFamily: 'Arial' + }; + `); + strictEqual(await ctx.page.evaluate(`window.term.options.fontSize`), 30); + strictEqual(await ctx.page.evaluate(`window.term.options.fontFamily`), 'Arial'); + }); + test('object.keys return the correct number of options', async () => { + await openTerminal(ctx); + notStrictEqual(await ctx.page.evaluate(`Object.keys(window.term.options).length`), 0); + }); + }); + + test.describe('renderer', () => { + test('foreground', async () => { + await openTerminal(ctx); + await ctx.proxy.write('\x1b[30m0\x1b[31m1\x1b[32m2\x1b[33m3\x1b[34m4\x1b[35m5\x1b[36m6\x1b[37m7'); + await pollFor(ctx.page, `document.querySelectorAll('.xterm-rows > :nth-child(1) > *').length`, 9); + deepStrictEqual(await ctx.page.evaluate(` + [ + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(1)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(2)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(3)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(4)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(5)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(6)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(7)').className + ] + `), [ + 'xterm-fg-0', + 'xterm-fg-1', + 'xterm-fg-2', + 'xterm-fg-3', + 'xterm-fg-4', + 'xterm-fg-5', + 'xterm-fg-6' + ]); + }); + + test('background', async () => { + await openTerminal(ctx); + await ctx.proxy.write('\x1b[40m0\x1b[41m1\x1b[42m2\x1b[43m3\x1b[44m4\x1b[45m5\x1b[46m6\x1b[47m7'); + await pollFor(ctx.page, `document.querySelectorAll('.xterm-rows > :nth-child(1) > *').length`, 9); + deepStrictEqual(await ctx.page.evaluate(` + [ + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(1)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(2)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(3)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(4)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(5)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(6)').className, + document.querySelector('.xterm-rows > :nth-child(1) > :nth-child(7)').className + ] + `), [ + 'xterm-bg-0', + 'xterm-bg-1', + 'xterm-bg-2', + 'xterm-bg-3', + 'xterm-bg-4', + 'xterm-bg-5', + 'xterm-bg-6' + ]); + }); + }); + + test('selection', async () => { + await openTerminal(ctx, { rows: 5, cols: 5 }); + await ctx.proxy.write(`\n\nfoo\n\n\rbar\n\n\rbaz`); + strictEqual(await ctx.page.evaluate(`window.term.hasSelection()`), false); + strictEqual(await ctx.page.evaluate(`window.term.getSelection()`), ''); + deepStrictEqual(await ctx.page.evaluate(`window.term.getSelectionPosition()`), undefined); + await ctx.page.evaluate(`window.term.selectAll()`); + strictEqual(await ctx.page.evaluate(`window.term.hasSelection()`), true); + if (process.platform === 'win32') { + strictEqual(await ctx.page.evaluate(`window.term.getSelection()`), '\r\n\r\nfoo\r\n\r\nbar\r\n\r\nbaz'); + } else { + strictEqual(await ctx.page.evaluate(`window.term.getSelection()`), '\n\nfoo\n\nbar\n\nbaz'); + } + deepStrictEqual(await ctx.page.evaluate(`window.term.getSelectionPosition()`), { start: { x: 0, y: 0 }, end: { x: 5, y: 6 } }); + await ctx.page.evaluate(`window.term.clearSelection()`); + strictEqual(await ctx.page.evaluate(`window.term.hasSelection()`), false); + strictEqual(await ctx.page.evaluate(`window.term.getSelection()`), ''); + deepStrictEqual(await ctx.page.evaluate(`window.term.getSelectionPosition()`), undefined); + await ctx.page.evaluate(`window.term.select(1, 2, 2)`); + strictEqual(await ctx.page.evaluate(`window.term.hasSelection()`), true); + strictEqual(await ctx.page.evaluate(`window.term.getSelection()`), 'oo'); + deepStrictEqual(await ctx.page.evaluate(`window.term.getSelectionPosition()`), { start: { x: 1, y: 2 }, end: { x: 3, y: 2 } }); + }); + + test('focus, blur', async () => { + await openTerminal(ctx); + strictEqual(await ctx.page.evaluate(`document.activeElement.className`), ''); + await ctx.page.evaluate(`window.term.focus()`); + strictEqual(await ctx.page.evaluate(`document.activeElement.className`), 'xterm-helper-textarea'); + await ctx.page.evaluate(`window.term.blur()`); + strictEqual(await ctx.page.evaluate(`document.activeElement.className`), ''); + }); + + test.describe('loadAddon', () => { + test('constructor', async () => { + await openTerminal(ctx, { cols: 5 }); + await ctx.page.evaluate(` + window.cols = 0; + window.term.loadAddon({ + activate: (t) => window.cols = t.cols, + dispose: () => {} + }); + `); + strictEqual(await ctx.page.evaluate(`window.cols`), 5); + }); + + test('dispose (addon)', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + window.disposeCalled = false + window.addon = { + activate: () => {}, + dispose: () => window.disposeCalled = true + }; + window.term.loadAddon(window.addon); + `); + strictEqual(await ctx.page.evaluate(`window.disposeCalled`), false); + await ctx.page.evaluate(`window.addon.dispose()`); + strictEqual(await ctx.page.evaluate(`window.disposeCalled`), true); + }); + + test('dispose (terminal)', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + window.disposeCalled = false + window.term.loadAddon({ + activate: () => {}, + dispose: () => window.disposeCalled = true + }); + `); + strictEqual(await ctx.page.evaluate(`window.disposeCalled`), false); + await ctx.page.evaluate(`window.term.dispose()`); + strictEqual(await ctx.page.evaluate(`window.disposeCalled`), true); + }); + }); + + test.describe('Events', () => { + test('onCursorMove', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + window.callCount = 0; + window.term.onCursorMove(e => window.callCount++); + window.term.write('foo'); + `); + await pollFor(ctx.page, `window.callCount`, 1); + await ctx.page.evaluate(`window.term.write('bar')`); + await pollFor(ctx.page, `window.callCount`, 2); + }); + + test('onData', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + window.calls = []; + window.term.onData(e => calls.push(e)); + `); + await ctx.page.type('.xterm-helper-textarea', 'foo'); + deepStrictEqual(await ctx.page.evaluate(`window.calls`), ['f', 'o', 'o']); + }); + + test('onKey', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + window.calls = []; + window.term.onKey(e => calls.push(e.key)); + `); + await ctx.page.type('.xterm-helper-textarea', 'foo'); + deepStrictEqual(await ctx.page.evaluate(`window.calls`), ['f', 'o', 'o']); + }); + + test('onLineFeed', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + window.callCount = 0; + window.term.onLineFeed(() => callCount++); + window.term.writeln('foo'); + `); + await pollFor(ctx.page, `window.callCount`, 1); + await ctx.page.evaluate(`window.term.writeln('bar')`); + await pollFor(ctx.page, `window.callCount`, 2); + }); + + test('onScroll', async () => { + await openTerminal(ctx, { rows: 5 }); + await ctx.page.evaluate(` + window.calls = []; + window.term.onScroll(e => window.calls.push(e)); + for (let i = 0; i < 4; i++) { + window.term.writeln('foo'); + } + `); + await pollFor(ctx.page, `window.calls`, []); + await ctx.page.evaluate(`window.term.writeln('bar')`); + await pollFor(ctx.page, `window.calls`, [1]); + await ctx.page.evaluate(`window.term.writeln('baz')`); + await pollFor(ctx.page, `window.calls`, [1, 2]); + }); + + test('onSelectionChange', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + window.callCount = 0; + window.term.onSelectionChange(() => window.callCount++); + `); + await pollFor(ctx.page, `window.callCount`, 0); + await ctx.page.evaluate(`window.term.selectAll()`); + await pollFor(ctx.page, `window.callCount`, 1); + await ctx.page.evaluate(`window.term.clearSelection()`); + await pollFor(ctx.page, `window.callCount`, 2); + }); + + test('onRender', async function(): Promise { + await openTerminal(ctx); + await timeout(20); // Ensure all init events are fired + await ctx.page.evaluate(` + window.calls = []; + window.term.onRender(e => window.calls.push([e.start, e.end])); + `); + await pollFor(ctx.page, `window.calls`, []); + await ctx.page.evaluate(`window.term.write('foo')`); + await pollFor(ctx.page, `window.calls`, [[0, 0]]); + await ctx.page.evaluate(`window.term.write('bar\\n\\nbaz')`); + await pollFor(ctx.page, `window.calls`, [[0, 0], [0, 2]]); + }); + + test('onResize', async () => { + await openTerminal(ctx); + await timeout(20); // Ensure all init events are fired + await ctx.page.evaluate(` + window.calls = []; + window.term.onResize(e => window.calls.push([e.cols, e.rows])); + `); + await pollFor(ctx.page, `window.calls`, []); + await ctx.page.evaluate(`window.term.resize(10, 5)`); + await pollFor(ctx.page, `window.calls`, [[10, 5]]); + await ctx.page.evaluate(`window.term.resize(20, 15)`); + await pollFor(ctx.page, `window.calls`, [[10, 5], [20, 15]]); + }); + + test('onTitleChange', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + window.calls = []; + window.term.onTitleChange(e => window.calls.push(e)); + `); + await pollFor(ctx.page, `window.calls`, []); + await ctx.page.evaluate(`window.term.write('\x1b]2;foo\x9c')`); + await pollFor(ctx.page, `window.calls`, ['foo']); + }); + test('onBell', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + window.calls = []; + window.term.onBell(() => window.calls.push(true)); + `); + await pollFor(ctx.page, `window.calls`, []); + await ctx.page.evaluate(`window.term.write('\x07')`); + await pollFor(ctx.page, `window.calls`, [true]); + }); + }); + + test.describe('buffer', () => { + test('cursorX, cursorY', async () => { + await openTerminal(ctx, { rows: 5, cols: 5 }); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.cursorX`), 0); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.cursorY`), 0); + await ctx.proxy.write('foo'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.cursorX`), 3); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.cursorY`), 0); + await ctx.proxy.write('\n'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.cursorX`), 3); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.cursorY`), 1); + await ctx.proxy.write('\r'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.cursorX`), 0); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.cursorY`), 1); + await ctx.proxy.write('abcde'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.cursorX`), 5); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.cursorY`), 1); + await ctx.proxy.write('\n\r\n\n\n\n\n'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.cursorX`), 0); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.cursorY`), 4); + }); + + test('viewportY', async () => { + await openTerminal(ctx, { rows: 5 }); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.viewportY`), 0); + await ctx.proxy.write('\n\n\n\n'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.viewportY`), 0); + await ctx.proxy.write('\n'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.viewportY`), 1); + await ctx.proxy.write('\n\n\n\n'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.viewportY`), 5); + await ctx.page.evaluate(`window.term.scrollLines(-1)`); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.viewportY`), 4); + await ctx.page.evaluate(`window.term.scrollToTop()`); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.viewportY`), 0); + }); + + test('baseY', async () => { + await openTerminal(ctx, { rows: 5 }); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.baseY`), 0); + await ctx.proxy.write('\n\n\n\n'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.baseY`), 0); + await ctx.proxy.write('\n'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.baseY`), 1); + await ctx.proxy.write('\n\n\n\n'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.baseY`), 5); + await ctx.page.evaluate(`window.term.scrollLines(-1)`); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.baseY`), 5); + await ctx.page.evaluate(`window.term.scrollToTop()`); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.baseY`), 5); + }); + + test('length', async () => { + await openTerminal(ctx, { rows: 5 }); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.length`), 5); + await ctx.proxy.write('\n\n\n\n'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.length`), 5); + await ctx.proxy.write('\n'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.length`), 6); + await ctx.proxy.write('\n\n\n\n'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.length`), 10); + }); + + test.describe('getLine', () => { + test('invalid index', async () => { + await openTerminal(ctx, { rows: 5 }); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(-1)`), undefined); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(5)`), undefined); + }); + + test('isWrapped', async () => { + await openTerminal(ctx, { cols: 5 }); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).isWrapped`), false); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(1).isWrapped`), false); + await ctx.proxy.write('abcde'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).isWrapped`), false); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(1).isWrapped`), false); + await ctx.proxy.write('f'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).isWrapped`), false); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(1).isWrapped`), true); + }); + + test('translateToString', async () => { + await openTerminal(ctx, { cols: 5 }); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).translateToString()`), ' '); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).translateToString(true)`), ''); + await ctx.proxy.write('foo'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).translateToString()`), 'foo '); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).translateToString(true)`), 'foo'); + await ctx.proxy.write('bar'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).translateToString()`), 'fooba'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).translateToString(true)`), 'fooba'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(1).translateToString(true)`), 'r'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).translateToString(false, 1)`), 'ooba'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).translateToString(false, 1, 3)`), 'oo'); + }); + + test('getCell', async () => { + await openTerminal(ctx, { cols: 5 }); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).getCell(-1)`), undefined); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).getCell(5)`), undefined); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).getCell(0).getChars()`), ''); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).getCell(0).getWidth()`), 1); + await ctx.proxy.write('a文'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).getCell(0).getChars()`), 'a'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).getCell(0).getWidth()`), 1); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).getCell(1).getChars()`), '文'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).getCell(1).getWidth()`), 2); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).getCell(2).getChars()`), ''); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).getCell(2).getWidth()`), 0); + }); + + test('clearMarkers', async () => { + await openTerminal(ctx, { cols: 5 }); + await ctx.page.evaluate(` + window.disposeStack = []; + `); + await ctx.proxy.write('\n\n\n\n'); + await ctx.proxy.write('\n\n\n\n'); + await ctx.proxy.write('\n\n\n\n'); + await ctx.proxy.write('\n\n\n\n'); + await ctx.page.evaluate(`window.term.registerMarker(1)`); + await ctx.page.evaluate(`window.term.registerMarker(2)`); + await ctx.page.evaluate(`window.term.scrollLines(10)`); + await ctx.page.evaluate(`window.term.registerMarker(3)`); + await ctx.page.evaluate(`window.term.registerMarker(4)`); + await ctx.page.evaluate(` + for (let i = 0; i < window.term.markers.length; ++i) { + const marker = window.term.markers[i]; + marker.onDispose(() => window.disposeStack.push(marker)); + }`); + await ctx.page.evaluate(`window.term.clear()`); + strictEqual(await ctx.page.evaluate(`window.disposeStack.length`), 4); + }); + }); + + test('active, normal, alternate', async () => { + await openTerminal(ctx, { cols: 5 }); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.type`), 'normal'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.normal.type`), 'normal'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.alternate.type`), 'alternate'); + + await ctx.proxy.write('norm '); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).translateToString()`), 'norm '); + strictEqual(await ctx.page.evaluate(`window.term.buffer.normal.getLine(0).translateToString()`), 'norm '); + strictEqual(await ctx.page.evaluate(`window.term.buffer.alternate.getLine(0)`), undefined); + + await ctx.proxy.write('\x1b[?47h\r'); // use alternate screen buffer + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.type`), 'alternate'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.normal.type`), 'normal'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.alternate.type`), 'alternate'); + + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).translateToString()`), ' '); + await ctx.proxy.write('alt '); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).translateToString()`), 'alt '); + strictEqual(await ctx.page.evaluate(`window.term.buffer.normal.getLine(0).translateToString()`), 'norm '); + strictEqual(await ctx.page.evaluate(`window.term.buffer.alternate.getLine(0).translateToString()`), 'alt '); + + await ctx.proxy.write('\x1b[?47l\r'); // use normal screen buffer + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.type`), 'normal'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.normal.type`), 'normal'); + strictEqual(await ctx.page.evaluate(`window.term.buffer.alternate.type`), 'alternate'); + + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.getLine(0).translateToString()`), 'norm '); + strictEqual(await ctx.page.evaluate(`window.term.buffer.normal.getLine(0).translateToString()`), 'norm '); + strictEqual(await ctx.page.evaluate(`window.term.buffer.alternate.getLine(0)`), undefined); + }); + }); + + test.describe('modes', () => { + test.beforeEach(() => openTerminal(ctx)); + test('defaults', async () => { + deepStrictEqual(await ctx.page.evaluate(`window.term.modes`), { + applicationCursorKeysMode: false, + applicationKeypadMode: false, + bracketedPasteMode: false, + insertMode: false, + mouseTrackingMode: 'none', + originMode: false, + reverseWraparoundMode: false, + sendFocusMode: false, + wraparoundMode: true + }); + }); + test('applicationCursorKeysMode', async () => { + await ctx.proxy.write('\x1b[?1h'); + strictEqual(await ctx.page.evaluate(`window.term.modes.applicationCursorKeysMode`), true); + await ctx.proxy.write('\x1b[?1l'); + strictEqual(await ctx.page.evaluate(`window.term.modes.applicationCursorKeysMode`), false); + }); + test('applicationKeypadMode', async () => { + await ctx.proxy.write('\x1b[?66h'); + strictEqual(await ctx.page.evaluate(`window.term.modes.applicationKeypadMode`), true); + await ctx.proxy.write('\x1b[?66l'); + strictEqual(await ctx.page.evaluate(`window.term.modes.applicationKeypadMode`), false); + }); + test('bracketedPasteMode', async () => { + await ctx.proxy.write('\x1b[?2004h'); + strictEqual(await ctx.page.evaluate(`window.term.modes.bracketedPasteMode`), true); + await ctx.proxy.write('\x1b[?2004l'); + strictEqual(await ctx.page.evaluate(`window.term.modes.bracketedPasteMode`), false); + }); + test('insertMode', async () => { + await ctx.proxy.write('\x1b[4h'); + strictEqual(await ctx.page.evaluate(`window.term.modes.insertMode`), true); + await ctx.proxy.write('\x1b[4l'); + strictEqual(await ctx.page.evaluate(`window.term.modes.insertMode`), false); + }); + test('mouseTrackingMode', async () => { + await ctx.proxy.write('\x1b[?9h'); + strictEqual(await ctx.page.evaluate(`window.term.modes.mouseTrackingMode`), 'x10'); + await ctx.proxy.write('\x1b[?9l'); + strictEqual(await ctx.page.evaluate(`window.term.modes.mouseTrackingMode`), 'none'); + await ctx.proxy.write('\x1b[?1000h'); + strictEqual(await ctx.page.evaluate(`window.term.modes.mouseTrackingMode`), 'vt200'); + await ctx.proxy.write('\x1b[?1000l'); + strictEqual(await ctx.page.evaluate(`window.term.modes.mouseTrackingMode`), 'none'); + await ctx.proxy.write('\x1b[?1002h'); + strictEqual(await ctx.page.evaluate(`window.term.modes.mouseTrackingMode`), 'drag'); + await ctx.proxy.write('\x1b[?1002l'); + strictEqual(await ctx.page.evaluate(`window.term.modes.mouseTrackingMode`), 'none'); + await ctx.proxy.write('\x1b[?1003h'); + strictEqual(await ctx.page.evaluate(`window.term.modes.mouseTrackingMode`), 'any'); + await ctx.proxy.write('\x1b[?1003l'); + strictEqual(await ctx.page.evaluate(`window.term.modes.mouseTrackingMode`), 'none'); + }); + test('originMode', async () => { + await ctx.proxy.write('\x1b[?6h'); + strictEqual(await ctx.page.evaluate(`window.term.modes.originMode`), true); + await ctx.proxy.write('\x1b[?6l'); + strictEqual(await ctx.page.evaluate(`window.term.modes.originMode`), false); + }); + test('reverseWraparoundMode', async () => { + await ctx.proxy.write('\x1b[?45h'); + strictEqual(await ctx.page.evaluate(`window.term.modes.reverseWraparoundMode`), true); + await ctx.proxy.write('\x1b[?45l'); + strictEqual(await ctx.page.evaluate(`window.term.modes.reverseWraparoundMode`), false); + }); + test('sendFocusMode', async () => { + await ctx.proxy.write('\x1b[?1004h'); + strictEqual(await ctx.page.evaluate(`window.term.modes.sendFocusMode`), true); + await ctx.proxy.write('\x1b[?1004l'); + strictEqual(await ctx.page.evaluate(`window.term.modes.sendFocusMode`), false); + }); + test('wraparoundMode', async () => { + await ctx.proxy.write('\x1b[?7h'); + strictEqual(await ctx.page.evaluate(`window.term.modes.wraparoundMode`), true); + await ctx.proxy.write('\x1b[?7l'); + strictEqual(await ctx.page.evaluate(`window.term.modes.wraparoundMode`), false); + }); + }); + + test('dispose', async () => { + await ctx.page.evaluate(` + if ('term' in window) { + try { + window.term.dispose(); + } catch {} + } + window.term = new Terminal(); + window.term.dispose(); + `); + strictEqual(await ctx.page.evaluate(`window.term._core._isDisposed`), true); + }); + + test('dispose (opened)', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + if ('term' in window) { + try { + window.term.dispose(); + } catch {} + } + `); + strictEqual(await ctx.page.evaluate(`window.term._core._isDisposed`), true); + }); + + test('render when visible after hidden', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(` + if ('term' in window) { + try { + window.term.dispose(); + } catch {} + } + `); + await ctx.page.evaluate(`document.querySelector('#terminal-container').style.display='none'`); + await ctx.page.evaluate(`window.term = new Terminal()`); + await ctx.page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`); + await ctx.page.evaluate(`document.querySelector('#terminal-container').style.display=''`); + await pollFor(ctx.page, `window.term._core._renderService.dimensions.css.cell.width > 0`, true); + }); + + test.describe('registerDecoration', () => { + test.describe('bufferDecorations', () => { + test('should register decorations and render them when terminal open is called', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(`window.marker1 = window.term.registerMarker(1)`); + await ctx.page.evaluate(`window.marker2 = window.term.registerMarker(2)`); + await ctx.page.evaluate(`window.term.registerDecoration({ marker: window.marker1 })`); + await ctx.page.evaluate(`window.term.registerDecoration({ marker: window.marker2 })`); + await pollFor(ctx.page, `document.querySelectorAll('.xterm-screen .xterm-decoration').length`, 2); + }); + test('should return undefined when the marker has already been disposed of', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(`window.marker = window.term.registerMarker(1)`); + await ctx.page.evaluate(`window.marker.dispose()`); + await pollFor(ctx.page, `window.decoration = window.term.registerDecoration({ marker: window.marker });`, undefined); + }); + test('should throw when a negative x offset is provided', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(`window.marker = window.term.registerMarker(1)`); + await ctx.page.evaluate(` + try { + window.decoration = window.term.registerDecoration({ marker: window.marker, x: -2 }); + } catch (e) { + window.throwMessage = e.message; + } + `); + await pollFor(ctx.page, 'window.throwMessage', 'This API only accepts positive integers'); + }); + }); + test.describe('overviewRulerDecorations', () => { + test('should not add an overview ruler when width is not set', async () => { + await openTerminal(ctx); + await ctx.page.evaluate(`window.marker1 = window.term.registerMarker(1)`); + await ctx.page.evaluate(`window.marker2 = window.term.registerMarker(2)`); + await ctx.page.evaluate(`window.term.registerDecoration({ marker: window.marker1, overviewRulerOptions: { color: 'red', position: 'full' } })`); + await ctx.page.evaluate(`window.term.registerDecoration({ marker: window.marker2, overviewRulerOptions: { color: 'blue', position: 'full' } })`); + await pollFor(ctx.page, `document.querySelectorAll('.xterm-decoration-overview-ruler').length`, 0); + }); + test('should add an overview ruler when width is set', async () => { + await openTerminal(ctx, { overviewRulerWidth: 15 }); + await ctx.page.evaluate(`window.marker1 = window.term.registerMarker(1)`); + await ctx.page.evaluate(`window.marker2 = window.term.registerMarker(2)`); + await ctx.page.evaluate(`window.term.registerDecoration({ marker: window.marker1, overviewRulerOptions: { color: 'red', position: 'full' } })`); + await ctx.page.evaluate(`window.term.registerDecoration({ marker: window.marker2, overviewRulerOptions: { color: 'blue', position: 'full' } })`); + await pollFor(ctx.page, `document.querySelectorAll('.xterm-decoration-overview-ruler').length`, 1); + }); + }); + }); + + test.describe('registerLinkProvider', () => { + test('should fire provideLinks when hovering cells', async () => { + await openTerminal(ctx); + // Focus the terminal as the cursor will show and trigger a rerender, which can clear the + // active link + await ctx.proxy.focus(); + await ctx.page.evaluate(` + window.calls = []; + window.disposable = window.term.registerLinkProvider({ + provideLinks: (position, cb) => { + calls.push(position); + cb(undefined); + } + }); + `); + const dims = await getDimensions(); + await moveMouseCell(dims, 1, 1); + await moveMouseCell(dims, 2, 2); + await moveMouseCell(dims, 10, 4); + await pollFor(ctx.page, `window.calls`, [1, 2, 4]); + await ctx.page.evaluate(`window.disposable.dispose()`); + }); + + test('should fire hover and leave events on the link', async () => { + await openTerminal(ctx); + // Focus the terminal as the cursor will show and trigger a rerender, which can clear the + // active link + await ctx.page.evaluate('window.term.focus()'); + await ctx.proxy.write('foo bar baz'); + // Wait for renderer to catch up as links are cleared on render + await pollFor(ctx.page, `document.querySelector('.xterm-rows').textContent`, 'foo bar baz '); + await ctx.page.evaluate(` + window.calls = []; + window.disposable = window.term.registerLinkProvider({ + provideLinks: (position, cb) => { + window.calls.push('provide ' + position); + if (position === 1) { + window.calls.push('match'); + cb([{ + range: { start: { x: 5, y: 1 }, end: { x: 7, y: 1 } }, + text: 'bar', + activate: () => window.calls.push('activate'), + hover: () => window.calls.push('hover'), + leave: () => window.calls.push('leave') + }]); + } + } + }); + `); + const dims = await getDimensions(); + await moveMouseCell(dims, 5, 1); + await timeout(100); + await moveMouseCell(dims, 4, 1); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'match', 'hover', 'leave' ]); + await moveMouseCell(dims, 7, 1); + await timeout(100); + await moveMouseCell(dims, 8, 1); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'match', 'hover', 'leave', 'hover', 'leave']); + await ctx.page.evaluate(`window.disposable.dispose()`); + }); + + test('should work fine when hover and leave callbacks are not provided', async () => { + await openTerminal(ctx); + // Focus the terminal as the cursor will show and trigger a rerender, which can clear the + // active link + await ctx.page.evaluate('window.term.focus()'); + await ctx.proxy.write('foo bar baz'); + // Wait for renderer to catch up as links are cleared on render + await pollFor(ctx.page, `document.querySelector('.xterm-rows').textContent`, 'foo bar baz '); + await ctx.page.evaluate(` + window.calls = []; + window.disposable = window.term.registerLinkProvider({ + provideLinks: (position, cb) => { + window.calls.push('provide ' + position); + if (position === 1) { + window.calls.push('match 1'); + cb([{ + range: { start: { x: 5, y: 1 }, end: { x: 7, y: 1 } }, + text: 'bar', + activate: () => window.calls.push('activate') + }]); + } else if (position === 2) { + window.calls.push('match 2'); + cb([{ + range: { start: { x: 5, y: 2 }, end: { x: 7, y: 2 } }, + text: 'bar', + activate: () => window.calls.push('activate') + }]); + } + } + }); + `); + const dims = await getDimensions(); + await moveMouseCell(dims, 5, 1); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'match 1']); + await moveMouseCell(dims, 4, 2); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'match 1', 'provide 2', 'match 2']); + await moveMouseCell(dims, 7, 1); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'match 1', 'provide 2', 'match 2', 'provide 1', 'match 1']); + await moveMouseCell(dims, 6, 2); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'match 1', 'provide 2', 'match 2', 'provide 1', 'match 1', 'provide 2', 'match 2']); + await ctx.page.evaluate(`window.disposable.dispose()`); + }); + + test('should fire activate events when clicking the link', async () => { + await openTerminal(ctx); + // Focus the terminal as the cursor will show and trigger a rerender, which can clear the + // active link + await ctx.page.evaluate('window.term.focus()'); + await ctx.proxy.write('a b c'); + // Wait for renderer to catch up as links are cleared on render + await pollFor(ctx.page, `document.querySelector('.xterm-rows').textContent`, 'a b c '); + await ctx.page.evaluate(` + window.calls = []; + window.disposable = window.term.registerLinkProvider({ + provideLinks: (y, cb) => { + window.calls.push('provide ' + y); + cb([{ + range: { start: { x: 1, y }, end: { x: 80, y } }, + text: window.term.buffer.active.getLine(y - 1).translateToString(), + activate: (_, text) => window.calls.push('activate ' + y), + hover: () => window.calls.push('hover ' + y), + leave: () => window.calls.push('leave ' + y) + }]); + } + }); + `); + const dims = await getDimensions(); + await moveMouseCell(dims, 3, 1); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'hover 1']); + await ctx.page.mouse.down(); + await ctx.page.mouse.up(); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'hover 1', 'activate 1']); + await moveMouseCell(dims, 1, 2); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'hover 1', 'activate 1', 'leave 1', 'provide 2', 'hover 2']); + await ctx.page.mouse.down(); + await ctx.page.mouse.up(); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'hover 1', 'activate 1', 'leave 1', 'provide 2', 'hover 2', 'activate 2']); + await moveMouseCell(dims, 5, 1); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'hover 1', 'activate 1', 'leave 1', 'provide 2', 'hover 2', 'activate 2', 'leave 2', 'provide 1', 'hover 1']); + await ctx.page.mouse.down(); + await ctx.page.mouse.up(); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'hover 1', 'activate 1', 'leave 1', 'provide 2', 'hover 2', 'activate 2', 'leave 2', 'provide 1', 'hover 1', 'activate 1']); + await ctx.page.evaluate(`window.disposable.dispose()`); + }); + + test('should work when multiple links are provided on the same line', async () => { + await openTerminal(ctx); + // Focus the terminal as the cursor will show and trigger a rerender, which can clear the + // active link + await ctx.page.evaluate('window.term.focus()'); + await ctx.proxy.write('foo bar baz'); + // Wait for renderer to catch up as links are cleared on render + await pollFor(ctx.page, `document.querySelector('.xterm-rows').textContent`, 'foo bar baz '); + await ctx.page.evaluate(` + window.calls = []; + window.disposable = window.term.registerLinkProvider({ + provideLinks: (position, cb) => { + window.calls.push('provide ' + position); + if (position === 1) { + cb([{ + range: { start: { x: 1, y: 1 }, end: { x: 3, y: 1 } }, + text: '', + activate: () => window.calls.push('activate'), + hover: () => window.calls.push('hover 1-3'), + leave: () => window.calls.push('leave 1-3') + }, { + range: { start: { x: 5, y: 1 }, end: { x: 7, y: 1 } }, + text: '', + activate: () => window.calls.push('activate'), + hover: () => window.calls.push('hover 5-7'), + leave: () => window.calls.push('leave 5-7') + }, { + range: { start: { x: 9, y: 1 }, end: { x: 11, y: 1 } }, + text: '', + activate: () => window.calls.push('activate'), + hover: () => window.calls.push('hover 9-11'), + leave: () => window.calls.push('leave 9-11') + }]); + } + } + }); + `); + const dims = await getDimensions(); + await moveMouseCell(dims, 2, 1); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'hover 1-3']); + await moveMouseCell(dims, 6, 1); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'hover 1-3', 'leave 1-3', 'hover 5-7']); + await moveMouseCell(dims, 6, 2); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'hover 1-3', 'leave 1-3', 'hover 5-7', 'leave 5-7', 'provide 2']); + await moveMouseCell(dims, 10, 1); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'hover 1-3', 'leave 1-3', 'hover 5-7', 'leave 5-7', 'provide 2', 'provide 1', 'hover 9-11']); + await ctx.page.evaluate(`window.disposable.dispose()`); + }); + + test('should dispose links when hovering away', async () => { + await openTerminal(ctx); + // Focus the terminal as the cursor will show and trigger a rerender, which can clear the + // active link + await ctx.page.evaluate('window.term.focus()'); + await ctx.proxy.write('foo bar baz'); + // Wait for renderer to catch up as links are cleared on render + await pollFor(ctx.page, `document.querySelector('.xterm-rows').textContent`, 'foo bar baz '); + await ctx.page.evaluate(` + window.calls = []; + window.disposable = window.term.registerLinkProvider({ + provideLinks: (position, cb) => { + window.calls.push('provide ' + position); + if (position === 1) { + cb([{ + range: { start: { x: 1, y: 1 }, end: { x: 3, y: 1 } }, + text: '', + activate: () => window.calls.push('activate'), + dispose: () => window.calls.push('dispose 1-3'), + hover: () => window.calls.push('hover 1-3'), + leave: () => window.calls.push('leave 1-3') + }, { + range: { start: { x: 5, y: 1 }, end: { x: 7, y: 1 } }, + text: '', + activate: () => window.calls.push('activate'), + dispose: () => window.calls.push('dispose 5-7'), + hover: () => window.calls.push('hover 5-7'), + leave: () => window.calls.push('leave 5-7') + }, { + range: { start: { x: 9, y: 1 }, end: { x: 11, y: 1 } }, + text: '', + activate: () => window.calls.push('activate'), + dispose: () => window.calls.push('dispose 9-11'), + hover: () => window.calls.push('hover 9-11'), + leave: () => window.calls.push('leave 9-11') + }]); + } + } + }); + `); + const dims = await getDimensions(); + await moveMouseCell(dims, 2, 1); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'hover 1-3']); + await moveMouseCell(dims, 6, 1); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'hover 1-3', 'leave 1-3', 'hover 5-7']); + await moveMouseCell(dims, 6, 2); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'hover 1-3', 'leave 1-3', 'hover 5-7', 'leave 5-7', 'dispose 1-3', 'dispose 5-7', 'dispose 9-11', 'provide 2']); + await moveMouseCell(dims, 10, 1); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'hover 1-3', 'leave 1-3', 'hover 5-7', 'leave 5-7', 'dispose 1-3', 'dispose 5-7', 'dispose 9-11', 'provide 2', 'provide 1', 'hover 9-11']); + await moveMouseCell(dims, 10, 2); + await pollFor(ctx.page, `window.calls`, ['provide 1', 'hover 1-3', 'leave 1-3', 'hover 5-7', 'leave 5-7', 'dispose 1-3', 'dispose 5-7', 'dispose 9-11', 'provide 2', 'provide 1', 'hover 9-11', 'leave 9-11', 'dispose 1-3', 'dispose 5-7', 'dispose 9-11', 'provide 2']); + await ctx.page.evaluate(`window.disposable.dispose()`); + }); + }); +}); + +interface IDimensions { + top: number; + left: number; + renderDimensions: IRenderDimensions; +} + +async function getDimensions(): Promise { + return await ctx.page.evaluate(` + (function() { + const rect = document.querySelector('.xterm-rows').getBoundingClientRect(); + return { + top: rect.top, + left: rect.left, + renderDimensions: window.term._core._renderService.dimensions + }; + })(); + `); +} + +async function getCellCoordinates(dimensions: IDimensions, col: number, row: number): Promise<{ x: number, y: number }> { + return { + x: dimensions.left + dimensions.renderDimensions.device.cell.width * (col - 0.5), + y: dimensions.top + dimensions.renderDimensions.device.cell.height * (row - 0.5) + }; +} + +async function moveMouseCell(dimensions: IDimensions, col: number, row: number): Promise { + const coords = await getCellCoordinates(dimensions, col, row); + await ctx.page.mouse.move(coords.x, coords.y); +} diff --git a/test/playwright/TestUtils.ts b/test/playwright/TestUtils.ts index 5111612e..0ab95b76 100644 --- a/test/playwright/TestUtils.ts +++ b/test/playwright/TestUtils.ts @@ -4,13 +4,13 @@ */ import { Browser, JSHandle, Page } from '@playwright/test'; -import { deepStrictEqual } from 'assert'; +import { deepStrictEqual, strictEqual } from 'assert'; import type { IRenderDimensions } from 'browser/renderer/shared/Types'; import type { IRenderService } from 'browser/services/Services'; import type { ICoreTerminal, IMarker } from 'common/Types'; import * as playwright from '@playwright/test'; import { PageFunction } from 'playwright-core/types/structs'; -import { IBuffer, IBufferCell, IBufferLine, IBufferNamespace, IBufferRange, IDecoration, IDecorationOptions, IModes, ITerminalOptions, Terminal } from 'xterm'; +import { IBuffer, IBufferCell, IBufferLine, IBufferNamespace, IBufferRange, IDecoration, IDecorationOptions, IModes, ITerminalInitOnlyOptions, ITerminalOptions, Terminal } from 'xterm'; import { EventEmitter } from '../../out/common/EventEmitter'; // TODO: We could avoid needing this import deepEqual = require('deep-equal'); @@ -353,12 +353,19 @@ class TerminalCoreProxy { } } -export async function openTerminal(ctx: ITestContext, options: ITerminalOptions = {}): Promise { +export async function openTerminal(ctx: ITestContext, options: ITerminalOptions | ITerminalInitOnlyOptions = {}): Promise { await ctx.page.evaluate(` - if ('term' in window) { + if ('term' in window) { + try { window.term.dispose(); - } - window.term = new Terminal(${JSON.stringify(options)}); + } catch {} + } + `); + // HACK: Tests may have side effects that could cause the terminal not to be removed. This + // assertion catches this case early. + strictEqual(await ctx.page.evaluate(`document.querySelector('#terminal-container').children.length`), 0, 'there must be no terminals on the page'); + await ctx.page.evaluate(` + window.term = new Terminal(${JSON.stringify({ allowProposedApi: true, ...options })}); window.term.open(document.querySelector('#terminal-container')); `); await ctx.page.waitForSelector('.xterm-rows');