diff --git a/test/api/MouseTracking.api.ts b/test/api/MouseTracking.api.ts new file mode 100644 index 00000000..d512f11c --- /dev/null +++ b/test/api/MouseTracking.api.ts @@ -0,0 +1,1167 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import * as puppeteer from 'puppeteer'; +import { assert } from 'chai'; +import { ITerminalOptions } from 'xterm'; + +const APP = 'http://127.0.0.1:3000/test'; + +let browser: puppeteer.Browser; +let page: puppeteer.Page; +const width = 1024; +const height = 768; + +// adjust terminal row/col size so we can test +// >80 up to 223 and >255 +const fontSize = 6; +const cols = 260; +const rows = 50; + +/** + * Helper functions. + */ +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'); + } +} + +async function resetMouseModes(): Promise { + return await page.evaluate(` + window.term.write('\x1b[?9l\x1b[?1000l\x1b[?1001l\x1b[?1002l\x1b[?1003l'); + window.term.write('\x1b[?1005l\x1b[?1006l\x1b[?1015l'); + `); +} + +async function getReports(encoding: string): Promise { + const reports = await page.evaluate(`window.calls`); + await page.evaluate(`window.calls = [];`); + return reports.map((report: number[]) => parseReport(encoding, report)); +} + +// translate cell positions into pixel offset +// always adds +2 in each direction so we dont end up in the wrong cell +// due to rounding issues +async function cellPos(col: number, row: number): Promise { + const coords = await page.evaluate(` + (function() { + const rect = window.term.element.getBoundingClientRect(); + const dim = term._core._renderService.dimensions; + return {left: rect.left, top: rect.top, bottom: rect.bottom, right: rect.right, width: dim.actualCellWidth, height: dim.actualCellHeight}; + })(); + `); + return [col * coords.width + coords.left + 2, row * coords.height + coords.top + 2]; +} + +/** + * Patched puppeteer functions. + * This is needed to: + * - translate cell positions into pixel positions + * - allow modifiers to be set + * - fake wheel events + */ +async function mouseMove(col: number, row: number, modifier?: number): Promise { + const [xPixels, yPixels] = await cellPos(col, row); + return await page.mouse.move(xPixels, yPixels); +} +async function mouseClick(col: number, row: number, modifier?: number): Promise { + const [xPixels, yPixels] = await cellPos(col, row); + return await page.mouse.click(xPixels, yPixels); +} +async function mouseDown(button: "left" | "right" | "middle" | undefined, modifier?: number): Promise { + return await page.mouse.down({button}); +} +async function mouseUp(button: "left" | "right" | "middle" | undefined, modifier?: number): Promise { + return await page.mouse.up({button}); +} +async function wheelUp(): Promise { + const self = (page.mouse as any); + return await self._client.send('Input.dispatchMouseEvent', { + type: 'mouseWheel', + x: self._x, + y: self._y, + deltaX: 0, + deltaY: -10 + }); +} +async function wheelDown(): Promise { + const self = (page.mouse as any); + return await self._client.send('Input.dispatchMouseEvent', { + type: 'mouseWheel', + x: self._x, + y: self._y, + deltaX: 0, + deltaY: 10 + }); +} + +// extract button data from buttonCode +function evalButtonCode(code: number) { + // 2 bits: 0 - left, 1 - middle, 2 - right, 3 - release + // higher bits: 4 - shift, 8 - meta, 16 - control + const modifier = {shift: !!(code & 4), meta: !!(code & 8), control: !!(code & 16)}; + const wheel = code & 64; + let action; + let button; + if (wheel) { + action = (code & 1) ? 'down' : 'up'; + button = 'wheel'; + } else { + action = code & 32 ? 'move' : code === 3 ? 'release' : 'press'; + code &= 3; // TODO: more than 3 buttons + wheel + button = code === 0 ? 'left' : code === 1 ? 'middle' : code === 2 ? 'right' : ''; + } + return {button, action, modifier}; +} + +// parse a single mouse report +function parseReport(encoding: string, msg: number[]): {state: any; row: number; col: number;} | string { + let sReport: string; + let buttonCode: number; + let row: number; + let col: number; + // unpack msg + const report = String.fromCharCode.apply(null, msg); + // console.log([report]); + // skip non mouse reports + if (!report || report[0] !== '\x1b') { + return report; + } + switch (encoding) { + case 'DEFAULT': + return { + state: evalButtonCode(report.charCodeAt(3) - 32), + col: report.charCodeAt(4) - 32, + row: report.charCodeAt(5) - 32 + }; + case 'UTF8': + return { + state: evalButtonCode(report.charCodeAt(3) - 32), + col: report.charCodeAt(4) - 32, + row: report.charCodeAt(5) - 32 + }; + case 'SGR': + sReport = report.slice(3, -1); + [buttonCode, col, row] = sReport.split(';').map(el => parseInt(el)); + const state = evalButtonCode(buttonCode); + if (report[report.length - 1] === 'm') { + state.action = 'release'; + } + return {state, row, col}; + case 'URXVT': + sReport = report.slice(2, -1); + [buttonCode, col, row] = sReport.split(';').map(el => parseInt(el)); + return {state: evalButtonCode(buttonCode - 32), row: --row, col: --col}; // FIXME: remove -1 here when fixed! + default: + return { + state: evalButtonCode(report.charCodeAt(3) - 32), + col: report.charCodeAt(4) - 32, + row: report.charCodeAt(5) - 32 + }; + } +} + +/** + * Mouse tracking tests. + */ +describe('Mouse Tracking Tests', function(): void { + this.timeout(20000); + + before(async function(): Promise { + 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); + await openTerminal(); + // patch terminal to get the onData calls + // we encode the msg here to an array of codes to not lose bytes + // (transmission strips non utf8 bytes) + // also resize so we can properly test the edge cases + await page.evaluate(` + window.calls = []; + window.term.onData(e => calls.push( Array.from(e).map(el => el.charCodeAt(0)) )); + window.term.setOption('fontSize', ${fontSize}); + window.term.resize(${cols}, ${rows}); + `); + }); + + describe('DECSET 9 (X10)', async () => { + /** + * X10 protocol: + * - only press events + * - no wheel? (TBD) + * - no move + * - no modifiers? (TDB) + */ + it('default encoding', async () => { + const encoding = 'DEFAULT'; + await resetMouseModes(); + await mouseMove(0, 0); + await page.evaluate(`window.term.write('\x1b[?9h');`); + + // test at 0,0 + await mouseDown('left'); + assert.deepEqual(await getReports(encoding), [{col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + + // mouseup should not report + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), []); + + // mousemove should not report + await mouseMove(50, 10); + assert.deepEqual(await getReports(encoding), []); + await mouseDown('left'); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [{col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + + // test at max rows/cols + // bug: we are capped at col 95 currently + // fix: allow values up to 223, any bigger should drop to 0 + await mouseMove(cols - 1, rows - 1); + await mouseDown('left'); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [{col: 95, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + + // button press/move/release tests + // left button + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await mouseDown('left'); + await mouseMove(44, 24); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + // middle button + // bug: default action not cancelled (adds data to getReports from clipboard under X11) + // await mouseMove(43, 24); + // await getReports(encoding); // clear reports + // await mouseDown('middle'); + // await mouseMove(44, 24); + // await mouseUp('middle'); + // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); + // right button + // bug: default action not cancelled (popup shown) + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await mouseDown('right'); + await mouseMove(44, 24); + await mouseUp('right'); + assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}]); + + // wheel + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await wheelUp(); + assert.deepEqual(await getReports(encoding), []); + await wheelDown(); + assert.deepEqual(await getReports(encoding), []); + + // modifiers + // TODO + }); + it('UTF8 encoding', async () => { + const encoding = 'UTF8'; + await resetMouseModes(); + await mouseMove(0, 0); + await page.evaluate(`window.term.write('\x1b[?9h\x1b[?1005h');`); + + // test at 0,0 + await mouseDown('left'); + assert.deepEqual(await getReports(encoding), [{col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + + // mouseup should not report + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), []); + + // mousemove should not report + await mouseMove(50, 10); + assert.deepEqual(await getReports(encoding), []); + await mouseDown('left'); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [{col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + + // test at max rows/cols + await mouseMove(cols - 1, rows - 1); + await mouseDown('left'); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [{col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + + // button press/move/release tests + // left button + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await mouseDown('left'); + await mouseMove(44, 24); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + // middle button + // bug: default action not cancelled (adds data to getReports from clipboard under X11) + // await mouseMove(43, 24); + // await getReports(encoding); // clear reports + // await mouseDown('middle'); + // await mouseMove(44, 24); + // await mouseUp('middle'); + // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); + // right button + // bug: default action not cancelled (popup shown) + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await mouseDown('right'); + await mouseMove(44, 24); + await mouseUp('right'); + assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}]); + + // wheel + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await wheelUp(); + assert.deepEqual(await getReports(encoding), []); + await wheelDown(); + assert.deepEqual(await getReports(encoding), []); + + // modifiers + // TODO + }); + it('SGR encoding', async () => { + const encoding = 'SGR'; + await resetMouseModes(); + await mouseMove(0, 0); + await page.evaluate(`window.term.write('\x1b[?9h\x1b[?1006h');`); + + // test at 0,0 + await mouseDown('left'); + assert.deepEqual(await getReports(encoding), [{col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + + // mouseup should not report + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), []); + + // mousemove should not report + await mouseMove(50, 10); + assert.deepEqual(await getReports(encoding), []); + await mouseDown('left'); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [{col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + + // test at max rows/cols + await mouseMove(cols - 1, rows - 1); + await mouseDown('left'); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [{col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + + // button press/move/release tests + // left button + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await mouseDown('left'); + await mouseMove(44, 24); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + // middle button + // bug: default action not cancelled (adds data to getReports from clipboard under X11) + // await mouseMove(43, 24); + // await getReports(encoding); // clear reports + // await mouseDown('middle'); + // await mouseMove(44, 24); + // await mouseUp('middle'); + // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); + // right button + // bug: default action not cancelled (popup shown) + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await mouseDown('right'); + await mouseMove(44, 24); + await mouseUp('right'); + assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}]); + + // wheel + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await wheelUp(); + assert.deepEqual(await getReports(encoding), []); + await wheelDown(); + assert.deepEqual(await getReports(encoding), []); + + // modifiers + // TODO + }); + it('URXVT encoding', async () => { + // bug: always reports +1 for row/col (temp. fixed in parseReport to pass tests) + const encoding = 'URXVT'; + await resetMouseModes(); + await mouseMove(0, 0); + await page.evaluate(`window.term.write('\x1b[?9h\x1b[?1015h');`); + + // test at 0,0 + await mouseDown('left'); + assert.deepEqual(await getReports(encoding), [{col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + + // mouseup should not report + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), []); + + // mousemove should not report + await mouseMove(50, 10); + assert.deepEqual(await getReports(encoding), []); + await mouseDown('left'); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [{col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + + // test at max rows/cols + await mouseMove(cols - 1, rows - 1); + await mouseDown('left'); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [{col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + + // button press/move/release tests + // left button + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await mouseDown('left'); + await mouseMove(44, 24); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]); + // middle button + // bug: default action not cancelled (adds data to getReports from clipboard under X11) + // await mouseMove(43, 24); + // await getReports(encoding); // clear reports + // await mouseDown('middle'); + // await mouseMove(44, 24); + // await mouseUp('middle'); + // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); + // right button + // bug: default action not cancelled (popup shown) + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await mouseDown('right'); + await mouseMove(44, 24); + await mouseUp('right'); + assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}]); + + // wheel + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await wheelUp(); + assert.deepEqual(await getReports(encoding), []); + await wheelDown(); + assert.deepEqual(await getReports(encoding), []); + + // modifiers + // TODO + }); + }); + describe('DECSET 1000 (VT200 mouse)', () => { + /** + * VT200 protocol: + * - press and release events + * - wheel up/down + * - no move + * - modifiers? (TBD) + */ + it('default encoding', async () => { + const encoding = 'DEFAULT'; + await resetMouseModes(); + await mouseMove(0, 0); + await page.evaluate(`window.term.write('\x1b[?1000h');`); + + // test at 0,0 + // bug: release is fired immediately - expected: only press event + await mouseDown('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 1, row: 1, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + ]); + + // mouseup should report, encoding cannot report released button + // bug: release already fired thus no event here - expected: release event + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), []); + + // mousemove should not report + await mouseMove(50, 10); + assert.deepEqual(await getReports(encoding), []); + await mouseDown('left'); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 51, row: 11, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, + ]); + + // test at max rows/cols + // bug: we are capped at col 95 currently + // fix: allow values up to 223, any bigger should drop to 0 + await mouseMove(cols - 1, rows - 1); + await mouseDown('left'); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 95, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 95, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, + ]); + + // button press/move/release tests + // left button + // bug: release is fired immediately thus with wrong coords - expected: col in release event should be 45 + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await mouseDown('left'); + await mouseMove(44, 24); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, + ]); + // middle button + // bug: default action not cancelled (adds data to getReports from clipboard under X11) + // await mouseMove(43, 24); + // await getReports(encoding); // clear reports + // await mouseDown('middle'); + // await mouseMove(44, 24); + // await mouseUp('middle'); + // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); + // right button + // bug: default action not cancelled (popup shown) + // bug: release is fired immediately thus with wrong coords - expected: col in release event should be 45 + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await mouseDown('right'); + await mouseMove(44, 24); + await mouseUp('right'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}, + {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, + ]); + + // wheel + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await wheelUp(); + assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + await wheelDown(); + assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + + // modifiers + // TODO + }); + it('UTF8 encoding', async () => { + const encoding = 'UTF8'; + await resetMouseModes(); + await mouseMove(0, 0); + await page.evaluate(`window.term.write('\x1b[?1000h\x1b[?1005h');`); + + // test at 0,0 + // bug: release is fired immediately - expected: only press event + await mouseDown('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 1, row: 1, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + ]); + + // mouseup should report, encoding cannot report released button + // bug: release already fired thus no event here - expected: release event + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), []); + + // mousemove should not report + await mouseMove(50, 10); + assert.deepEqual(await getReports(encoding), []); + await mouseDown('left'); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 51, row: 11, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, + ]); + + // test at max rows/cols + await mouseMove(cols - 1, rows - 1); + await mouseDown('left'); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: cols, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, + ]); + + // button press/move/release tests + // left button + // bug: release is fired immediately thus with wrong coords - expected: col in release event should be 45 + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await mouseDown('left'); + await mouseMove(44, 24); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, + ]); + // middle button + // bug: default action not cancelled (adds data to getReports from clipboard under X11) + // await mouseMove(43, 24); + // await getReports(encoding); // clear reports + // await mouseDown('middle'); + // await mouseMove(44, 24); + // await mouseUp('middle'); + // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); + // right button + // bug: default action not cancelled (popup shown) + // bug: release is fired immediately thus with wrong coords - expected: col in release event should be 45 + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await mouseDown('right'); + await mouseMove(44, 24); + await mouseUp('right'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}, + {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, + ]); + + // wheel + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await wheelUp(); + assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + await wheelDown(); + assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + + // modifiers + // TODO + }); + it('SGR encoding', async () => { + const encoding = 'SGR'; + await resetMouseModes(); + await mouseMove(0, 0); + await page.evaluate(`window.term.write('\x1b[?1000h\x1b[?1006h');`); + + // test at 0,0 + // bug: release is fired immediately - expected: only press event + await mouseDown('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 1, row: 1, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}} + ]); + + // mouseup should report + // bug: release already fired thus no event here - expected: release event + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), []); + + // mousemove should not report + await mouseMove(50, 10); + assert.deepEqual(await getReports(encoding), []); + await mouseDown('left'); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 51, row: 11, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + ]); + + // test at max rows/cols + await mouseMove(cols - 1, rows - 1); + await mouseDown('left'); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: cols, row: rows, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + ]); + + // button press/move/release tests + // left button + // bug: release is fired immediately thus with wrong coords - expected: col in release event should be 45 + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await mouseDown('left'); + await mouseMove(44, 24); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 44, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + ]); + // middle button + // bug: default action not cancelled (adds data to getReports from clipboard under X11) + // await mouseMove(43, 24); + // await getReports(encoding); // clear reports + // await mouseDown('middle'); + // await mouseMove(44, 24); + // await mouseUp('middle'); + // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); + // right button + // bug: default action not cancelled (popup shown) + // bug: release is fired immediately thus with wrong coords - expected: col in release event should be 45 + // bug: release reports wrong button + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await mouseDown('right'); + await mouseMove(44, 24); + await mouseUp('right'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}, + {col: 44, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + ]); + + // wheel + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await wheelUp(); + assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + await wheelDown(); + assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + + // modifiers + // TODO + }); + it('URXVT encoding', async () => { + const encoding = 'URXVT'; + await resetMouseModes(); + await mouseMove(0, 0); + await page.evaluate(`window.term.write('\x1b[?1000h\x1b[?1015h');`); + + // test at 0,0 + // bug: release is fired immediately - expected: only press event + await mouseDown('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 1, row: 1, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + ]); + + // mouseup should report, encoding cannot report released button + // bug: release already fired thus no event here - expected: release event + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), []); + + // mousemove should not report + await mouseMove(50, 10); + assert.deepEqual(await getReports(encoding), []); + await mouseDown('left'); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 51, row: 11, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, + ]); + + // test at max rows/cols + await mouseMove(cols - 1, rows - 1); + await mouseDown('left'); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: cols, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, + ]); + + // button press/move/release tests + // left button + // bug: release is fired immediately thus with wrong coords - expected: col in release event should be 45 + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await mouseDown('left'); + await mouseMove(44, 24); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, + ]); + // middle button + // bug: default action not cancelled (adds data to getReports from clipboard under X11) + // await mouseMove(43, 24); + // await getReports(encoding); // clear reports + // await mouseDown('middle'); + // await mouseMove(44, 24); + // await mouseUp('middle'); + // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); + // right button + // bug: default action not cancelled (popup shown) + // bug: release is fired immediately thus with wrong coords - expected: col in release event should be 45 + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await mouseDown('right'); + await mouseMove(44, 24); + await mouseUp('right'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}, + {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, + ]); + + // wheel + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await wheelUp(); + assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + await wheelDown(); + assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + + // modifiers + // TODO + }); + }); + describe('DECSET 1002 (xterm with drag)', () => { + /** + * VT200 protocol: + * - press and release events + * - wheel up/down + * - move only on press (drag) + * - all modifiers + * Note: tmux runs this with SGR encoding. + */ + it('default encoding', async () => { + const encoding = 'DEFAULT'; + await resetMouseModes(); + await mouseMove(0, 0); + await page.evaluate(`window.term.write('\x1b[?1002h');`); + + // test at 0,0 + // bug: release is fired immediately + await mouseDown('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}} + ]); + + // mouseup should report, encoding cannot report released button + // bug: release already fired thus no event here - expected: release event + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 1, row: 1, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + ]); + + // mousemove should not report + await mouseMove(50, 10); + assert.deepEqual(await getReports(encoding), []); + await mouseDown('left'); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 51, row: 11, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, + ]); + + // test at max rows/cols + // bug: we are capped at col 95 currently + // fix: allow values up to 223, any bigger should drop to 0 + await mouseMove(cols - 1, rows - 1); + await mouseDown('left'); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 95, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 95, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, + ]); + + // button press/move/release tests + // left button + // bug: release is fired immediately thus with wrong coords - expected: col in release event should be 45 + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await mouseDown('left'); + await mouseMove(44, 24); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, + ]); + // middle button + // bug: default action not cancelled (adds data to getReports from clipboard under X11) + // await mouseMove(43, 24); + // await getReports(encoding); // clear reports + // await mouseDown('middle'); + // await mouseMove(44, 24); + // await mouseUp('middle'); + // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); + // right button + // bug: default action not cancelled (popup shown) + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await mouseDown('right'); + await mouseMove(44, 24); + await mouseUp('right'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'move', button: 'right', modifier: {control: false, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, + ]); + + // wheel + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await wheelUp(); + assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + await wheelDown(); + assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + + // modifiers + // TODO + }); + it('UTF8 encoding', async () => { + const encoding = 'UTF8'; + await resetMouseModes(); + await mouseMove(0, 0); + await page.evaluate(`window.term.write('\x1b[?1002h\x1b[?1005h');`); + + // test at 0,0 + // bug: release is fired immediately + await mouseDown('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}} + ]); + + // mouseup should report, encoding cannot report released button + // bug: release already fired thus no event here - expected: release event + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 1, row: 1, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + ]); + + // mousemove should not report + await mouseMove(50, 10); + assert.deepEqual(await getReports(encoding), []); + await mouseDown('left'); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 51, row: 11, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, + ]); + + // test at max rows/cols + await mouseMove(cols - 1, rows - 1); + await mouseDown('left'); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: cols, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, + ]); + + // button press/move/release tests + // left button + // bug: release is fired immediately thus with wrong coords - expected: col in release event should be 45 + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await mouseDown('left'); + await mouseMove(44, 24); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, + ]); + // middle button + // bug: default action not cancelled (adds data to getReports from clipboard under X11) + // await mouseMove(43, 24); + // await getReports(encoding); // clear reports + // await mouseDown('middle'); + // await mouseMove(44, 24); + // await mouseUp('middle'); + // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); + // right button + // bug: default action not cancelled (popup shown) + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await mouseDown('right'); + await mouseMove(44, 24); + await mouseUp('right'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'move', button: 'right', modifier: {control: false, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, + ]); + + // wheel + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await wheelUp(); + assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + await wheelDown(); + assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + + // modifiers + // TODO + }); + it('SGR encoding', async () => { + const encoding = 'SGR'; + await resetMouseModes(); + await mouseMove(0, 0); + await page.evaluate(`window.term.write('\x1b[?1002h\x1b[?1006h');`); + + // test at 0,0 + // bug: release is fired immediately + await mouseDown('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}} + ]); + + // mouseup should report, encoding cannot report released button + // bug: release already fired thus no event here - expected: release event + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 1, row: 1, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}} + ]); + + // mousemove should not report + await mouseMove(50, 10); + assert.deepEqual(await getReports(encoding), []); + await mouseDown('left'); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 51, row: 11, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + ]); + + // test at max rows/cols + await mouseMove(cols - 1, rows - 1); + await mouseDown('left'); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: cols, row: rows, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + ]); + + // button press/move/release tests + // left button + // bug: release is fired immediately thus with wrong coords - expected: col in release event should be 45 + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await mouseDown('left'); + await mouseMove(44, 24); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + ]); + // middle button + // bug: default action not cancelled (adds data to getReports from clipboard under X11) + // await mouseMove(43, 24); + // await getReports(encoding); // clear reports + // await mouseDown('middle'); + // await mouseMove(44, 24); + // await mouseUp('middle'); + // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); + // right button + // bug: default action not cancelled (popup shown) + // bug: release reports wrong button + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await mouseDown('right'); + await mouseMove(44, 24); + await mouseUp('right'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'move', button: 'right', modifier: {control: false, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + ]); + + // wheel + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await wheelUp(); + assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + await wheelDown(); + assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + + // modifiers + // TODO + }); + it('URXVT encoding', async () => { + const encoding = 'URXVT'; + await resetMouseModes(); + await mouseMove(0, 0); + await page.evaluate(`window.term.write('\x1b[?1002h\x1b[?1015h');`); + + // test at 0,0 + // bug: release is fired immediately + await mouseDown('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}} + ]); + + // mouseup should report, encoding cannot report released button + // bug: release already fired thus no event here - expected: release event + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 1, row: 1, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}} + ]); + + // mousemove should not report + await mouseMove(50, 10); + assert.deepEqual(await getReports(encoding), []); + await mouseDown('left'); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 51, row: 11, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, + ]); + + // test at max rows/cols + await mouseMove(cols - 1, rows - 1); + await mouseDown('left'); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: cols, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, + ]); + + // button press/move/release tests + // left button + // bug: release is fired immediately thus with wrong coords - expected: col in release event should be 45 + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await mouseDown('left'); + await mouseMove(44, 24); + await mouseUp('left'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, + ]); + // middle button + // bug: default action not cancelled (adds data to getReports from clipboard under X11) + // await mouseMove(43, 24); + // await getReports(encoding); // clear reports + // await mouseDown('middle'); + // await mouseMove(44, 24); + // await mouseUp('middle'); + // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]); + // right button + // bug: default action not cancelled (popup shown) + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await mouseDown('right'); + await mouseMove(44, 24); + await mouseUp('right'); + assert.deepEqual(await getReports(encoding), [ + {col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'move', button: 'right', modifier: {control: false, shift: false, meta: false}}}, + {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}, + ]); + + // wheel + await mouseMove(43, 24); + await getReports(encoding); // clear reports + await wheelUp(); + assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + await wheelDown(); + assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]); + + // modifiers + // TODO + }); + }); + describe('DECSET 1003 (xterm any event)', () => { + /** + * VT200 protocol: + * - all events (press, release, wheel, move) + * - all modifiers + */ + // bug: currently same reports as 1002, FIXME: implement tests once fixed + }); +});