Mike Frysinger | 598e801 | 2022-09-07 08:38:34 -0400 | [diff] [blame] | 1 | // Copyright 2022 The ChromiumOS Authors |
Jason Lin | abad756 | 2022-08-22 14:49:05 +1000 | [diff] [blame] | 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | /** |
| 6 | * @fileoverview Unit tests for terminal_emulator.js. |
| 7 | */ |
| 8 | |
Jason Lin | c0f14fe | 2022-10-25 15:31:29 +1100 | [diff] [blame] | 9 | import {hterm, lib} from './deps_local.concat.js'; |
Mike Frysinger | 75895da | 2022-10-04 00:42:28 +0545 | [diff] [blame] | 10 | |
Jason Lin | abad756 | 2022-08-22 14:49:05 +1000 | [diff] [blame] | 11 | import {sleep} from './terminal_common.js'; |
Jason Lin | a8adea5 | 2022-10-25 13:14:14 +1100 | [diff] [blame] | 12 | import {A11yButtons, Modifier, XtermTerminal, XtermTerminalTestParams, |
Jason Lin | 97a0428 | 2023-03-06 10:36:56 +1100 | [diff] [blame^] | 13 | encodeKeyCombo, keyCodes} from './terminal_emulator.js'; |
Jason Lin | 5690e75 | 2022-08-30 15:36:45 +1000 | [diff] [blame] | 14 | import {MockFunction, MockObject} from './terminal_test_mocks.js'; |
Jason Lin | a8adea5 | 2022-10-25 13:14:14 +1100 | [diff] [blame] | 15 | import {Terminal} from './xterm.js'; |
Jason Lin | abad756 | 2022-08-22 14:49:05 +1000 | [diff] [blame] | 16 | |
| 17 | describe('terminal_emulator_tests.js', function() { |
| 18 | describe('XtermTerminal', function() { |
| 19 | beforeEach(async function() { |
| 20 | this.mocks = { |
Jason Lin | e9231bc | 2022-09-01 13:54:02 +1000 | [diff] [blame] | 21 | term: new MockObject({ |
| 22 | options: {}, |
| 23 | parser: { |
| 24 | registerOscHandler: () => {}, |
| 25 | }, |
| 26 | }), |
Jason Lin | abad756 | 2022-08-22 14:49:05 +1000 | [diff] [blame] | 27 | fontManager: new MockObject(), |
Jason Lin | 2649da2 | 2022-10-12 10:16:44 +1100 | [diff] [blame] | 28 | xtermInternal: new MockObject({ |
| 29 | getActualCellDimensions: () => ({width: 9, height: 22}), |
| 30 | }), |
Jason Lin | 97a0428 | 2023-03-06 10:36:56 +1100 | [diff] [blame^] | 31 | onVTKeystroke: new MockFunction(), |
Jason Lin | abad756 | 2022-08-22 14:49:05 +1000 | [diff] [blame] | 32 | }; |
| 33 | const testParams = {}; |
Jason Lin | 97a0428 | 2023-03-06 10:36:56 +1100 | [diff] [blame^] | 34 | for (const prop of ['term', 'fontManager', 'xtermInternal']) { |
Jason Lin | abad756 | 2022-08-22 14:49:05 +1000 | [diff] [blame] | 35 | testParams[prop] = this.mocks[prop].proxy; |
| 36 | } |
| 37 | |
| 38 | this.terminal = new XtermTerminal({ |
| 39 | storage: new lib.Storage.Memory(), |
| 40 | profileId: 'test', |
| 41 | enableWebGL: true, |
| 42 | testParams: /** @type {!XtermTerminalTestParams} */(testParams), |
| 43 | }); |
Jason Lin | c2504ae | 2022-09-02 13:03:31 +1000 | [diff] [blame] | 44 | |
Jason Lin | 97a0428 | 2023-03-06 10:36:56 +1100 | [diff] [blame^] | 45 | this.terminal.io.onVTKeystroke = this.mocks.onVTKeystroke.proxy; |
| 46 | |
Jason Lin | c2504ae | 2022-09-02 13:03:31 +1000 | [diff] [blame] | 47 | // Some hacking because we don't run the decorate() function. Maybe we |
| 48 | // should just run it. |
| 49 | this.terminal.container_ = /** @type {!Element} */({ |
| 50 | offsetWidth: 1000, |
| 51 | offsetHeight: 500, |
| 52 | }); |
Jason Lin | 8de3d28 | 2022-09-01 21:29:05 +1000 | [diff] [blame] | 53 | this.terminal.inited_ = true; |
Jason Lin | abad756 | 2022-08-22 14:49:05 +1000 | [diff] [blame] | 54 | }); |
| 55 | |
| 56 | describe('updateFont_()', function() { |
| 57 | it('updates font', async function() { |
| 58 | const updateFontPromise = this.terminal.updateFont_('font one'); |
| 59 | assert.deepEqual( |
| 60 | await this.mocks.fontManager.whenCalled('loadFont'), |
| 61 | [['font one']]); |
| 62 | assert.equal(this.mocks.term.baseObj.options.fontFamily, undefined); |
| 63 | assert.isNotNull(this.terminal.pendingFont_); |
Jason Lin | abad756 | 2022-08-22 14:49:05 +1000 | [diff] [blame] | 64 | |
| 65 | await updateFontPromise; |
| 66 | assert.equal(this.mocks.term.baseObj.options.fontFamily, 'font one'); |
| 67 | assert.isNull(this.terminal.pendingFont_); |
| 68 | await sleep(0); |
Jason Lin | abad756 | 2022-08-22 14:49:05 +1000 | [diff] [blame] | 69 | }); |
| 70 | |
| 71 | it('refresh font when the font is the same', async function() { |
| 72 | this.mocks.term.baseObj.options.fontFamily = 'font one'; |
| 73 | const updateFontPromise = this.terminal.updateFont_('font one'); |
| 74 | assert.deepEqual( |
| 75 | await this.mocks.fontManager.whenCalled('loadFont'), |
| 76 | [['font one']]); |
| 77 | assert.equal(this.mocks.term.baseObj.options.fontFamily, 'font one'); |
| 78 | assert.isNotNull(this.terminal.pendingFont_); |
Jason Lin | abad756 | 2022-08-22 14:49:05 +1000 | [diff] [blame] | 79 | |
| 80 | await updateFontPromise; |
| 81 | // Note the extra space at the end. |
| 82 | assert.equal(this.mocks.term.baseObj.options.fontFamily, 'font one '); |
| 83 | assert.isNull(this.terminal.pendingFont_); |
| 84 | await sleep(0); |
Jason Lin | abad756 | 2022-08-22 14:49:05 +1000 | [diff] [blame] | 85 | }); |
| 86 | |
| 87 | it('aborts if pendingFont_ was changed', async function() { |
| 88 | const updateFontPromise = this.terminal.updateFont_('font one'); |
| 89 | assert.deepEqual( |
| 90 | await this.mocks.fontManager.whenCalled('loadFont'), |
| 91 | [['font one']]); |
| 92 | assert.equal(this.mocks.term.baseObj.options.fontFamily, undefined); |
| 93 | assert.isNotNull(this.terminal.pendingFont_); |
Jason Lin | abad756 | 2022-08-22 14:49:05 +1000 | [diff] [blame] | 94 | |
| 95 | this.terminal.pendingFont_ = 'font two'; |
| 96 | |
| 97 | await updateFontPromise; |
| 98 | assert.equal(this.mocks.term.baseObj.options.fontFamily, undefined); |
| 99 | assert.equal(this.terminal.pendingFont_, 'font two'); |
| 100 | await sleep(0); |
Jason Lin | abad756 | 2022-08-22 14:49:05 +1000 | [diff] [blame] | 101 | }); |
| 102 | }); |
Jason Lin | 5690e75 | 2022-08-30 15:36:45 +1000 | [diff] [blame] | 103 | |
Jason Lin | 97a0428 | 2023-03-06 10:36:56 +1100 | [diff] [blame^] | 104 | describe('handleKeyEvent_', function() { |
| 105 | it('keyDownHandlers_', async function() { |
| 106 | const mockHandler = new MockFunction(); |
| 107 | const fakeEvent = { |
| 108 | type: 'keydown', |
| 109 | keyCode: 65, |
| 110 | ctrlKey: true, |
| 111 | }; |
| 112 | this.terminal.keyDownHandlers_.set(encodeKeyCombo(Modifier.Ctrl, 65), |
| 113 | mockHandler.proxy); |
| 114 | assert.isTrue(this.terminal.handleKeyEvent_(fakeEvent)); |
| 115 | const history = mockHandler.popHistory(); |
| 116 | assert.equal(history.length, 1); |
| 117 | assert.equal(history[0][0], fakeEvent); |
Jason Lin | 5690e75 | 2022-08-30 15:36:45 +1000 | [diff] [blame] | 118 | |
Jason Lin | 97a0428 | 2023-03-06 10:36:56 +1100 | [diff] [blame^] | 119 | assert.isTrue(this.terminal.handleKeyEvent_({...fakeEvent, |
| 120 | type: 'keypress'})); |
| 121 | assert.isEmpty(mockHandler.popHistory()); |
Jason Lin | 5690e75 | 2022-08-30 15:36:45 +1000 | [diff] [blame] | 122 | |
Jason Lin | 97a0428 | 2023-03-06 10:36:56 +1100 | [diff] [blame^] | 123 | assert.isFalse(this.terminal.handleKeyEvent_({...fakeEvent, |
| 124 | shiftKey: true})); |
| 125 | assert.isEmpty(mockHandler.popHistory()); |
Jason Lin | 5690e75 | 2022-08-30 15:36:45 +1000 | [diff] [blame] | 126 | |
Jason Lin | 97a0428 | 2023-03-06 10:36:56 +1100 | [diff] [blame^] | 127 | assert.isFalse(this.terminal.handleKeyEvent_({...fakeEvent, |
| 128 | keyCode: 66})); |
| 129 | assert.isEmpty(mockHandler.popHistory()); |
Jason Lin | 5690e75 | 2022-08-30 15:36:45 +1000 | [diff] [blame] | 130 | |
Jason Lin | 97a0428 | 2023-03-06 10:36:56 +1100 | [diff] [blame^] | 131 | assert.isFalse(this.terminal.handleKeyEvent_({...fakeEvent, |
| 132 | ctrlKey: false})); |
| 133 | assert.isEmpty(mockHandler.popHistory()); |
| 134 | }); |
| 135 | |
| 136 | it('arrow keys and 6 pack keys', async function() { |
| 137 | const check = (ev, handled, vtKeystroke) => { |
| 138 | const mockPreventDefault = new MockFunction(); |
| 139 | const mockStopPropagation = new MockFunction(); |
| 140 | assert.equal(this.terminal.handleKeyEvent_({ |
| 141 | type: 'keydown', |
| 142 | preventDefault: mockPreventDefault.proxy, |
| 143 | stopPropagation: mockStopPropagation.proxy, |
| 144 | ...ev, |
| 145 | }), handled); |
| 146 | const history = this.mocks.onVTKeystroke.popHistory(); |
| 147 | if (vtKeystroke) { |
| 148 | assert.deepEqual(history, [[vtKeystroke]]); |
| 149 | } else { |
| 150 | assert.isEmpty(history); |
| 151 | } |
| 152 | assert.equal(mockPreventDefault.getHistory().length, handled ? 1 : 0); |
| 153 | assert.equal(mockStopPropagation.getHistory().length, |
| 154 | handled ? 1 : 0); |
| 155 | }; |
| 156 | |
| 157 | check({keyCode: keyCodes.UP}, false, null); |
| 158 | check({keyCode: keyCodes.UP, shiftKey: true}, true, '\x1b[1;2A'); |
| 159 | check({keyCode: keyCodes.UP, altKey: true}, true, '\x1b[1;3A'); |
| 160 | check({keyCode: keyCodes.UP, shiftKey: true, altKey: true}, true, |
| 161 | '\x1b[1;4A'); |
| 162 | |
| 163 | check({keyCode: keyCodes.INSERT}, false, null); |
| 164 | check({keyCode: keyCodes.INSERT, altKey: true}, true, '\x1b[2;3~'); |
| 165 | check({keyCode: keyCodes.INSERT, shiftKey: true, altKey: true}, true, |
| 166 | '\x1b[2;4~'); |
| 167 | |
| 168 | check({keyCode: keyCodes.HOME}, false, null); |
| 169 | check({keyCode: keyCodes.HOME, altKey: true}, true, '\x1b[1;3H'); |
| 170 | check({keyCode: keyCodes.HOME, shiftKey: true, altKey: true}, true, |
| 171 | '\x1b[1;4H'); |
| 172 | |
| 173 | // Shift+HOME should scroll the page. |
| 174 | assert.equal(this.mocks.term.getMethodHistory('scrollToTop').length, 0); |
| 175 | check({keyCode: keyCodes.HOME, shiftKey: true}, true, null); |
| 176 | assert.equal(this.mocks.term.getMethodHistory('scrollToTop').length, 1); |
| 177 | |
| 178 | // For non-keydown event, if a modifier key is depressed, we do nothing |
| 179 | // but `handleKeyEvent_()` will return true to prevent xterm.js from |
| 180 | // handling it. |
| 181 | check({type: 'keypress', keyCode: keyCodes.HOME, altKey: true}, true, |
| 182 | undefined); |
| 183 | // If there is no modifiers, we still pass through it to xterm.js. |
| 184 | check({type: 'keypress', keyCode: keyCodes.HOME}, false, null); |
| 185 | }); |
Jason Lin | 5690e75 | 2022-08-30 15:36:45 +1000 | [diff] [blame] | 186 | }); |
Jason Lin | abad756 | 2022-08-22 14:49:05 +1000 | [diff] [blame] | 187 | }); |
Jason Lin | a8adea5 | 2022-10-25 13:14:14 +1100 | [diff] [blame] | 188 | |
| 189 | describe('A11yButtons', () => { |
| 190 | const ROWS = 5; |
| 191 | |
| 192 | beforeEach(function() { |
| 193 | this.elem = document.createElement('div'); |
| 194 | this.elem.style.height = '500px'; |
| 195 | this.elem.style.width = '500px'; |
| 196 | document.body.appendChild(this.elem); |
| 197 | |
| 198 | this.terminal = new Terminal({cols: 80, rows: ROWS, |
| 199 | allowProposedApi: true}); |
| 200 | this.htermA11yReaderMock = new MockObject(); |
Jason Lin | c0f14fe | 2022-10-25 15:31:29 +1100 | [diff] [blame] | 201 | this.a11yButtons = new A11yButtons(this.terminal, |
Jason Lin | a8adea5 | 2022-10-25 13:14:14 +1100 | [diff] [blame] | 202 | /** @type {!hterm.AccessibilityReader} */( |
| 203 | this.htermA11yReaderMock.proxy)); |
| 204 | |
| 205 | this.write = async (content) => { |
| 206 | return new Promise((resolve) => this.terminal.write(content, resolve)); |
| 207 | }; |
| 208 | }); |
| 209 | |
| 210 | afterEach(function() { |
| 211 | this.terminal.dispose(); |
| 212 | document.body.removeChild(this.elem); |
| 213 | }); |
| 214 | |
| 215 | it('announceScreenContent_', async function() { |
| 216 | this.a11yButtons.announceScreenContent_(); |
| 217 | assert.deepEqual( |
| 218 | this.htermA11yReaderMock.popMethodHistory('assertiveAnnounce'), |
| 219 | [['100% scrolled,']]); |
| 220 | |
| 221 | await this.write('hello'); |
| 222 | this.a11yButtons.announceScreenContent_(); |
| 223 | assert.deepEqual( |
| 224 | this.htermA11yReaderMock.popMethodHistory('assertiveAnnounce'), |
| 225 | [['100% scrolled,\nhello']]); |
| 226 | |
| 227 | await this.write('\r\nworld'); |
| 228 | this.a11yButtons.announceScreenContent_(); |
| 229 | assert.deepEqual( |
| 230 | this.htermA11yReaderMock.popMethodHistory('assertiveAnnounce'), |
| 231 | [['100% scrolled,\nhello\nworld']]); |
| 232 | |
| 233 | for (let i = 0; i < ROWS; ++i) { |
| 234 | await this.write(`\r\n${i}`); |
| 235 | } |
| 236 | this.a11yButtons.announceScreenContent_(); |
| 237 | assert.deepEqual( |
| 238 | this.htermA11yReaderMock.popMethodHistory('assertiveAnnounce'), |
| 239 | [['100% scrolled,\n0\n1\n2\n3\n4']]); |
| 240 | |
| 241 | this.terminal.scrollLines(-1); |
| 242 | this.a11yButtons.announceScreenContent_(); |
| 243 | assert.deepEqual( |
| 244 | this.htermA11yReaderMock.popMethodHistory('assertiveAnnounce'), |
| 245 | [['50% scrolled,\nworld\n0\n1\n2\n3']]); |
| 246 | |
| 247 | this.terminal.scrollLines(-1); |
| 248 | this.a11yButtons.announceScreenContent_(); |
| 249 | assert.deepEqual( |
| 250 | this.htermA11yReaderMock.popMethodHistory('assertiveAnnounce'), |
| 251 | [['0% scrolled,\nhello\nworld\n0\n1\n2']]); |
| 252 | }); |
| 253 | }); |
Jason Lin | abad756 | 2022-08-22 14:49:05 +1000 | [diff] [blame] | 254 | }); |