import { Ansi, KeyCommand, readKey } from './ansi.ts'; import Buffer from './buffer.ts'; import Document from './document.ts'; import Editor from './editor.ts'; import Row from './row.ts'; import { getTestRunner } from './runtime.ts'; import { defaultTerminalSize } from './termios.ts'; import { Position } from './types.ts'; import * as Util from './utils.ts'; const { assertEquals, assertExists, assertInstanceOf, assertNotEquals, assertFalse, assertTrue, testSuite, } = await getTestRunner(); const testKeyMap = (codes: string[], expected: string) => { codes.forEach((code) => { assertEquals(readKey(code), expected); }); }; testSuite({ 'ANSI::ANSI utils': { 'Ansi.moveCursor': () => { assertEquals(Ansi.moveCursor(1, 2), '\x1b[2;3H'); }, 'Ansi.moveCursorForward': () => { assertEquals(Ansi.moveCursorForward(2), '\x1b[2C'); }, 'Ansi.moveCursorDown': () => { assertEquals(Ansi.moveCursorDown(7), '\x1b[7B'); }, }, 'ANSI::readKey()': { 'readKey passthrough': () => { // Ignore unhandled escape sequences assertEquals(readKey('\x1b[]'), '\x1b[]'); // Pass explicitly mapped values right through assertEquals(readKey(KeyCommand.ArrowUp), KeyCommand.ArrowUp); assertEquals(readKey(KeyCommand.Home), KeyCommand.Home); assertEquals(readKey(KeyCommand.Delete), KeyCommand.Delete); // And pass through whatever else assertEquals(readKey('foobaz'), 'foobaz'); }, 'readKey Esc': () => testKeyMap(['\x1b', Util.ctrlKey('l')], KeyCommand.Escape), 'readKey Backspace': () => testKeyMap( [Util.ctrlKey('h'), '\x7f'], KeyCommand.Backspace, ), 'readKey Home': () => testKeyMap(['\x1b[1~', '\x1b[7~', '\x1b[H', '\x1bOH'], KeyCommand.Home), 'readKey End': () => testKeyMap(['\x1b[4~', '\x1b[8~', '\x1b[F', '\x1bOF'], KeyCommand.End), 'readKey Enter': () => testKeyMap(['\n', '\r', '\v'], KeyCommand.Enter), }, Buffer: { 'Buffer exists': () => { const b = new Buffer(); assertInstanceOf(b, Buffer); assertEquals(b.strlen(), 0); }, 'Buffer.appendLine': () => { const b = new Buffer(); // Carriage return and line feed b.appendLine(); assertEquals(b.strlen(), 2); b.clear(); assertEquals(b.strlen(), 0); b.appendLine('foo'); assertEquals(b.strlen(), 5); }, 'Buffer.append': () => { const b = new Buffer(); b.append('foobar'); assertEquals(b.strlen(), 6); b.clear(); b.append('foobar', 3); assertEquals(b.strlen(), 3); }, 'Buffer.flush': async () => { const b = new Buffer(); b.appendLine('foobarbaz' + Ansi.ClearLine); assertEquals(b.strlen(), 14); await b.flush(); assertEquals(b.strlen(), 0); }, }, Document: { 'Document.empty': () => { const doc = Document.empty(); assertEquals(doc.numRows, 0); assertTrue(doc.isEmpty()); assertEquals(doc.row(0), null); }, 'Document.insertRow': () => { const doc = Document.empty(); doc.insertRow(undefined, 'foobar'); assertEquals(doc.numRows, 1); assertFalse(doc.isEmpty()); assertInstanceOf(doc.row(0), Row); }, 'Document.insert': () => { const doc = Document.empty(); assertFalse(doc.dirty); doc.insert(Position.at(0, 0), 'foobar'); assertEquals(doc.numRows, 1); assertTrue(doc.dirty); doc.insert(Position.at(2, 0), 'baz'); assertEquals(doc.numRows, 1); assertTrue(doc.dirty); doc.insert(Position.at(9, 0), 'buzz'); assertEquals(doc.numRows, 1); assertTrue(doc.dirty); const row0 = doc.row(0); assertEquals(row0?.toString(), 'foobazbarbuzz'); assertEquals(row0?.rstring(), 'foobazbarbuzz'); assertEquals(row0?.rsize, 13); doc.insert(Position.at(0, 1), 'Lorem Ipsum'); assertEquals(doc.numRows, 2); assertTrue(doc.dirty); }, 'Document.delete': () => { const doc = Document.empty(); doc.insert(Position.default(), 'foobar'); doc.delete(Position.at(3, 0)); assertEquals(doc.row(0)?.toString(), 'fooar'); }, }, Editor: { 'new Editor': () => { const e = new Editor(defaultTerminalSize); assertInstanceOf(e, Editor); }, }, Position: { 'default': () => { const p = Position.default(); assertEquals(p.x, 0); assertEquals(p.y, 0); }, 'at': () => { const p = Position.at(5, 7); assertEquals(p.x, 5); assertEquals(p.y, 7); }, }, Row: { 'Row.default': () => { const row = Row.default(); assertEquals(row.toString(), ''); }, 'Row.from': () => { // From string const row = Row.from('xyz'); assertEquals(row.toString(), 'xyz'); // From existing Row assertEquals(Row.from(row).toString(), row.toString()); // From 'chars' assertEquals(Row.from(['😺', '😸', '😹']).toString(), '😺😸😹'); }, }, 'Util misc fns': { 'arrayInsert() strings': () => { const a = ['😺', '😸', '😹']; const b = Util.arrayInsert(a, 1, 'x'); const c = ['😺', 'x', '😸', '😹']; assertEquals(b, c); const d = Util.arrayInsert(c, 17, 'y'); const e = ['😺', 'x', '😸', '😹', 'y']; assertEquals(d, e); assertEquals(Util.arrayInsert([], 0, 'foo'), ['foo']); }, 'arrayInsert() numbers': () => { const a = [1, 3, 5]; const b = [1, 3, 4, 5]; assertEquals(Util.arrayInsert(a, 2, 4), b); const c = [1, 2, 3, 4, 5]; assertEquals(Util.arrayInsert(b, 1, 2), c); }, 'noop fn': () => { assertExists(Util.noop); assertEquals(Util.noop(), undefined); }, 'posSub()': () => { assertEquals(Util.posSub(14, 15), 0); assertEquals(Util.posSub(15, 1), 14); }, 'minSub()': () => { assertEquals(Util.minSub(13, 25, -1), -1); assertEquals(Util.minSub(25, 13, 0), 12); }, 'maxAdd()': () => { assertEquals(Util.maxAdd(99, 99, 75), 75); assertEquals(Util.maxAdd(25, 74, 101), 99); }, }, 'Util string fns': { 'ord()': () => { // Invalid output assertEquals(Util.ord(''), 256); // Valid output assertEquals(Util.ord('a'), 97); }, 'chars() properly splits strings into unicode characters': () => { assertEquals(Util.chars('😺😸😹'), ['😺', '😸', '😹']); }, 'ctrl_key()': () => { const ctrl_a = Util.ctrlKey('a'); assertTrue(Util.isControl(ctrl_a)); assertEquals(ctrl_a, String.fromCodePoint(0x01)); const invalid = Util.ctrlKey('😺'); assertFalse(Util.isControl(invalid)); assertEquals(invalid, '😺'); }, 'is_ascii()': () => { assertTrue(Util.isAscii('asjyverkjhsdf1928374')); assertFalse(Util.isAscii('😺acalskjsdf')); assertFalse(Util.isAscii('ab😺ac')); }, 'is_control()': () => { assertFalse(Util.isControl('abc')); assertTrue(Util.isControl(String.fromCodePoint(0x01))); assertFalse(Util.isControl('😺')); }, 'strlen()': () => { // Ascii length assertEquals(Util.strlen('abc'), 'abc'.length); // Get number of visible unicode characters assertEquals(Util.strlen('😺😸😹'), 3); assertNotEquals('😺😸😹'.length, Util.strlen('😺😸😹')); // Skin tone modifier + base character assertEquals(Util.strlen('🀰🏼'), 2); assertNotEquals('🀰🏼'.length, Util.strlen('🀰🏼')); // This has 4 sub-characters, and 3 zero-width-joiners assertEquals(Util.strlen('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦'), 7); assertNotEquals('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦'.length, Util.strlen('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦')); }, 'truncate()': () => { assertEquals(Util.truncate('😺😸😹', 1), '😺'); assertEquals(Util.truncate('😺😸😹', 5), '😺😸😹'); assertEquals(Util.truncate('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦', 5), 'πŸ‘¨β€πŸ‘©β€πŸ‘§'); }, }, });