1'use strict'; 2const common = require('../common'); 3const PassThrough = require('stream').PassThrough; 4const assert = require('assert'); 5const Interface = require('readline').Interface; 6 7class FakeInput extends PassThrough {} 8 9function extend(k) { 10 return Object.assign({ ctrl: false, meta: false, shift: false }, k); 11} 12 13 14const fi = new FakeInput(); 15const fo = new FakeInput(); 16new Interface({ input: fi, output: fo, terminal: true }); 17 18let keys = []; 19fi.on('keypress', (s, k) => { 20 keys.push(k); 21}); 22 23 24function addTest(sequences, expectedKeys) { 25 if (!Array.isArray(sequences)) { 26 sequences = [ sequences ]; 27 } 28 29 if (!Array.isArray(expectedKeys)) { 30 expectedKeys = [ expectedKeys ]; 31 } 32 33 expectedKeys = expectedKeys.map(extend); 34 35 keys = []; 36 37 sequences.forEach((sequence) => { 38 fi.write(sequence); 39 }); 40 assert.deepStrictEqual(keys, expectedKeys); 41} 42 43// Simulate key interval test cases 44// Returns a function that takes `next` test case and returns a thunk 45// that can be called to run tests in sequence 46// e.g. 47// addKeyIntervalTest(..) 48// (addKeyIntervalTest(..) 49// (addKeyIntervalTest(..)(noop)))() 50// where noop is a terminal function(() => {}). 51 52const addKeyIntervalTest = (sequences, expectedKeys, interval = 550, 53 assertDelay = 550) => { 54 const fn = common.mustCall((next) => () => { 55 56 if (!Array.isArray(sequences)) { 57 sequences = [ sequences ]; 58 } 59 60 if (!Array.isArray(expectedKeys)) { 61 expectedKeys = [ expectedKeys ]; 62 } 63 64 expectedKeys = expectedKeys.map(extend); 65 66 const keys = []; 67 fi.on('keypress', (s, k) => keys.push(k)); 68 69 const emitKeys = ([head, ...tail]) => { 70 if (head) { 71 fi.write(head); 72 setTimeout(() => emitKeys(tail), interval); 73 } else { 74 setTimeout(() => { 75 next(); 76 assert.deepStrictEqual(keys, expectedKeys); 77 }, assertDelay); 78 } 79 }; 80 emitKeys(sequences); 81 }); 82 return fn; 83}; 84 85// Regular alphanumerics 86addTest('io.JS', [ 87 { name: 'i', sequence: 'i' }, 88 { name: 'o', sequence: 'o' }, 89 { name: undefined, sequence: '.' }, 90 { name: 'j', sequence: 'J', shift: true }, 91 { name: 's', sequence: 'S', shift: true }, 92]); 93 94// Named characters 95addTest('\n\r\t\x1b\n\x1b\r\x1b\t', [ 96 { name: 'enter', sequence: '\n' }, 97 { name: 'return', sequence: '\r' }, 98 { name: 'tab', sequence: '\t' }, 99 { name: 'enter', sequence: '\x1b\n', meta: true }, 100 { name: 'return', sequence: '\x1b\r', meta: true }, 101 { name: 'tab', sequence: '\x1b\t', meta: true }, 102]); 103 104// Space and backspace 105addTest('\b\x7f\x1b\b\x1b\x7f\x1b\x1b \x1b ', [ 106 { name: 'backspace', sequence: '\b' }, 107 { name: 'backspace', sequence: '\x7f' }, 108 { name: 'backspace', sequence: '\x1b\b', meta: true }, 109 { name: 'backspace', sequence: '\x1b\x7f', meta: true }, 110 { name: 'space', sequence: '\x1b\x1b ', meta: true }, 111 { name: 'space', sequence: ' ' }, 112 { name: 'space', sequence: '\x1b ', meta: true }, 113]); 114 115// Escape key 116addTest('\x1b\x1b\x1b', [ 117 { name: 'escape', sequence: '\x1b\x1b\x1b', meta: true }, 118]); 119 120// Escape sequence 121addTest('\x1b]', [{ name: undefined, sequence: '\x1B]', meta: true }]); 122 123// Control keys 124addTest('\x01\x0b\x10', [ 125 { name: 'a', sequence: '\x01', ctrl: true }, 126 { name: 'k', sequence: '\x0b', ctrl: true }, 127 { name: 'p', sequence: '\x10', ctrl: true }, 128]); 129 130// Alt keys 131addTest('a\x1baA\x1bA', [ 132 { name: 'a', sequence: 'a' }, 133 { name: 'a', sequence: '\x1ba', meta: true }, 134 { name: 'a', sequence: 'A', shift: true }, 135 { name: 'a', sequence: '\x1bA', meta: true, shift: true }, 136]); 137 138// xterm/gnome ESC [ letter (with modifiers) 139/* eslint-disable max-len */ 140addTest('\x1b[2P\x1b[3P\x1b[4P\x1b[5P\x1b[6P\x1b[7P\x1b[8P\x1b[3Q\x1b[8Q\x1b[3R\x1b[8R\x1b[3S\x1b[8S', [ 141 { name: 'f1', sequence: '\x1b[2P', code: '[P', shift: true, meta: false, ctrl: false }, 142 { name: 'f1', sequence: '\x1b[3P', code: '[P', shift: false, meta: true, ctrl: false }, 143 { name: 'f1', sequence: '\x1b[4P', code: '[P', shift: true, meta: true, ctrl: false }, 144 { name: 'f1', sequence: '\x1b[5P', code: '[P', shift: false, meta: false, ctrl: true }, 145 { name: 'f1', sequence: '\x1b[6P', code: '[P', shift: true, meta: false, ctrl: true }, 146 { name: 'f1', sequence: '\x1b[7P', code: '[P', shift: false, meta: true, ctrl: true }, 147 { name: 'f1', sequence: '\x1b[8P', code: '[P', shift: true, meta: true, ctrl: true }, 148 { name: 'f2', sequence: '\x1b[3Q', code: '[Q', meta: true }, 149 { name: 'f2', sequence: '\x1b[8Q', code: '[Q', shift: true, meta: true, ctrl: true }, 150 { name: 'f3', sequence: '\x1b[3R', code: '[R', meta: true }, 151 { name: 'f3', sequence: '\x1b[8R', code: '[R', shift: true, meta: true, ctrl: true }, 152 { name: 'f4', sequence: '\x1b[3S', code: '[S', meta: true }, 153 { name: 'f4', sequence: '\x1b[8S', code: '[S', shift: true, meta: true, ctrl: true }, 154]); 155/* eslint-enable max-len */ 156 157// xterm/gnome ESC O letter 158addTest('\x1bOP\x1bOQ\x1bOR\x1bOS', [ 159 { name: 'f1', sequence: '\x1bOP', code: 'OP' }, 160 { name: 'f2', sequence: '\x1bOQ', code: 'OQ' }, 161 { name: 'f3', sequence: '\x1bOR', code: 'OR' }, 162 { name: 'f4', sequence: '\x1bOS', code: 'OS' }, 163]); 164 165// xterm/rxvt ESC [ number ~ */ 166addTest('\x1b[11~\x1b[12~\x1b[13~\x1b[14~', [ 167 { name: 'f1', sequence: '\x1b[11~', code: '[11~' }, 168 { name: 'f2', sequence: '\x1b[12~', code: '[12~' }, 169 { name: 'f3', sequence: '\x1b[13~', code: '[13~' }, 170 { name: 'f4', sequence: '\x1b[14~', code: '[14~' }, 171]); 172 173// From Cygwin and used in libuv 174addTest('\x1b[[A\x1b[[B\x1b[[C\x1b[[D\x1b[[E', [ 175 { name: 'f1', sequence: '\x1b[[A', code: '[[A' }, 176 { name: 'f2', sequence: '\x1b[[B', code: '[[B' }, 177 { name: 'f3', sequence: '\x1b[[C', code: '[[C' }, 178 { name: 'f4', sequence: '\x1b[[D', code: '[[D' }, 179 { name: 'f5', sequence: '\x1b[[E', code: '[[E' }, 180]); 181 182// Common 183addTest('\x1b[15~\x1b[17~\x1b[18~\x1b[19~\x1b[20~\x1b[21~\x1b[23~\x1b[24~', [ 184 { name: 'f5', sequence: '\x1b[15~', code: '[15~' }, 185 { name: 'f6', sequence: '\x1b[17~', code: '[17~' }, 186 { name: 'f7', sequence: '\x1b[18~', code: '[18~' }, 187 { name: 'f8', sequence: '\x1b[19~', code: '[19~' }, 188 { name: 'f9', sequence: '\x1b[20~', code: '[20~' }, 189 { name: 'f10', sequence: '\x1b[21~', code: '[21~' }, 190 { name: 'f11', sequence: '\x1b[23~', code: '[23~' }, 191 { name: 'f12', sequence: '\x1b[24~', code: '[24~' }, 192]); 193 194// xterm ESC [ letter 195addTest('\x1b[A\x1b[B\x1b[C\x1b[D\x1b[E\x1b[F\x1b[H', [ 196 { name: 'up', sequence: '\x1b[A', code: '[A' }, 197 { name: 'down', sequence: '\x1b[B', code: '[B' }, 198 { name: 'right', sequence: '\x1b[C', code: '[C' }, 199 { name: 'left', sequence: '\x1b[D', code: '[D' }, 200 { name: 'clear', sequence: '\x1b[E', code: '[E' }, 201 { name: 'end', sequence: '\x1b[F', code: '[F' }, 202 { name: 'home', sequence: '\x1b[H', code: '[H' }, 203]); 204 205// xterm/gnome ESC O letter 206addTest('\x1bOA\x1bOB\x1bOC\x1bOD\x1bOE\x1bOF\x1bOH', [ 207 { name: 'up', sequence: '\x1bOA', code: 'OA' }, 208 { name: 'down', sequence: '\x1bOB', code: 'OB' }, 209 { name: 'right', sequence: '\x1bOC', code: 'OC' }, 210 { name: 'left', sequence: '\x1bOD', code: 'OD' }, 211 { name: 'clear', sequence: '\x1bOE', code: 'OE' }, 212 { name: 'end', sequence: '\x1bOF', code: 'OF' }, 213 { name: 'home', sequence: '\x1bOH', code: 'OH' }, 214]); 215 216// Old xterm shift-arrows 217addTest('\x1bO2A\x1bO2B', [ 218 { name: 'up', sequence: '\x1bO2A', code: 'OA', shift: true }, 219 { name: 'down', sequence: '\x1bO2B', code: 'OB', shift: true }, 220]); 221 222// xterm/rxvt ESC [ number ~ 223addTest('\x1b[1~\x1b[2~\x1b[3~\x1b[4~\x1b[5~\x1b[6~', [ 224 { name: 'home', sequence: '\x1b[1~', code: '[1~' }, 225 { name: 'insert', sequence: '\x1b[2~', code: '[2~' }, 226 { name: 'delete', sequence: '\x1b[3~', code: '[3~' }, 227 { name: 'end', sequence: '\x1b[4~', code: '[4~' }, 228 { name: 'pageup', sequence: '\x1b[5~', code: '[5~' }, 229 { name: 'pagedown', sequence: '\x1b[6~', code: '[6~' }, 230]); 231 232// putty 233addTest('\x1b[[5~\x1b[[6~', [ 234 { name: 'pageup', sequence: '\x1b[[5~', code: '[[5~' }, 235 { name: 'pagedown', sequence: '\x1b[[6~', code: '[[6~' }, 236]); 237 238// rxvt 239addTest('\x1b[7~\x1b[8~', [ 240 { name: 'home', sequence: '\x1b[7~', code: '[7~' }, 241 { name: 'end', sequence: '\x1b[8~', code: '[8~' }, 242]); 243 244// gnome terminal 245addTest('\x1b[A\x1b[B\x1b[2A\x1b[2B', [ 246 { name: 'up', sequence: '\x1b[A', code: '[A' }, 247 { name: 'down', sequence: '\x1b[B', code: '[B' }, 248 { name: 'up', sequence: '\x1b[2A', code: '[A', shift: true }, 249 { name: 'down', sequence: '\x1b[2B', code: '[B', shift: true }, 250]); 251 252// `rxvt` keys with modifiers. 253// eslint-disable-next-line max-len 254addTest('\x1b[20~\x1b[2$\x1b[2^\x1b[3$\x1b[3^\x1b[5$\x1b[5^\x1b[6$\x1b[6^\x1b[7$\x1b[7^\x1b[8$\x1b[8^', [ 255 { name: 'f9', sequence: '\x1b[20~', code: '[20~' }, 256 { name: 'insert', sequence: '\x1b[2$', code: '[2$', shift: true }, 257 { name: 'insert', sequence: '\x1b[2^', code: '[2^', ctrl: true }, 258 { name: 'delete', sequence: '\x1b[3$', code: '[3$', shift: true }, 259 { name: 'delete', sequence: '\x1b[3^', code: '[3^', ctrl: true }, 260 { name: 'pageup', sequence: '\x1b[5$', code: '[5$', shift: true }, 261 { name: 'pageup', sequence: '\x1b[5^', code: '[5^', ctrl: true }, 262 { name: 'pagedown', sequence: '\x1b[6$', code: '[6$', shift: true }, 263 { name: 'pagedown', sequence: '\x1b[6^', code: '[6^', ctrl: true }, 264 { name: 'home', sequence: '\x1b[7$', code: '[7$', shift: true }, 265 { name: 'home', sequence: '\x1b[7^', code: '[7^', ctrl: true }, 266 { name: 'end', sequence: '\x1b[8$', code: '[8$', shift: true }, 267 { name: 'end', sequence: '\x1b[8^', code: '[8^', ctrl: true }, 268]); 269 270// Misc 271addTest('\x1b[Z', [ 272 { name: 'tab', sequence: '\x1b[Z', code: '[Z', shift: true }, 273]); 274 275// xterm + modifiers 276addTest('\x1b[20;5~\x1b[6;5^', [ 277 { name: 'f9', sequence: '\x1b[20;5~', code: '[20~', ctrl: true }, 278 { name: 'pagedown', sequence: '\x1b[6;5^', code: '[6^', ctrl: true }, 279]); 280 281addTest('\x1b[H\x1b[5H\x1b[1;5H', [ 282 { name: 'home', sequence: '\x1b[H', code: '[H' }, 283 { name: 'home', sequence: '\x1b[5H', code: '[H', ctrl: true }, 284 { name: 'home', sequence: '\x1b[1;5H', code: '[H', ctrl: true }, 285]); 286 287// Escape sequences broken into multiple data chunks 288addTest('\x1b[D\x1b[C\x1b[D\x1b[C'.split(''), [ 289 { name: 'left', sequence: '\x1b[D', code: '[D' }, 290 { name: 'right', sequence: '\x1b[C', code: '[C' }, 291 { name: 'left', sequence: '\x1b[D', code: '[D' }, 292 { name: 'right', sequence: '\x1b[C', code: '[C' }, 293]); 294 295// Escape sequences mixed with regular ones 296addTest('\x1b[DD\x1b[2DD\x1b[2^D', [ 297 { name: 'left', sequence: '\x1b[D', code: '[D' }, 298 { name: 'd', sequence: 'D', shift: true }, 299 { name: 'left', sequence: '\x1b[2D', code: '[D', shift: true }, 300 { name: 'd', sequence: 'D', shift: true }, 301 { name: 'insert', sequence: '\x1b[2^', code: '[2^', ctrl: true }, 302 { name: 'd', sequence: 'D', shift: true }, 303]); 304 305// Color sequences 306addTest('\x1b[31ma\x1b[39ma', [ 307 { name: 'undefined', sequence: '\x1b[31m', code: '[31m' }, 308 { name: 'a', sequence: 'a' }, 309 { name: 'undefined', sequence: '\x1b[39m', code: '[39m' }, 310 { name: 'a', sequence: 'a' }, 311]); 312 313// `rxvt` keys with modifiers. 314addTest('\x1b[a\x1b[b\x1b[c\x1b[d\x1b[e', [ 315 { name: 'up', sequence: '\x1b[a', code: '[a', shift: true }, 316 { name: 'down', sequence: '\x1b[b', code: '[b', shift: true }, 317 { name: 'right', sequence: '\x1b[c', code: '[c', shift: true }, 318 { name: 'left', sequence: '\x1b[d', code: '[d', shift: true }, 319 { name: 'clear', sequence: '\x1b[e', code: '[e', shift: true }, 320]); 321 322addTest('\x1bOa\x1bOb\x1bOc\x1bOd\x1bOe', [ 323 { name: 'up', sequence: '\x1bOa', code: 'Oa', ctrl: true }, 324 { name: 'down', sequence: '\x1bOb', code: 'Ob', ctrl: true }, 325 { name: 'right', sequence: '\x1bOc', code: 'Oc', ctrl: true }, 326 { name: 'left', sequence: '\x1bOd', code: 'Od', ctrl: true }, 327 { name: 'clear', sequence: '\x1bOe', code: 'Oe', ctrl: true }, 328]); 329 330// Reduce array of addKeyIntervalTest(..) right to left 331// with () => {} as initial function. 332const runKeyIntervalTests = [ 333 // Escape character 334 addKeyIntervalTest('\x1b', [ 335 { name: 'escape', sequence: '\x1b', meta: true }, 336 ]), 337 // Chain of escape characters. 338 addKeyIntervalTest('\x1b\x1b\x1b\x1b'.split(''), [ 339 { name: 'escape', sequence: '\x1b', meta: true }, 340 { name: 'escape', sequence: '\x1b', meta: true }, 341 { name: 'escape', sequence: '\x1b', meta: true }, 342 { name: 'escape', sequence: '\x1b', meta: true }, 343 ]), 344].reverse().reduce((acc, fn) => fn(acc), () => {}); 345 346// Run key interval tests one after another. 347runKeyIntervalTests(); 348