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');
+ }
+}