diff --git a/azure-pipelines.yml b/azure-pipelines.yml index db74a4ea..fd65a6a8 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -21,7 +21,12 @@ jobs: displayName: 'Install dependencies and build' - script: | yarn mocha - displayName: 'Test' + displayName: 'Unit tests' + - script: | + yarn start & + sleep 5 + yarn test-api --headless + displayName: 'Integration tests' - script: | yarn lint displayName: 'Lint' @@ -39,7 +44,7 @@ jobs: displayName: 'Install dependencies and build' - script: | yarn mocha - displayName: 'Test' + displayName: 'Unit tests' - script: | yarn lint displayName: 'Lint' @@ -62,7 +67,7 @@ jobs: displayName: 'Install dependencies and build' - script: | yarn mocha - displayName: 'Test' + displayName: 'Unit tests' - script: | yarn lint displayName: 'Lint' diff --git a/demo/client.ts b/demo/client.ts index 90a913d7..b7409109 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -21,6 +21,7 @@ import { Terminal as TerminalType, ITerminalOptions } from 'xterm'; export interface IWindowWithTerminal extends Window { term: TerminalType; + Terminal?: typeof TerminalType; } declare let window: IWindowWithTerminal; @@ -57,8 +58,6 @@ function getSearchOptions(): ISearchOptions { }; } -createTerminal(); - const disposeRecreateButtonHandler = () => { // If the terminal exists dispose of it, otherwise recreate it if (term) { @@ -74,7 +73,12 @@ const disposeRecreateButtonHandler = () => { } }; -document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler); +if (document.location.pathname === '/test') { + window.Terminal = Terminal; +} else { + createTerminal(); + document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler); +} function createTerminal(): void { // Clean terminal diff --git a/demo/server.js b/demo/server.js index 758023c7..8270a398 100644 --- a/demo/server.js +++ b/demo/server.js @@ -16,6 +16,10 @@ function startServer() { res.sendFile(__dirname + '/index.html'); }); + app.get('/test', function(req, res){ + res.sendFile(__dirname + '/test.html'); + }); + app.get('/style.css', function(req, res){ res.sendFile(__dirname + '/style.css'); }); diff --git a/demo/test.html b/demo/test.html new file mode 100644 index 00000000..275c542d --- /dev/null +++ b/demo/test.html @@ -0,0 +1,12 @@ + + + + xterm.js integration test fixture + + + + +
+ + + diff --git a/package.json b/package.json index 1727af82..68d72a1b 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@types/jsdom": "11.0.1", "@types/mocha": "^2.2.33", "@types/node": "6.0.108", + "@types/puppeteer": "^1.12.4", "@types/webpack": "^4.4.11", "browserify": "^13.3.0", "chai": "3.5.0", @@ -32,6 +33,7 @@ "node-pty": "0.7.6", "nodemon": "1.10.2", "nyc": "^11.8.0", + "puppeteer": "^1.15.0", "sorcery": "^0.10.0", "source-map-loader": "^0.2.4", "ts-loader": "^4.5.0", @@ -54,6 +56,7 @@ "test-debug": "node --inspect-brk node_modules/.bin/gulp test", "test-suite": "gulp mocha-suite --test", "test-coverage": "nyc -x gulpfile.js -x '**/*test*' npm run mocha", + "test-api": "mocha \"**/*.api.js\"", "mocha": "gulp test", "prebuild": "tsc -b ./src/tsconfig.all.json", "build": "gulp build", diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index dabcc9a4..39d6c3a4 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -246,6 +246,7 @@ export class SelectionManager implements ISelectionManager { this._model.clearSelection(); this._removeMouseDownListeners(); this.refresh(); + this._onSelectionChange.fire(); } /** diff --git a/src/public/Terminal.api.ts b/src/public/Terminal.api.ts new file mode 100644 index 00000000..380cc89c --- /dev/null +++ b/src/public/Terminal.api.ts @@ -0,0 +1,244 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import * as puppeteer from 'puppeteer'; +import { assert } from 'chai'; +import { ITerminalOptions } from '../Types'; + +const APP = 'http://127.0.0.1:3000/test'; + +let browser: puppeteer.Browser; +let page: puppeteer.Page; +const width = 800; +const height = 600; + +describe('API Integration Tests', () => { + before(async function(): Promise { + this.timeout(10000); + browser = await puppeteer.launch({ + headless: process.argv.indexOf('--headless') !== -1, + slowMo: 80, + args: [`--window-size=${width},${height}`] + }); + page = (await browser.pages())[0]; + await page.setViewport({ width, height }); + }); + + after(() => { + browser.close(); + }); + + beforeEach(async () => { + await page.goto(APP); + }); + + it('Default options', async function(): Promise { + this.timeout(10000); + await openTerminal(); + assert.equal(await page.evaluate(`window.term.cols`), 80); + assert.equal(await page.evaluate(`window.term.rows`), 24); + }); + + it('write', async function(): Promise { + this.timeout(10000); + await openTerminal(); + await page.evaluate(` + window.term.write('foo'); + window.term.write('bar'); + `); + assert.equal(await page.evaluate(`window.term._core.buffer.translateBufferLineToString(0, true)`), 'foobar'); + }); + + it('writeln', async function(): Promise { + this.timeout(10000); + await openTerminal(); + await page.evaluate(` + window.term.writeln('foo'); + window.term.writeln('bar'); + `); + assert.equal(await page.evaluate(`window.term._core.buffer.translateBufferLineToString(0, true)`), 'foo'); + assert.equal(await page.evaluate(`window.term._core.buffer.translateBufferLineToString(1, true)`), 'bar'); + }); + + it('clear', async function(): Promise { + this.timeout(10000); + await openTerminal({ rows: 5 }); + await page.evaluate(` + window.term.write('test0'); + for (let i = 1; i < 10; i++) { + window.term.write('\\n\\rtest' + i); + } + `); + await page.evaluate(`window.term.clear()`); + assert.equal(await page.evaluate(`window.term._core.buffer.lines.length`), '5'); + assert.equal(await page.evaluate(`window.term._core.buffer.translateBufferLineToString(0, true)`), 'test9'); + for (let i = 1; i < 5; i++) { + assert.equal(await page.evaluate(`window.term._core.buffer.translateBufferLineToString(${i}, true)`), ''); + } + }); + + it('getOption, setOption', async function(): Promise { + this.timeout(10000); + await openTerminal(); + assert.equal(await page.evaluate(`window.term.getOption('rendererType')`), 'canvas'); + await page.evaluate(`window.term.setOption('rendererType', 'dom')`); + assert.equal(await page.evaluate(`window.term.getOption('rendererType')`), 'dom'); + }); + + it('selection', async function(): Promise { + this.timeout(10000); + await openTerminal({ rows: 5 }); + await page.evaluate(`window.term.write('\\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()`), ''); + await page.evaluate(`window.term.selectAll()`); + assert.equal(await page.evaluate(`window.term.hasSelection()`), true); + assert.equal(await page.evaluate(`window.term.getSelection()`), '\n\nfoo\n\nbar\n\nbaz'); + await page.evaluate(`window.term.clearSelection()`); + assert.equal(await page.evaluate(`window.term.hasSelection()`), false); + assert.equal(await page.evaluate(`window.term.getSelection()`), ''); + }); + + it('focus, blur', async function(): Promise { + this.timeout(10000); + await openTerminal(); + 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('Events', () => { + it('onCursorMove', async function(): Promise { + this.timeout(10000); + await openTerminal(); + await page.evaluate(` + window.callCount = 0; + window.term.onCursorMove(e => window.callCount++); + window.term.write('foo'); + `); + assert.equal(await page.evaluate(`window.callCount`), 1); + await page.evaluate(`window.term.write('bar')`); + assert.equal(await page.evaluate(`window.callCount`), 2); + }); + + it('onData', async function(): Promise { + this.timeout(10000); + await openTerminal(); + 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 function(): Promise { + this.timeout(10000); + await openTerminal(); + 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 function(): Promise { + this.timeout(10000); + await openTerminal(); + await page.evaluate(` + window.callCount = 0; + window.term.onLineFeed(() => callCount++); + window.term.writeln('foo'); + `); + assert.equal(await page.evaluate(`window.callCount`), 1); + await page.evaluate(`window.term.writeln('bar')`); + assert.equal(await page.evaluate(`window.callCount`), 2); + }); + + it('onScroll', async function(): Promise { + this.timeout(10000); + await openTerminal({ 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'); + } + `); + assert.deepEqual(await page.evaluate(`window.calls`), []); + await page.evaluate(`window.term.writeln('bar')`); + assert.deepEqual(await page.evaluate(`window.calls`), [1]); + await page.evaluate(`window.term.writeln('baz')`); + assert.deepEqual(await page.evaluate(`window.calls`), [1, 2]); + }); + + it('onSelectionChange', async function(): Promise { + this.timeout(10000); + await openTerminal(); + await page.evaluate(` + window.callCount = 0; + window.term.onSelectionChange(() => window.callCount++); + `); + assert.equal(await page.evaluate(`window.callCount`), 0); + await page.evaluate(`window.term.selectAll()`); + assert.equal(await page.evaluate(`window.callCount`), 1); + await page.evaluate(`window.term.clearSelection()`); + assert.equal(await page.evaluate(`window.callCount`), 2); + }); + + it('onRender', async function(): Promise { + this.timeout(10000); + await openTerminal(); + await page.evaluate(` + window.calls = []; + window.term.onRender(e => window.calls.push([e.start, e.end])); + `); + assert.deepEqual(await page.evaluate(`window.calls`), []); + await page.evaluate(`window.term.write('foo')`); + assert.deepEqual(await page.evaluate(`window.calls`), [[0, 0]]); + await page.evaluate(`window.term.write('bar\\n\\nbaz')`); + assert.deepEqual(await page.evaluate(`window.calls`), [[0, 0], [0, 2]]); + }); + + it('onResize', async function(): Promise { + this.timeout(10000); + await openTerminal(); + await page.evaluate(` + window.calls = []; + window.term.onResize(e => window.calls.push([e.cols, e.rows])); + `); + assert.deepEqual(await page.evaluate(`window.calls`), []); + await page.evaluate(`window.term.resize(10, 5)`); + assert.deepEqual(await page.evaluate(`window.calls`), [[10, 5]]); + await page.evaluate(`window.term.resize(20, 15)`); + assert.deepEqual(await page.evaluate(`window.calls`), [[10, 5], [20, 15]]); + }); + + it('onTitleChange', async function(): Promise { + this.timeout(10000); + await openTerminal(); + await page.evaluate(` + window.calls = []; + window.term.onTitleChange(e => window.calls.push(e)); + `); + assert.deepEqual(await page.evaluate(`window.calls`), []); + await page.evaluate(`window.term.write('\\x1b]2;foo\\x9c')`); + assert.deepEqual(await page.evaluate(`window.calls`), ['foo']); + }); + }); +}); + +async function openTerminal(options: ITerminalOptions = {}): Promise { + await page.evaluate(`window.term = new Terminal(${JSON.stringify(options)})`); + await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`); + if (options.rendererType === 'dom') { + await page.waitForSelector('.xterm-rows'); + } else { + await page.waitForSelector('.xterm-text-layer'); + } +}