diff --git a/src/Interfaces.ts b/src/Interfaces.ts index f97d9294..f4ae917f 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -158,6 +158,9 @@ export interface IBufferSet { export interface IViewport { syncScrollArea(): void; + onWheel(ev: WheelEvent): void; + onTouchStart(ev: TouchEvent): void; + onTouchMove(ev: TouchEvent): void; } export interface ISelectionManager { @@ -171,6 +174,14 @@ export interface ISelectionManager { setSelection(row: number, col: number, length: number): void; } +export interface ICompositionHelper { + compositionstart(): void; + compositionupdate(ev: CompositionEvent): void; + compositionend(): void; + updateCompositionElements(dontRecurse?: boolean): void; + keydown(ev: KeyboardEvent): boolean; +} + export interface ICharMeasure { width: number; height: number; diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts new file mode 100644 index 00000000..753733c2 --- /dev/null +++ b/src/Terminal.test.ts @@ -0,0 +1,907 @@ +import { assert, expect } from 'chai'; +import { Terminal } from './Terminal'; +import { MockViewport, MockCompositionHelper } from './utils/TestUtils'; +import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer'; + +class TestTerminal extends Terminal { + public evaluateKeyEscapeSequence(ev: any): {cancel: boolean, key: string, scrollDisp: number} { return this._evaluateKeyEscapeSequence(ev); } + public keyDown(ev: any): boolean { return this._keyDown(ev); } + public keyPress(ev: any): boolean { return this._keyPress(ev); } +} + +describe('term.js addons', () => { + let term: TestTerminal; + + beforeEach(() => { + term = new TestTerminal(); + term.refresh = () => {}; + term.viewport = new MockViewport(); + (term).compositionHelper = new MockCompositionHelper(); + // Force synchronous writes + term.write = (data) => { + term.writeBuffer.push(data); + (term).innerWrite(); + }; + (term).element = { + classList: { + toggle: () => {}, + remove: () => {} + } + }; + }); + + it('should load addons with Terminal.loadAddon', () => { + Terminal.loadAddon('attach'); + // Test that addon was loaded successfully, adding attach to Terminal's + // prototype. + assert.equal(typeof (Terminal).prototype.attach, 'function'); + }); + + describe('getOption', () => { + it('should retrieve the option correctly', () => { + // In the `options` namespace. + term.options.cursorBlink = true; + assert.equal(term.getOption('cursorBlink'), true); + + // On the Terminal instance + delete term.options.cursorBlink; + term.options.cursorBlink = false; + assert.equal(term.getOption('cursorBlink'), false); + }); + it('should throw when retrieving a non-existant option', () => { + assert.throws(term.getOption.bind(term, 'fake', true)); + }); + }); + + describe('attachCustomKeyEventHandler', () => { + let evKeyDown = { + preventDefault: () => {}, + stopPropagation: () => {}, + type: 'keydown', + keyCode: 77 + }; + let evKeyPress = { + preventDefault: () => {}, + stopPropagation: () => {}, + type: 'keypress', + keyCode: 77 + }; + + beforeEach(() => { + term.handler = () => {}; + term.showCursor = () => {}; + term.clearSelection = () => {}; + }); + + it('should process the keydown/keypress event based on what the handler returns', () => { + assert.equal(term.keyDown(evKeyDown), true); + assert.equal(term.keyPress(evKeyPress), true); + term.attachCustomKeyEventHandler(ev => ev.keyCode === 77); + assert.equal(term.keyDown(evKeyDown), true); + assert.equal(term.keyPress(evKeyPress), true); + term.attachCustomKeyEventHandler(ev => ev.keyCode !== 77); + assert.equal(term.keyDown(evKeyDown), false); + assert.equal(term.keyPress(evKeyPress), false); + }); + + it('should alive after reset(ESC c Full Reset (RIS))', () => { + term.attachCustomKeyEventHandler(ev => ev.keyCode !== 77); + assert.equal(term.keyDown(evKeyDown), false); + assert.equal(term.keyPress(evKeyPress), false); + term.reset(); + assert.equal(term.keyDown(evKeyDown), false); + assert.equal(term.keyPress(evKeyPress), false); + }); + }); + + describe('setOption', () => { + let originalWarn; + let warnCallCount; + beforeEach(() => { + originalWarn = console.warn; + warnCallCount = 0; + console.warn = () => warnCallCount++; + }); + afterEach(() => { + console.warn = originalWarn; + }); + it('should set the option correctly', () => { + term.setOption('cursorBlink', true); + assert.equal(term.options.cursorBlink, true); + term.setOption('cursorBlink', false); + assert.equal(term.options.cursorBlink, false); + }); + it('should throw when setting a non-existant option', () => { + assert.throws(term.setOption.bind(term, 'fake', true)); + }); + it('should warn and do nothing when scrollback is less than number of rows', () => { + term.setOption('scrollback', term.rows - 1); + assert.equal(term.getOption('scrollback'), 1000); + assert.equal(warnCallCount, 1); + }); + }); + + describe('clear', () => { + it('should clear a buffer equal to rows', () => { + let promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y); + term.clear(); + assert.equal(term.buffer.y, 0); + assert.equal(term.buffer.ybase, 0); + assert.equal(term.buffer.ydisp, 0); + assert.equal(term.buffer.lines.length, term.rows); + assert.deepEqual(term.buffer.lines.get(0), promptLine); + for (let i = 1; i < term.rows; i++) { + assert.deepEqual(term.buffer.lines.get(i), term.blankLine()); + } + }); + it('should clear a buffer larger than rows', () => { + // Fill the buffer with dummy rows + for (let i = 0; i < term.rows * 2; i++) { + term.write('test\n'); + } + + let promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y); + term.clear(); + assert.equal(term.buffer.y, 0); + assert.equal(term.buffer.ybase, 0); + assert.equal(term.buffer.ydisp, 0); + assert.equal(term.buffer.lines.length, term.rows); + assert.deepEqual(term.buffer.lines.get(0), promptLine); + for (let i = 1; i < term.rows; i++) { + assert.deepEqual(term.buffer.lines.get(i), term.blankLine()); + } + }); + it('should not break the prompt when cleared twice', () => { + let promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y); + term.clear(); + term.clear(); + assert.equal(term.buffer.y, 0); + assert.equal(term.buffer.ybase, 0); + assert.equal(term.buffer.ydisp, 0); + assert.equal(term.buffer.lines.length, term.rows); + assert.deepEqual(term.buffer.lines.get(0), promptLine); + for (let i = 1; i < term.rows; i++) { + assert.deepEqual(term.buffer.lines.get(i), term.blankLine()); + } + }); + }); + + describe('scroll', () => { + describe('scrollDisp', () => { + let startYDisp; + beforeEach(() => { + for (let i = 0; i < term.rows * 2; i++) { + term.writeln('test'); + } + startYDisp = term.rows + 1; + }); + it('should scroll a single line', () => { + assert.equal(term.buffer.ydisp, startYDisp); + term.scrollDisp(-1); + assert.equal(term.buffer.ydisp, startYDisp - 1); + term.scrollDisp(1); + assert.equal(term.buffer.ydisp, startYDisp); + }); + it('should scroll multiple lines', () => { + assert.equal(term.buffer.ydisp, startYDisp); + term.scrollDisp(-5); + assert.equal(term.buffer.ydisp, startYDisp - 5); + term.scrollDisp(5); + assert.equal(term.buffer.ydisp, startYDisp); + }); + it('should not scroll beyond the bounds of the buffer', () => { + assert.equal(term.buffer.ydisp, startYDisp); + term.scrollDisp(1); + assert.equal(term.buffer.ydisp, startYDisp); + for (let i = 0; i < startYDisp; i++) { + term.scrollDisp(-1); + } + assert.equal(term.buffer.ydisp, 0); + term.scrollDisp(-1); + assert.equal(term.buffer.ydisp, 0); + }); + }); + + describe('scrollPages', () => { + let startYDisp; + beforeEach(() => { + for (let i = 0; i < term.rows * 3; i++) { + term.writeln('test'); + } + startYDisp = (term.rows * 2) + 1; + }); + it('should scroll a single page', () => { + assert.equal(term.buffer.ydisp, startYDisp); + term.scrollPages(-1); + assert.equal(term.buffer.ydisp, startYDisp - (term.rows - 1)); + term.scrollPages(1); + assert.equal(term.buffer.ydisp, startYDisp); + }); + it('should scroll a multiple pages', () => { + assert.equal(term.buffer.ydisp, startYDisp); + term.scrollPages(-2); + assert.equal(term.buffer.ydisp, startYDisp - (term.rows - 1) * 2); + term.scrollPages(2); + assert.equal(term.buffer.ydisp, startYDisp); + }); + }); + + describe('scrollToTop', () => { + beforeEach(() => { + for (let i = 0; i < term.rows * 3; i++) { + term.writeln('test'); + } + }); + it('should scroll to the top', () => { + assert.notEqual(term.buffer.ydisp, 0); + term.scrollToTop(); + assert.equal(term.buffer.ydisp, 0); + }); + }); + + describe('scrollToBottom', () => { + let startYDisp; + beforeEach(() => { + for (let i = 0; i < term.rows * 3; i++) { + term.writeln('test'); + } + startYDisp = (term.rows * 2) + 1; + }); + it('should scroll to the bottom', () => { + term.scrollDisp(-1); + term.scrollToBottom(); + assert.equal(term.buffer.ydisp, startYDisp); + term.scrollPages(-1); + term.scrollToBottom(); + assert.equal(term.buffer.ydisp, startYDisp); + term.scrollToTop(); + term.scrollToBottom(); + assert.equal(term.buffer.ydisp, startYDisp); + }); + }); + + describe('keyDown', () => { + it('should scroll down, when a key is pressed and terminal is scrolled up', () => { + // Override _evaluateKeyEscapeSequence to return cancel code + (term)._evaluateKeyEscapeSequence = () => { + return { key: 'a' }; + }; + let event = { + type: 'keydown', + keyCode: 0, + preventDefault: () => {}, + stopPropagation: () => {} + }; + + term.buffer.ydisp = 0; + term.buffer.ybase = 40; + term.keyDown(event); + + // Ensure that now the terminal is scrolled to bottom + assert.equal(term.buffer.ydisp, term.buffer.ybase); + }); + + it('should not scroll down, when a custom keydown handler prevents the event', () => { + // Add some output to the terminal + for (let i = 0; i < term.rows * 3; i++) { + term.writeln('test'); + } + let startYDisp = (term.rows * 2) + 1; + term.attachCustomKeyEventHandler(() => { + return false; + }); + + assert.equal(term.buffer.ydisp, startYDisp); + term.scrollDisp(-1); + assert.equal(term.buffer.ydisp, startYDisp - 1); + term.keyDown({ keyCode: 0 }); + assert.equal(term.buffer.ydisp, startYDisp - 1); + }); + }); + }); + + describe('evaluateKeyEscapeSequence', () => { + it('should return the correct escape sequence for unmodified keys', () => { + // Backspace + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 8 }).key, '\x7f'); // ^? + // Tab + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 9 }).key, '\t'); + // Return/enter + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 13 }).key, '\r'); // CR + // Escape + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 27 }).key, '\x1b'); + // Page up, page down + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 33 }).key, '\x1b[5~'); // CSI 5 ~ + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 34 }).key, '\x1b[6~'); // CSI 6 ~ + // End, Home + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 35 }).key, '\x1b[F'); // SS3 F + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 36 }).key, '\x1b[H'); // SS3 H + // Left, up, right, down arrows + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 37 }).key, '\x1b[D'); // CSI D + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 38 }).key, '\x1b[A'); // CSI A + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 39 }).key, '\x1b[C'); // CSI C + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 40 }).key, '\x1b[B'); // CSI B + // Insert + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 45 }).key, '\x1b[2~'); // CSI 2 ~ + // Delete + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 46 }).key, '\x1b[3~'); // CSI 3 ~ + // F1-F12 + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 112 }).key, '\x1bOP'); // SS3 P + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 113 }).key, '\x1bOQ'); // SS3 Q + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 114 }).key, '\x1bOR'); // SS3 R + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 115 }).key, '\x1bOS'); // SS3 S + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 116 }).key, '\x1b[15~'); // CSI 1 5 ~ + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 117 }).key, '\x1b[17~'); // CSI 1 7 ~ + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 118 }).key, '\x1b[18~'); // CSI 1 8 ~ + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 119 }).key, '\x1b[19~'); // CSI 1 9 ~ + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 120 }).key, '\x1b[20~'); // CSI 2 0 ~ + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 121 }).key, '\x1b[21~'); // CSI 2 1 ~ + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 122 }).key, '\x1b[23~'); // CSI 2 3 ~ + assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 123 }).key, '\x1b[24~'); // CSI 2 4 ~ + }); + it('should return \\x1b[3;5~ for ctrl+delete', () => { + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 46 }).key, '\x1b[3;5~'); + }); + it('should return \\x1b[3;2~ for shift+delete', () => { + assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 46 }).key, '\x1b[3;2~'); + }); + it('should return \\x1b[3;3~ for alt+delete', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 46 }).key, '\x1b[3;3~'); + }); + it('should return \\x1b[5D for ctrl+left', () => { + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 37 }).key, '\x1b[1;5D'); // CSI 5 D + }); + it('should return \\x1b[5C for ctrl+right', () => { + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 39 }).key, '\x1b[1;5C'); // CSI 5 C + }); + it('should return \\x1b[5A for ctrl+up', () => { + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 38 }).key, '\x1b[1;5A'); // CSI 5 A + }); + it('should return \\x1b[5B for ctrl+down', () => { + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 40 }).key, '\x1b[1;5B'); // CSI 5 B + }); + + describe('On non-macOS platforms', () => { + beforeEach(() => { + term.browser.isMac = false; + }); + // Evalueate alt + arrow key movement, which is a feature of terminal emulators but not VT100 + // http://unix.stackexchange.com/a/108106 + it('should return \\x1b[5D for alt+left', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 37 }).key, '\x1b[1;5D'); // CSI 5 D + }); + it('should return \\x1b[5C for alt+right', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 39 }).key, '\x1b[1;5C'); // CSI 5 C + }); + }); + + describe('On macOS platforms', () => { + beforeEach(() => { + term.browser.isMac = true; + }); + it('should return \\x1bb for alt+left', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 37 }).key, '\x1bb'); // CSI 5 D + }); + it('should return \\x1bf for alt+right', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 39 }).key, '\x1bf'); // CSI 5 C + }); + }); + + it('should return \\x1b[5A for alt+up', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 38 }).key, '\x1b[1;5A'); // CSI 5 A + }); + it('should return \\x1b[5B for alt+down', () => { + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 40 }).key, '\x1b[1;5B'); // CSI 5 B + }); + it('should return the correct escape sequence for modified F1-F12 keys', () => { + assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 112 }).key, '\x1b[1;2P'); + assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 113 }).key, '\x1b[1;2Q'); + assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 114 }).key, '\x1b[1;2R'); + assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 115 }).key, '\x1b[1;2S'); + assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 116 }).key, '\x1b[15;2~'); + assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 117 }).key, '\x1b[17;2~'); + assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 118 }).key, '\x1b[18;2~'); + assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 119 }).key, '\x1b[19;2~'); + assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 120 }).key, '\x1b[20;2~'); + assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 121 }).key, '\x1b[21;2~'); + assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 122 }).key, '\x1b[23;2~'); + assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 123 }).key, '\x1b[24;2~'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 112 }).key, '\x1b[1;3P'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 113 }).key, '\x1b[1;3Q'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 114 }).key, '\x1b[1;3R'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 115 }).key, '\x1b[1;3S'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 116 }).key, '\x1b[15;3~'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 117 }).key, '\x1b[17;3~'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 118 }).key, '\x1b[18;3~'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 119 }).key, '\x1b[19;3~'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 120 }).key, '\x1b[20;3~'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 121 }).key, '\x1b[21;3~'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 122 }).key, '\x1b[23;3~'); + assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 123 }).key, '\x1b[24;3~'); + + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 112 }).key, '\x1b[1;5P'); + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 113 }).key, '\x1b[1;5Q'); + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 114 }).key, '\x1b[1;5R'); + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 115 }).key, '\x1b[1;5S'); + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 116 }).key, '\x1b[15;5~'); + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 117 }).key, '\x1b[17;5~'); + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 118 }).key, '\x1b[18;5~'); + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 119 }).key, '\x1b[19;5~'); + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 120 }).key, '\x1b[20;5~'); + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 121 }).key, '\x1b[21;5~'); + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 122 }).key, '\x1b[23;5~'); + assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 123 }).key, '\x1b[24;5~'); + }); + }); + + describe('Third level shift', () => { + let evKeyDown; + let evKeyPress; + + beforeEach(() => { + term.handler = () => {}; + term.showCursor = () => {}; + term.clearSelection = () => {}; + // term.compositionHelper = { + // isComposing: false, + // keydown: { + // bind: () => { + // return () => { return true; }; + // } + // } + // }; + evKeyDown = { + preventDefault: () => {}, + stopPropagation: () => {}, + type: 'keydown', + altKey: null, + keyCode: null + }; + evKeyPress = { + preventDefault: () => {}, + stopPropagation: () => {}, + type: 'keypress', + altKey: null, + charCode: null, + keyCode: null + }; + }); + + describe('On Mac OS', () => { + beforeEach(() => { + term.browser.isMac = true; + }); + + it('should not interfere with the alt key on keyDown', () => { + evKeyDown.altKey = true; + evKeyDown.keyCode = 81; + assert.equal(term.keyDown(evKeyDown), true); + evKeyDown.altKey = true; + evKeyDown.keyCode = 192; + assert.equal(term.keyDown(evKeyDown), true); + }); + + it('should interefere with the alt + arrow keys', () => { + evKeyDown.altKey = true; + evKeyDown.keyCode = 37; + assert.equal(term.keyDown(evKeyDown), false); + evKeyDown.altKey = true; + evKeyDown.keyCode = 39; + assert.equal(term.keyDown(evKeyDown), false); + }); + + it('should emit key with alt + key on keyPress', (done) => { + let keys = ['@', '@', '\\', '\\', '|', '|']; + + term.on('keypress', (key) => { + if (key) { + let index = keys.indexOf(key); + assert(index !== -1, 'Emitted wrong key: ' + key); + keys.splice(index, 1); + } + if (keys.length === 0) done(); + }); + + evKeyPress.altKey = true; + // @ + evKeyPress.charCode = null; + evKeyPress.keyCode = 64; + term.keyPress(evKeyPress); + // Firefox @ + evKeyPress.charCode = 64; + evKeyPress.keyCode = 0; + term.keyPress(evKeyPress); + // \ + evKeyPress.charCode = null; + evKeyPress.keyCode = 92; + term.keyPress(evKeyPress); + // Firefox \ + evKeyPress.charCode = 92; + evKeyPress.keyCode = 0; + term.keyPress(evKeyPress); + // | + evKeyPress.charCode = null; + evKeyPress.keyCode = 124; + term.keyPress(evKeyPress); + // Firefox | + evKeyPress.charCode = 124; + evKeyPress.keyCode = 0; + term.keyPress(evKeyPress); + }); + }); + + describe('On MS Windows', () => { + beforeEach(() => { + term.browser.isMSWindows = true; + }); + + it('should not interfere with the alt + ctrl key on keyDown', () => { + evKeyPress.altKey = true; + evKeyPress.ctrlKey = true; + evKeyPress.keyCode = 81; + assert.equal(term.keyDown(evKeyPress), true); + evKeyDown.altKey = true; + evKeyDown.ctrlKey = true; + evKeyDown.keyCode = 81; + assert.equal(term.keyDown(evKeyDown), true); + }); + + it('should interefere with the alt + ctrl + arrow keys', () => { + evKeyDown.altKey = true; + evKeyDown.ctrlKey = true; + + evKeyDown.keyCode = 37; + assert.equal(term.keyDown(evKeyDown), false); + evKeyDown.keyCode = 39; + assert.equal(term.keyDown(evKeyDown), false); + }); + + it('should emit key with alt + ctrl + key on keyPress', (done) => { + let keys = ['@', '@', '\\', '\\', '|', '|']; + + term.on('keypress', (key) => { + if (key) { + let index = keys.indexOf(key); + assert(index !== -1, 'Emitted wrong key: ' + key); + keys.splice(index, 1); + } + if (keys.length === 0) done(); + }); + + evKeyPress.altKey = true; + evKeyPress.ctrlKey = true; + + // @ + evKeyPress.charCode = null; + evKeyPress.keyCode = 64; + term.keyPress(evKeyPress); + // Firefox @ + evKeyPress.charCode = 64; + evKeyPress.keyCode = 0; + term.keyPress(evKeyPress); + // \ + evKeyPress.charCode = null; + evKeyPress.keyCode = 92; + term.keyPress(evKeyPress); + // Firefox \ + evKeyPress.charCode = 92; + evKeyPress.keyCode = 0; + term.keyPress(evKeyPress); + // | + evKeyPress.charCode = null; + evKeyPress.keyCode = 124; + term.keyPress(evKeyPress); + // Firefox | + evKeyPress.charCode = 124; + evKeyPress.keyCode = 0; + term.keyPress(evKeyPress); + }); + }); + }); + + describe('unicode - surrogates', () => { + it('2 characters per cell', function (): void { + this.timeout(10000); // This is needed because istanbul patches code and slows it down + let high = String.fromCharCode(0xD800); + for (let i = 0xDC00; i <= 0xDCFF; ++i) { + term.write(high + String.fromCharCode(i)); + let tchar = term.buffer.lines.get(0)[0]; + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); + expect(term.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX]).eql(' '); + term.reset(); + } + }); + it('2 characters at last cell', () => { + let high = String.fromCharCode(0xD800); + for (let i = 0xDC00; i <= 0xDCFF; ++i) { + term.buffer.x = term.cols - 1; + term.write(high + String.fromCharCode(i)); + expect(term.buffer.lines.get(0)[term.buffer.x - 1][CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); + expect(term.buffer.lines.get(0)[term.buffer.x - 1][CHAR_DATA_CHAR_INDEX].length).eql(2); + expect(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX]).eql(' '); + term.reset(); + } + }); + it('2 characters per cell over line end with autowrap', () => { + let high = String.fromCharCode(0xD800); + for (let i = 0xDC00; i <= 0xDCFF; ++i) { + term.buffer.x = term.cols - 1; + term.wraparoundMode = true; + term.write('a' + high + String.fromCharCode(i)); + expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX]).eql('a'); + expect(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); + expect(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX].length).eql(2); + expect(term.buffer.lines.get(1)[1][CHAR_DATA_CHAR_INDEX]).eql(' '); + term.reset(); + } + }); + it('2 characters per cell over line end without autowrap', () => { + let high = String.fromCharCode(0xD800); + for (let i = 0xDC00; i <= 0xDCFF; ++i) { + term.buffer.x = term.cols - 1; + term.wraparoundMode = false; + term.write('a' + high + String.fromCharCode(i)); + // auto wraparound mode should cut off the rest of the line + expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX]).eql('a'); + expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX].length).eql(1); + expect(term.buffer.lines.get(1)[1][CHAR_DATA_CHAR_INDEX]).eql(' '); + term.reset(); + } + }); + it('splitted surrogates', () => { + let high = String.fromCharCode(0xD800); + for (let i = 0xDC00; i <= 0xDCFF; ++i) { + term.write(high); + term.write(String.fromCharCode(i)); + let tchar = term.buffer.lines.get(0)[0]; + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); + expect(term.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX]).eql(' '); + term.reset(); + } + }); + }); + + describe('unicode - combining characters', () => { + it('café', () => { + term.write('cafe\u0301'); + expect(term.buffer.lines.get(0)[3][CHAR_DATA_CHAR_INDEX]).eql('e\u0301'); + expect(term.buffer.lines.get(0)[3][CHAR_DATA_CHAR_INDEX].length).eql(2); + expect(term.buffer.lines.get(0)[3][CHAR_DATA_WIDTH_INDEX]).eql(1); + }); + it('café - end of line', () => { + term.buffer.x = term.cols - 1 - 3; + term.write('cafe\u0301'); + expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX]).eql('e\u0301'); + expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX].length).eql(2); + expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_WIDTH_INDEX]).eql(1); + expect(term.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX]).eql(' '); + expect(term.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX].length).eql(1); + expect(term.buffer.lines.get(0)[1][CHAR_DATA_WIDTH_INDEX]).eql(1); + }); + it('multiple combined é', () => { + term.wraparoundMode = true; + term.write(Array(100).join('e\u0301')); + for (let i = 0; i < term.cols; ++i) { + let tchar = term.buffer.lines.get(0)[i]; + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('e\u0301'); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); + } + let tchar = term.buffer.lines.get(1)[0]; + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('e\u0301'); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); + }); + it('multiple surrogate with combined', () => { + term.wraparoundMode = true; + term.write(Array(100).join('\uD800\uDC00\u0301')); + for (let i = 0; i < term.cols; ++i) { + let tchar = term.buffer.lines.get(0)[i]; + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\uD800\uDC00\u0301'); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); + } + let tchar = term.buffer.lines.get(1)[0]; + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\uD800\uDC00\u0301'); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); + }); + }); + + describe('unicode - fullwidth characters', () => { + it('cursor movement even', () => { + expect(term.buffer.x).eql(0); + term.write('¥'); + expect(term.buffer.x).eql(2); + }); + it('cursor movement odd', () => { + term.buffer.x = 1; + expect(term.buffer.x).eql(1); + term.write('¥'); + expect(term.buffer.x).eql(3); + }); + it('line of ¥ even', () => { + term.wraparoundMode = true; + term.write(Array(50).join('¥')); + for (let i = 0; i < term.cols; ++i) { + let tchar = term.buffer.lines.get(0)[i]; + if (i % 2) { + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0); + } else { + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥'); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + } + } + let tchar = term.buffer.lines.get(1)[0]; + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥'); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + }); + it('line of ¥ odd', () => { + term.wraparoundMode = true; + term.buffer.x = 1; + term.write(Array(50).join('¥')); + for (let i = 1; i < term.cols - 1; ++i) { + let tchar = term.buffer.lines.get(0)[i]; + if (!(i % 2)) { + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0); + } else { + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥'); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + } + } + let tchar = term.buffer.lines.get(0)[term.cols - 1]; + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' '); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); + tchar = term.buffer.lines.get(1)[0]; + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥'); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + }); + it('line of ¥ with combining odd', () => { + term.wraparoundMode = true; + term.buffer.x = 1; + term.write(Array(50).join('¥\u0301')); + for (let i = 1; i < term.cols - 1; ++i) { + let tchar = term.buffer.lines.get(0)[i]; + if (!(i % 2)) { + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0); + } else { + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301'); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + } + } + let tchar = term.buffer.lines.get(0)[term.cols - 1]; + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' '); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); + tchar = term.buffer.lines.get(1)[0]; + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301'); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + }); + it('line of ¥ with combining even', () => { + term.wraparoundMode = true; + term.write(Array(50).join('¥\u0301')); + for (let i = 0; i < term.cols; ++i) { + let tchar = term.buffer.lines.get(0)[i]; + if (i % 2) { + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0); + } else { + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301'); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + } + } + let tchar = term.buffer.lines.get(1)[0]; + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301'); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + }); + it('line of surrogate fullwidth with combining odd', () => { + term.wraparoundMode = true; + term.buffer.x = 1; + term.write(Array(50).join('\ud843\ude6d\u0301')); + for (let i = 1; i < term.cols - 1; ++i) { + let tchar = term.buffer.lines.get(0)[i]; + if (!(i % 2)) { + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0); + } else { + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301'); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + } + } + let tchar = term.buffer.lines.get(0)[term.cols - 1]; + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' '); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); + tchar = term.buffer.lines.get(1)[0]; + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301'); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + }); + it('line of surrogate fullwidth with combining even', () => { + term.wraparoundMode = true; + term.write(Array(50).join('\ud843\ude6d\u0301')); + for (let i = 0; i < term.cols; ++i) { + let tchar = term.buffer.lines.get(0)[i]; + if (i % 2) { + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0); + } else { + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301'); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + } + } + let tchar = term.buffer.lines.get(1)[0]; + expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301'); + expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); + expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); + }); + }); + + describe('insert mode', () => { + it('halfwidth - all', () => { + term.write(Array(9).join('0123456789').slice(-80)); + term.buffer.x = 10; + term.buffer.y = 0; + term.insertMode = true; + term.write('abcde'); + expect(term.buffer.lines.get(0).length).eql(term.cols); + expect(term.buffer.lines.get(0)[10][CHAR_DATA_CHAR_INDEX]).eql('a'); + expect(term.buffer.lines.get(0)[14][CHAR_DATA_CHAR_INDEX]).eql('e'); + expect(term.buffer.lines.get(0)[15][CHAR_DATA_CHAR_INDEX]).eql('0'); + expect(term.buffer.lines.get(0)[79][CHAR_DATA_CHAR_INDEX]).eql('4'); + }); + it('fullwidth - insert', () => { + term.write(Array(9).join('0123456789').slice(-80)); + term.buffer.x = 10; + term.buffer.y = 0; + term.insertMode = true; + term.write('¥¥¥'); + expect(term.buffer.lines.get(0).length).eql(term.cols); + expect(term.buffer.lines.get(0)[10][CHAR_DATA_CHAR_INDEX]).eql('¥'); + expect(term.buffer.lines.get(0)[11][CHAR_DATA_CHAR_INDEX]).eql(''); + expect(term.buffer.lines.get(0)[14][CHAR_DATA_CHAR_INDEX]).eql('¥'); + expect(term.buffer.lines.get(0)[15][CHAR_DATA_CHAR_INDEX]).eql(''); + expect(term.buffer.lines.get(0)[79][CHAR_DATA_CHAR_INDEX]).eql('3'); + }); + it('fullwidth - right border', () => { + term.write(Array(41).join('¥')); + term.buffer.x = 10; + term.buffer.y = 0; + term.insertMode = true; + term.write('a'); + expect(term.buffer.lines.get(0).length).eql(term.cols); + expect(term.buffer.lines.get(0)[10][CHAR_DATA_CHAR_INDEX]).eql('a'); + expect(term.buffer.lines.get(0)[11][CHAR_DATA_CHAR_INDEX]).eql('¥'); + expect(term.buffer.lines.get(0)[79][CHAR_DATA_CHAR_INDEX]).eql(' '); // fullwidth char got replaced + term.write('b'); + expect(term.buffer.lines.get(0).length).eql(term.cols); + expect(term.buffer.lines.get(0)[11][CHAR_DATA_CHAR_INDEX]).eql('b'); + expect(term.buffer.lines.get(0)[12][CHAR_DATA_CHAR_INDEX]).eql('¥'); + expect(term.buffer.lines.get(0)[79][CHAR_DATA_CHAR_INDEX]).eql(''); // empty cell after fullwidth + }); + }); +}); diff --git a/src/Terminal.ts b/src/Terminal.ts index 31eaef8a..a7b4b336 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -29,7 +29,7 @@ import * as Mouse from './utils/Mouse'; import { CHARSETS } from './Charsets'; import { getRawByteCoords } from './utils/Mouse'; import { CustomKeyEventHandler, Charset, LinkMatcherHandler, LinkMatcherValidationCallback, CharData, LineData, Option, StringOption, BooleanOption, StringArrayOption, NumberOption, GeometryOption, HandlerOption } from './Types'; -import { ITerminal, IBrowser, ITerminalOptions, IInputHandlingTerminal, ILinkMatcherOptions } from './Interfaces'; +import { ITerminal, IBrowser, ITerminalOptions, IInputHandlingTerminal, ILinkMatcherOptions, IViewport, ICompositionHelper } from './Interfaces'; // Declare for RequireJS in loadAddon declare var define: any; @@ -259,8 +259,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT private linkifier: Linkifier; public buffers: BufferSet; public buffer: Buffer; - public viewport: Viewport; - private compositionHelper: CompositionHelper; + public viewport: IViewport; + private compositionHelper: ICompositionHelper; public charMeasure: CharMeasure; public cols: number; @@ -601,14 +601,14 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT if (document.activeElement !== this) { return; } - self.keyDown(ev); + self._keyDown(ev); }, true); on(this.element, 'keypress', function (ev: KeyboardEvent): void { if (document.activeElement !== this) { return; } - self.keyPress(ev); + self._keyPress(ev); }, true); on(this.element, 'keyup', (ev: KeyboardEvent) => { @@ -618,19 +618,19 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT }, true); on(this.textarea, 'keydown', (ev: KeyboardEvent) => { - this.keyDown(ev); + this._keyDown(ev); }, true); on(this.textarea, 'keypress', (ev: KeyboardEvent) => { - this.keyPress(ev); + this._keyPress(ev); // Truncate the textarea's value, since it is not needed this.textarea.value = ''; }, true); - on(this.textarea, 'compositionstart', this.compositionHelper.compositionstart.bind(this.compositionHelper)); - on(this.textarea, 'compositionupdate', this.compositionHelper.compositionupdate.bind(this.compositionHelper)); - on(this.textarea, 'compositionend', this.compositionHelper.compositionend.bind(this.compositionHelper)); - this.on('refresh', this.compositionHelper.updateCompositionElements.bind(this.compositionHelper)); + on(this.textarea, 'compositionstart', () => this.compositionHelper.compositionstart()); + on(this.textarea, 'compositionupdate', (e: CompositionEvent) => this.compositionHelper.compositionupdate(e)); + on(this.textarea, 'compositionend', () => this.compositionHelper.compositionend()); + this.on('refresh', () => this.compositionHelper.updateCompositionElements()); this.on('refresh', (data) => this.queueLinkification(data.start, data.end)); } @@ -1474,21 +1474,21 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent * @param {KeyboardEvent} ev The keydown event to be handled. */ - private keyDown(ev: KeyboardEvent): boolean { + protected _keyDown(ev: KeyboardEvent): boolean { if (this.customKeyEventHandler && this.customKeyEventHandler(ev) === false) { return false; } this.restartCursorBlinking(); - if (!this.compositionHelper.keydown.bind(this.compositionHelper)(ev)) { + if (!this.compositionHelper.keydown(ev)) { if (this.buffer.ybase !== this.buffer.ydisp) { this.scrollToBottom(); } return false; } - const result = this.evaluateKeyEscapeSequence(ev); + const result = this._evaluateKeyEscapeSequence(ev); if (result.key === C0.DC3) { // XOFF this.writeStopped = true; @@ -1501,7 +1501,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT return this.cancel(ev, true); } - if (isThirdLevelShift(ev)) { + if (isThirdLevelShift(this.browser, ev)) { return true; } @@ -1529,7 +1529,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * Reference: http://invisible-island.net/xterm/ctlseqs/ctlseqs.html * @param ev The keyboard event to be translated to key escape sequence. */ - private evaluateKeyEscapeSequence(ev: KeyboardEvent): {cancel: boolean, key: string, scrollDisp: number} { + protected _evaluateKeyEscapeSequence(ev: KeyboardEvent): {cancel: boolean, key: string, scrollDisp: number} { const result: {cancel: boolean, key: string, scrollDisp: number} = { // Whether to cancel event propogation (NOTE: this may not be needed since the event is // canceled at the end of keyDown @@ -1836,7 +1836,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent * @param {KeyboardEvent} ev The keypress event to be handled. */ - private keyPress(ev: KeyboardEvent): boolean { + protected _keyPress(ev: KeyboardEvent): boolean { let key; if (this.customKeyEventHandler && this.customKeyEventHandler(ev) === false) { @@ -1856,7 +1856,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT } if (!key || ( - (ev.altKey || ev.ctrlKey || ev.metaKey) && !isThirdLevelShift(ev) + (ev.altKey || ev.ctrlKey || ev.metaKey) && !isThirdLevelShift(this.browser, ev) )) { return false; } @@ -2309,10 +2309,10 @@ function off(el: any, type: string, handler: (event: Event) => any, capture: boo el.removeEventListener(type, handler, capture); } -function isThirdLevelShift(ev: KeyboardEvent): boolean { +function isThirdLevelShift(browser: IBrowser, ev: KeyboardEvent): boolean { const thirdLevelKey = - (Browser.isMac && ev.altKey && !ev.ctrlKey && !ev.metaKey) || - (Browser.isMSWindows && ev.altKey && ev.ctrlKey && !ev.metaKey); + (browser.isMac && ev.altKey && !ev.ctrlKey && !ev.metaKey) || + (browser.isMSWindows && ev.altKey && ev.ctrlKey && !ev.metaKey); if (ev.type === 'keypress') { return thirdLevelKey; diff --git a/src/Viewport.test.ts b/src/Viewport.test.ts index 8c7a914b..428ef098 100644 --- a/src/Viewport.test.ts +++ b/src/Viewport.test.ts @@ -4,7 +4,7 @@ import { assert } from 'chai'; import { Viewport } from './Viewport'; -import {BufferSet} from './BufferSet'; +import { BufferSet } from './BufferSet'; describe('Viewport', () => { let terminal; diff --git a/src/test/addons/test.js b/src/test/addons/test.js deleted file mode 100644 index 42ede9db..00000000 --- a/src/test/addons/test.js +++ /dev/null @@ -1,10 +0,0 @@ -var assert = require('chai').assert; -var Terminal = require('../../xterm'); - -describe('xterm.js addons', function() { - it('should load addons with Terminal.loadAddon', function () { - Terminal.loadAddon('attach'); - // Test that addon was loaded successfully - assert.equal(typeof Terminal.prototype.attach, 'function'); - }); -}); diff --git a/src/test/test.js b/src/test/test.js deleted file mode 100644 index 2c44f1e7..00000000 --- a/src/test/test.js +++ /dev/null @@ -1,889 +0,0 @@ -var assert = require('chai').assert; -var expect = require('chai').expect; -var Terminal = require('../xterm'); -var CHAR_DATA_CHAR_INDEX = require('../Buffer').CHAR_DATA_CHAR_INDEX; -var CHAR_DATA_WIDTH_INDEX = require('../Buffer').CHAR_DATA_WIDTH_INDEX; - -describe('xterm.js', function() { - var xterm; - - beforeEach(function () { - xterm = new Terminal(); - xterm.refresh = function(){}; - xterm.viewport = { - syncScrollArea: function(){} - }; - xterm.compositionHelper = { - keydown: function(){ return true; } - }; - // Force synchronous writes - xterm.write = function(data) { - xterm.writeBuffer.push(data); - xterm.innerWrite(); - }; - xterm.element = { - classList: { - toggle: function(){}, - remove: function(){} - } - }; - }); - - describe('getOption', function() { - it('should retrieve the option correctly', function() { - // In the `options` namespace. - xterm.options.cursorBlink = true; - assert.equal(xterm.getOption('cursorBlink'), true); - - // On the Terminal instance - delete xterm.options.cursorBlink; - xterm.cursorBlink = false; - assert.equal(xterm.getOption('cursorBlink'), false); - }); - it('should throw when retrieving a non-existant option', function() { - assert.throws(xterm.getOption.bind(xterm, 'fake', true)); - }); - }); - - describe('setOption', function() { - let originalWarn; - let warnCallCount; - beforeEach(() => { - originalWarn = console.warn; - warnCallCount = 0; - console.warn = () => warnCallCount++; - }); - afterEach(() => { - console.warn = originalWarn; - }); - it('should set the option correctly', function() { - xterm.setOption('cursorBlink', true); - assert.equal(xterm.cursorBlink, true); - assert.equal(xterm.options.cursorBlink, true); - xterm.setOption('cursorBlink', false); - assert.equal(xterm.cursorBlink, false); - assert.equal(xterm.options.cursorBlink, false); - }); - it('should throw when setting a non-existant option', function() { - assert.throws(xterm.setOption.bind(xterm, 'fake', true)); - }); - it('should warn and do nothing when scrollback is less than number of rows', function() { - xterm.setOption('scrollback', xterm.rows - 1); - assert.equal(xterm.getOption('scrollback'), 1000); - assert.equal(warnCallCount, 1); - }); - }); - - describe('clear', function() { - it('should clear a buffer equal to rows', function() { - var promptLine = xterm.buffer.lines.get(xterm.buffer.ybase + xterm.buffer.y); - xterm.clear(); - assert.equal(xterm.buffer.y, 0); - assert.equal(xterm.buffer.ybase, 0); - assert.equal(xterm.buffer.ydisp, 0); - assert.equal(xterm.buffer.lines.length, xterm.rows); - assert.deepEqual(xterm.buffer.lines.get(0), promptLine); - for (var i = 1; i < xterm.rows; i++) { - assert.deepEqual(xterm.buffer.lines.get(i), xterm.blankLine()); - } - }); - it('should clear a buffer larger than rows', function() { - // Fill the buffer with dummy rows - for (var i = 0; i < xterm.rows * 2; i++) { - xterm.write('test\n'); - } - - var promptLine = xterm.buffer.lines.get(xterm.buffer.ybase + xterm.buffer.y); - xterm.clear(); - assert.equal(xterm.buffer.y, 0); - assert.equal(xterm.buffer.ybase, 0); - assert.equal(xterm.buffer.ydisp, 0); - assert.equal(xterm.buffer.lines.length, xterm.rows); - assert.deepEqual(xterm.buffer.lines.get(0), promptLine); - for (var i = 1; i < xterm.rows; i++) { - assert.deepEqual(xterm.buffer.lines.get(i), xterm.blankLine()); - } - }); - it('should not break the prompt when cleared twice', function() { - var promptLine = xterm.buffer.lines.get(xterm.buffer.ybase + xterm.buffer.y); - xterm.clear(); - xterm.clear(); - assert.equal(xterm.buffer.y, 0); - assert.equal(xterm.buffer.ybase, 0); - assert.equal(xterm.buffer.ydisp, 0); - assert.equal(xterm.buffer.lines.length, xterm.rows); - assert.deepEqual(xterm.buffer.lines.get(0), promptLine); - for (var i = 1; i < xterm.rows; i++) { - assert.deepEqual(xterm.buffer.lines.get(i), xterm.blankLine()); - } - }); - }); - - describe('scroll', function() { - describe('scrollDisp', function() { - var startYDisp; - beforeEach(function() { - for (var i = 0; i < xterm.rows * 2; i++) { - xterm.writeln('test'); - } - startYDisp = xterm.rows + 1; - }); - it('should scroll a single line', function() { - assert.equal(xterm.buffer.ydisp, startYDisp); - xterm.scrollDisp(-1); - assert.equal(xterm.buffer.ydisp, startYDisp - 1); - xterm.scrollDisp(1); - assert.equal(xterm.buffer.ydisp, startYDisp); - }); - it('should scroll multiple lines', function() { - assert.equal(xterm.buffer.ydisp, startYDisp); - xterm.scrollDisp(-5); - assert.equal(xterm.buffer.ydisp, startYDisp - 5); - xterm.scrollDisp(5); - assert.equal(xterm.buffer.ydisp, startYDisp); - }); - it('should not scroll beyond the bounds of the buffer', function() { - assert.equal(xterm.buffer.ydisp, startYDisp); - xterm.scrollDisp(1); - assert.equal(xterm.buffer.ydisp, startYDisp); - for (var i = 0; i < startYDisp; i++) { - xterm.scrollDisp(-1); - } - assert.equal(xterm.buffer.ydisp, 0); - xterm.scrollDisp(-1); - assert.equal(xterm.buffer.ydisp, 0); - }); - }); - - describe('scrollPages', function() { - var startYDisp; - beforeEach(function() { - for (var i = 0; i < xterm.rows * 3; i++) { - xterm.writeln('test'); - } - startYDisp = (xterm.rows * 2) + 1; - }); - it('should scroll a single page', function() { - assert.equal(xterm.buffer.ydisp, startYDisp); - xterm.scrollPages(-1); - assert.equal(xterm.buffer.ydisp, startYDisp - (xterm.rows - 1)); - xterm.scrollPages(1); - assert.equal(xterm.buffer.ydisp, startYDisp); - }); - it('should scroll a multiple pages', function() { - assert.equal(xterm.buffer.ydisp, startYDisp); - xterm.scrollPages(-2); - assert.equal(xterm.buffer.ydisp, startYDisp - (xterm.rows - 1) * 2); - xterm.scrollPages(2); - assert.equal(xterm.buffer.ydisp, startYDisp); - }); - }); - - describe('scrollToTop', function() { - beforeEach(function() { - for (var i = 0; i < xterm.rows * 3; i++) { - xterm.writeln('test'); - } - }); - it('should scroll to the top', function() { - assert.notEqual(xterm.buffer.ydisp, 0); - xterm.scrollToTop(); - assert.equal(xterm.buffer.ydisp, 0); - }); - }); - - describe('scrollToBottom', function() { - var startYDisp; - beforeEach(function() { - for (var i = 0; i < xterm.rows * 3; i++) { - xterm.writeln('test'); - } - startYDisp = (xterm.rows * 2) + 1; - }); - it('should scroll to the bottom', function() { - xterm.scrollDisp(-1); - xterm.scrollToBottom(); - assert.equal(xterm.buffer.ydisp, startYDisp); - xterm.scrollPages(-1); - xterm.scrollToBottom(); - assert.equal(xterm.buffer.ydisp, startYDisp); - xterm.scrollToTop(); - xterm.scrollToBottom(); - assert.equal(xterm.buffer.ydisp, startYDisp); - }); - }); - - describe('keyDown', function () { - it('should scroll down, when a key is pressed and terminal is scrolled up', function () { - // Override evaluateKeyEscapeSequence to return cancel code - xterm.evaluateKeyEscapeSequence = function() { - return { key: 'a' }; - }; - var event = { - type: 'keydown', - keyCode: 0, - preventDefault: function(){}, - stopPropagation: function(){} - }; - - xterm.buffer.ydisp = 0; - xterm.buffer.ybase = 40; - assert.notEqual(xterm.buffer.ydisp, xterm.buffer.ybase); - xterm.keyDown(event); - - // Ensure that now the terminal is scrolled to bottom - assert.equal(xterm.buffer.ydisp, xterm.buffer.ybase); - }); - - it('should not scroll down, when a custom keydown handler prevents the event', function () { - // Add some output to the terminal - for (var i = 0; i < xterm.rows * 3; i++) { - xterm.writeln('test'); - } - var startYDisp = (xterm.rows * 2) + 1; - xterm.attachCustomKeyEventHandler(function () { - return false; - }); - - assert.equal(xterm.buffer.ydisp, startYDisp); - xterm.scrollDisp(-1); - assert.equal(xterm.buffer.ydisp, startYDisp - 1); - xterm.keyDown({ keyCode: 0 }); - assert.equal(xterm.buffer.ydisp, startYDisp - 1); - }); - }); - }); - - describe('evaluateKeyEscapeSequence', function() { - it('should return the correct escape sequence for unmodified keys', function() { - // Backspace - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 8 }).key, '\x7f'); // ^? - // Tab - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 9 }).key, '\t'); - // Return/enter - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 13 }).key, '\r'); // CR - // Escape - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 27 }).key, '\x1b'); - // Page up, page down - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 33 }).key, '\x1b[5~'); // CSI 5 ~ - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 34 }).key, '\x1b[6~'); // CSI 6 ~ - // End, Home - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 35 }).key, '\x1b[F'); // SS3 F - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 36 }).key, '\x1b[H'); // SS3 H - // Left, up, right, down arrows - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 37 }).key, '\x1b[D'); // CSI D - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 38 }).key, '\x1b[A'); // CSI A - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 39 }).key, '\x1b[C'); // CSI C - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 40 }).key, '\x1b[B'); // CSI B - // Insert - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 45 }).key, '\x1b[2~'); // CSI 2 ~ - // Delete - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 46 }).key, '\x1b[3~'); // CSI 3 ~ - // F1-F12 - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 112 }).key, '\x1bOP'); // SS3 P - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 113 }).key, '\x1bOQ'); // SS3 Q - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 114 }).key, '\x1bOR'); // SS3 R - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 115 }).key, '\x1bOS'); // SS3 S - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 116 }).key, '\x1b[15~'); // CSI 1 5 ~ - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 117 }).key, '\x1b[17~'); // CSI 1 7 ~ - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 118 }).key, '\x1b[18~'); // CSI 1 8 ~ - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 119 }).key, '\x1b[19~'); // CSI 1 9 ~ - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 120 }).key, '\x1b[20~'); // CSI 2 0 ~ - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 121 }).key, '\x1b[21~'); // CSI 2 1 ~ - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 122 }).key, '\x1b[23~'); // CSI 2 3 ~ - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 123 }).key, '\x1b[24~'); // CSI 2 4 ~ - }); - it('should return \\x1b[3;5~ for ctrl+delete', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 46 }).key, '\x1b[3;5~'); - }); - it('should return \\x1b[3;2~ for shift+delete', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 46 }).key, '\x1b[3;2~'); - }); - it('should return \\x1b[3;3~ for alt+delete', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 46 }).key, '\x1b[3;3~'); - }); - it('should return \\x1b[5D for ctrl+left', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 37 }).key, '\x1b[1;5D'); // CSI 5 D - }); - it('should return \\x1b[5C for ctrl+right', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 39 }).key, '\x1b[1;5C'); // CSI 5 C - }); - it('should return \\x1b[5A for ctrl+up', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 38 }).key, '\x1b[1;5A'); // CSI 5 A - }); - it('should return \\x1b[5B for ctrl+down', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 40 }).key, '\x1b[1;5B'); // CSI 5 B - }); - - describe('On non-macOS platforms', function() { - beforeEach(function() { - xterm.browser.isMac = false; - }); - // Evalueate alt + arrow key movement, which is a feature of terminal emulators but not VT100 - // http://unix.stackexchange.com/a/108106 - it('should return \\x1b[5D for alt+left', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 37 }).key, '\x1b[1;5D'); // CSI 5 D - }); - it('should return \\x1b[5C for alt+right', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 39 }).key, '\x1b[1;5C'); // CSI 5 C - }); - }); - - describe('On macOS platforms', function() { - beforeEach(function() { - xterm.browser.isMac = true; - }); - it('should return \\x1bb for alt+left', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 37 }).key, '\x1bb'); // CSI 5 D - }); - it('should return \\x1bf for alt+right', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 39 }).key, '\x1bf'); // CSI 5 C - }); - }); - - it('should return \\x1b[5A for alt+up', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 38 }).key, '\x1b[1;5A'); // CSI 5 A - }); - it('should return \\x1b[5B for alt+down', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 40 }).key, '\x1b[1;5B'); // CSI 5 B - }); - it('should return the correct escape sequence for modified F1-F12 keys', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 112 }).key, '\x1b[1;2P'); - assert.equal(xterm.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 113 }).key, '\x1b[1;2Q'); - assert.equal(xterm.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 114 }).key, '\x1b[1;2R'); - assert.equal(xterm.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 115 }).key, '\x1b[1;2S'); - assert.equal(xterm.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 116 }).key, '\x1b[15;2~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 117 }).key, '\x1b[17;2~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 118 }).key, '\x1b[18;2~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 119 }).key, '\x1b[19;2~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 120 }).key, '\x1b[20;2~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 121 }).key, '\x1b[21;2~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 122 }).key, '\x1b[23;2~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 123 }).key, '\x1b[24;2~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 112 }).key, '\x1b[1;3P'); - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 113 }).key, '\x1b[1;3Q'); - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 114 }).key, '\x1b[1;3R'); - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 115 }).key, '\x1b[1;3S'); - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 116 }).key, '\x1b[15;3~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 117 }).key, '\x1b[17;3~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 118 }).key, '\x1b[18;3~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 119 }).key, '\x1b[19;3~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 120 }).key, '\x1b[20;3~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 121 }).key, '\x1b[21;3~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 122 }).key, '\x1b[23;3~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ altKey: true, keyCode: 123 }).key, '\x1b[24;3~'); - - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 112 }).key, '\x1b[1;5P'); - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 113 }).key, '\x1b[1;5Q'); - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 114 }).key, '\x1b[1;5R'); - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 115 }).key, '\x1b[1;5S'); - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 116 }).key, '\x1b[15;5~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 117 }).key, '\x1b[17;5~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 118 }).key, '\x1b[18;5~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 119 }).key, '\x1b[19;5~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 120 }).key, '\x1b[20;5~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 121 }).key, '\x1b[21;5~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 122 }).key, '\x1b[23;5~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 123 }).key, '\x1b[24;5~'); - }); - }); - - describe('attachCustomKeyEventHandler', function () { - var evKeyDown = { - preventDefault: function() {}, - stopPropagation: function() {}, - type: 'keydown' - } - var evKeyPress = { - preventDefault: function() {}, - stopPropagation: function() {}, - type: 'keypress' - } - - beforeEach(function() { - xterm.handler = function() {}; - xterm.showCursor = function() {}; - xterm.clearSelection = function() {}; - xterm.compositionHelper = { - keydown: { - bind: function() { - return function () { return true; } - } - }, - keypress: { - bind: function() { - return function () { return true; } - } - } - } - }); - - it('should process the keydown/keypress event based on what the handler returns', function () { - assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), true); - assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), true); - xterm.attachCustomKeyEventHandler(function (ev) { - return ev.keyCode === 77; - }); - assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), true); - assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), true); - xterm.attachCustomKeyEventHandler(function (ev) { - return ev.keyCode !== 77; - }); - assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), false); - assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), false); - }); - - it('should alive after reset(ESC c Full Reset (RIS))', function () { - xterm.attachCustomKeyEventHandler(function (ev) { - return ev.keyCode !== 77; - }); - assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), false); - assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), false); - xterm.reset(); - assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), false); - assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), false); - }); - }); - - describe('Third level shift', function() { - var evKeyDown = { - preventDefault: function() {}, - stopPropagation: function() {}, - type: 'keydown' - }, - evKeyPress = { - preventDefault: function() {}, - stopPropagation: function() {}, - type: 'keypress' - }; - - beforeEach(function() { - xterm.handler = function() {}; - xterm.showCursor = function() {}; - xterm.clearSelection = function() {}; - xterm.compositionHelper = { - isComposing: false, - keydown: { - bind: function() { - return function() { return true; }; - } - } - }; - }); - - describe('On Mac OS', function() { - beforeEach(function() { - xterm.browser.isMac = true; - }); - - it('should not interfere with the alt key on keyDown', function() { - assert.equal( - xterm.keyDown(Object.assign({}, evKeyDown, { altKey: true, keyCode: 81 })), - true - ); - assert.equal( - xterm.keyDown(Object.assign({}, evKeyDown, { altKey: true, keyCode: 192 })), - true - ); - }); - - it('should interefere with the alt + arrow keys', function() { - assert.equal( - xterm.keyDown(Object.assign({}, evKeyDown, { altKey: true, keyCode: 37 })), - false - ); - assert.equal( - xterm.keyDown(Object.assign({}, evKeyDown, { altKey: true, keyCode: 39 })), - false - ); - }); - - it('should emit key with alt + key on keyPress', function(done) { - var keys = ['@', '@', '\\', '\\', '|', '|']; - - xterm.on('keypress', function(key) { - if (key) { - var index = keys.indexOf(key); - assert(index !== -1, "Emitted wrong key: " + key); - keys.splice(index, 1); - } - if (keys.length === 0) done(); - }); - - xterm.keyPress(Object.assign({}, evKeyPress, { altKey: true, keyCode: 64 })); // @ - // Firefox - xterm.keyPress(Object.assign({}, evKeyPress, { altKey: true, charCode: 64, keyCode: 0 })); - xterm.keyPress(Object.assign({}, evKeyPress, { altKey: true, keyCode: 92 })); // \ - xterm.keyPress(Object.assign({}, evKeyPress, { altKey: true, charCode: 92, keyCode: 0 })); - xterm.keyPress(Object.assign({}, evKeyPress, { altKey: true, keyCode: 124 })); // | - xterm.keyPress(Object.assign({}, evKeyPress, { altKey: true, charCode: 124, keyCode: 0 })); - }); - }); - - describe('On MS Windows', function() { - beforeEach(function() { - xterm.browser.isMSWindows = true; - }); - - it('should not interfere with the alt + ctrl key on keyDown', function() { - assert.equal( - xterm.keyDown(Object.assign({}, evKeyDown, { altKey: true, ctrlKey: true, keyCode: 81 })), - true - ); - assert.equal( - xterm.keyDown(Object.assign({}, evKeyDown, { altKey: true, ctrlKey: true, keyCode: 192 })), - true - ); - }); - - it('should interefere with the alt + ctrl + arrow keys', function() { - assert.equal( - xterm.keyDown(Object.assign({}, evKeyDown, { altKey: true, ctrlKey: true, keyCode: 37 })), - false - ); - assert.equal( - xterm.keyDown(Object.assign({}, evKeyDown, { altKey: true, ctrlKey: true, keyCode: 39 })), - false - ); - }); - - it('should emit key with alt + ctrl + key on keyPress', function(done) { - var keys = ['@', '@', '\\', '\\', '|', '|']; - - xterm.on('keypress', function(key) { - if (key) { - var index = keys.indexOf(key); - assert(index !== -1, "Emitted wrong key: " + key); - keys.splice(index, 1); - } - if (keys.length === 0) done(); - }); - - xterm.keyPress( - Object.assign({}, evKeyPress, { altKey: true, ctrlKey: true, keyCode: 64 }) - ); // @ - xterm.keyPress( - Object.assign({}, evKeyPress, { altKey: true, ctrlKey: true, charCode: 64, keyCode: 0 }) - ); - xterm.keyPress( - Object.assign({}, evKeyPress, { altKey: true, ctrlKey: true, keyCode: 92 }) - ); // \ - xterm.keyPress( - Object.assign({}, evKeyPress, { altKey: true, ctrlKey: true, charCode: 92, keyCode: 0 }) - ); - xterm.keyPress( - Object.assign({}, evKeyPress, { altKey: true, ctrlKey: true, keyCode: 124 }) - ); // | - xterm.keyPress( - Object.assign({}, evKeyPress, { altKey: true, ctrlKey: true, charCode: 124, keyCode: 0 }) - ); - }); - }); - }); - - describe('unicode - surrogates', function() { - it('2 characters per cell', function () { - this.timeout(10000); // This is needed because istanbul patches code and slows it down - var high = String.fromCharCode(0xD800); - for (var i=0xDC00; i<=0xDCFF; ++i) { - xterm.write(high + String.fromCharCode(i)); - var tchar = xterm.buffer.lines.get(0)[0]; - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); - expect(xterm.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX]).eql(' '); - xterm.reset(); - } - }); - it('2 characters at last cell', function() { - var high = String.fromCharCode(0xD800); - for (var i=0xDC00; i<=0xDCFF; ++i) { - xterm.buffer.x = xterm.cols - 1; - xterm.write(high + String.fromCharCode(i)); - expect(xterm.buffer.lines.get(0)[xterm.buffer.x-1][CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); - expect(xterm.buffer.lines.get(0)[xterm.buffer.x-1][CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(xterm.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX]).eql(' '); - xterm.reset(); - } - }); - it('2 characters per cell over line end with autowrap', function() { - var high = String.fromCharCode(0xD800); - for (var i=0xDC00; i<=0xDCFF; ++i) { - xterm.buffer.x = xterm.cols - 1; - xterm.wraparoundMode = true; - xterm.write('a' + high + String.fromCharCode(i)); - expect(xterm.buffer.lines.get(0)[xterm.cols-1][CHAR_DATA_CHAR_INDEX]).eql('a'); - expect(xterm.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); - expect(xterm.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(xterm.buffer.lines.get(1)[1][CHAR_DATA_CHAR_INDEX]).eql(' '); - xterm.reset(); - } - }); - it('2 characters per cell over line end without autowrap', function() { - var high = String.fromCharCode(0xD800); - for (var i=0xDC00; i<=0xDCFF; ++i) { - xterm.buffer.x = xterm.cols - 1; - xterm.wraparoundMode = false; - xterm.write('a' + high + String.fromCharCode(i)); - // auto wraparound mode should cut off the rest of the line - expect(xterm.buffer.lines.get(0)[xterm.cols-1][CHAR_DATA_CHAR_INDEX]).eql('a'); - expect(xterm.buffer.lines.get(0)[xterm.cols-1][CHAR_DATA_CHAR_INDEX].length).eql(1); - expect(xterm.buffer.lines.get(1)[1][CHAR_DATA_CHAR_INDEX]).eql(' '); - xterm.reset(); - } - }); - it('splitted surrogates', function() { - var high = String.fromCharCode(0xD800); - for (var i=0xDC00; i<=0xDCFF; ++i) { - xterm.write(high); - xterm.write(String.fromCharCode(i)); - var tchar = xterm.buffer.lines.get(0)[0]; - expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); - expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); - expect(xterm.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX]).eql(' '); - xterm.reset(); - } - }); - }); - - describe('unicode - combining characters', function() { - it('café', function () { - xterm.write('cafe\u0301'); - expect(xterm.buffer.lines.get(0)[3][CHAR_DATA_CHAR_INDEX]).eql('e\u0301'); - expect(xterm.buffer.lines.get(0)[3][CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(xterm.buffer.lines.get(0)[3][CHAR_DATA_WIDTH_INDEX]).eql(1); - }); - it('café - end of line', function() { - xterm.buffer.x = xterm.cols - 1 - 3; - xterm.write('cafe\u0301'); - expect(xterm.buffer.lines.get(0)[xterm.cols-1][CHAR_DATA_CHAR_INDEX]).eql('e\u0301'); - expect(xterm.buffer.lines.get(0)[xterm.cols-1][CHAR_DATA_CHAR_INDEX].length).eql(2); - expect(xterm.buffer.lines.get(0)[xterm.cols-1][CHAR_DATA_WIDTH_INDEX]).eql(1); - expect(xterm.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX]).eql(' '); - expect(xterm.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX].length).eql(1); - expect(xterm.buffer.lines.get(0)[1][CHAR_DATA_WIDTH_INDEX]).eql(1); - }); - it('multiple combined é', function() { - xterm.wraparoundMode = true; - xterm.write(Array(100).join('e\u0301')); - for (var i=0; iBrowser; writeBuffer: string[]; children: HTMLElement[]; cursorHidden: boolean; @@ -184,3 +185,34 @@ export class MockBuffer implements IBuffer { throw new Error('Method not implemented.'); } } + +export class MockViewport implements IViewport { + onWheel(ev: WheelEvent): void { + throw new Error('Method not implemented.'); + } + onTouchStart(ev: TouchEvent): void { + throw new Error('Method not implemented.'); + } + onTouchMove(ev: TouchEvent): void { + throw new Error('Method not implemented.'); + } + syncScrollArea(): void { } +} + +export class MockCompositionHelper implements ICompositionHelper { + compositionstart(): void { + throw new Error('Method not implemented.'); + } + compositionupdate(ev: CompositionEvent): void { + throw new Error('Method not implemented.'); + } + compositionend(): void { + throw new Error('Method not implemented.'); + } + updateCompositionElements(dontRecurse?: boolean): void { + throw new Error('Method not implemented.'); + } + keydown(ev: KeyboardEvent): boolean { + return true; + } +}