diff --git a/src/common/all_test.ts b/src/common/all_test.ts index bb9fdea..c9a2065 100644 --- a/src/common/all_test.ts +++ b/src/common/all_test.ts @@ -2,6 +2,7 @@ import Ansi, * as _Ansi from './ansi.ts'; import Buffer from './buffer.ts'; import Document from './document.ts'; import Editor from './editor.ts'; +import { highlightToColor, HighlightType } from './highlight.ts'; import Position from './position.ts'; import Row from './row.ts'; @@ -10,7 +11,8 @@ import { defaultTerminalSize, SCROLL_TAB_SIZE } from './config.ts'; import { getTestRunner } from './runtime.ts'; const { - assertEquals, + assertStrictEquals: assertEquals, + assertEquals: assertLooseEquals, assertExists, assertInstanceOf, assertNotEquals, @@ -20,6 +22,203 @@ const { testSuite, } = await getTestRunner(); +const THIS_FILE = './src/common/all_test.ts'; + +// ---------------------------------------------------------------------------- +// Helper Function Tests +// ---------------------------------------------------------------------------- + +const fnTest = () => { + const { + some, + none, + arrayInsert, + noop, + posSub, + minSub, + maxAdd, + ord, + strChars, + ctrlKey, + isControl, + isAscii, + isAsciiDigit, + strlen, + truncate, + } = Fn; + + return { + 'some()': () => { + assertFalse(some(null)); + assertFalse(some(void 0)); + assertFalse(some(undefined)); + assertTrue(some(0)); + assertTrue(some(false)); + }, + 'none()': () => { + assertTrue(none(null)); + assertTrue(none(void 0)); + assertTrue(none(undefined)); + assertFalse(none(0)); + assertFalse(none(false)); + }, + 'arrayInsert() strings': () => { + const a = ['😺', '😸', '😹']; + const b = arrayInsert(a, 1, 'x'); + const c = ['😺', 'x', '😸', '😹']; + assertLooseEquals(b, c); + + const d = arrayInsert(c, 17, 'y'); + const e = ['😺', 'x', '😸', '😹', 'y']; + assertLooseEquals(d, e); + + assertLooseEquals(arrayInsert([], 0, 'foo'), ['foo']); + }, + 'arrayInsert() numbers': () => { + const a = [1, 3, 5]; + const b = [1, 3, 4, 5]; + assertLooseEquals(arrayInsert(a, 2, 4), b); + + const c = [1, 2, 3, 4, 5]; + assertLooseEquals(arrayInsert(b, 1, 2), c); + }, + 'noop fn': () => { + assertExists(noop); + assertEquals(noop(), undefined); + }, + 'posSub()': () => { + assertEquals(posSub(14, 15), 0); + assertEquals(posSub(15, 1), 14); + }, + 'minSub()': () => { + assertEquals(minSub(13, 25, -1), -1); + assertEquals(minSub(25, 13, 0), 12); + }, + 'maxAdd()': () => { + assertEquals(maxAdd(99, 99, 75), 75); + assertEquals(maxAdd(25, 74, 101), 99); + }, + 'ord()': () => { + // Invalid output + assertEquals(ord(''), 256); + + // Valid output + assertEquals(ord('a'), 97); + }, + 'strChars() properly splits strings into unicode characters': () => { + assertLooseEquals(strChars('😺😸😹'), ['😺', '😸', '😹']); + }, + 'ctrlKey()': () => { + const ctrl_a = ctrlKey('a'); + assertTrue(isControl(ctrl_a)); + assertEquals(ctrl_a, String.fromCodePoint(0x01)); + + const invalid = ctrlKey('😺'); + assertFalse(isControl(invalid)); + assertEquals(invalid, '😺'); + }, + 'isAscii()': () => { + assertTrue(isAscii('asjyverkjhsdf1928374')); + assertFalse(isAscii('😺acalskjsdf')); + assertFalse(isAscii('ab😺ac')); + }, + 'isAsciiDigit()': () => { + assertTrue(isAsciiDigit('1234567890')); + assertFalse(isAsciiDigit('A1')); + assertFalse(isAsciiDigit('/')); + assertFalse(isAsciiDigit(':')); + }, + 'isControl()': () => { + assertFalse(isControl('abc')); + assertTrue(isControl(String.fromCodePoint(0x01))); + assertFalse(isControl('😺')); + }, + 'strlen()': () => { + // Ascii length + assertEquals(strlen('abc'), 'abc'.length); + + // Get number of visible unicode characters + assertEquals(strlen('😺😸😹'), 3); + assertNotEquals('😺😸😹'.length, strlen('😺😸😹')); + + // Skin tone modifier + base character + assertEquals(strlen('🀰🏼'), 2); + assertNotEquals('🀰🏼'.length, strlen('🀰🏼')); + + // This has 4 sub-characters, and 3 zero-width-joiners + assertEquals(strlen('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦'), 7); + assertNotEquals('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦'.length, strlen('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦')); + }, + 'truncate()': () => { + assertEquals(truncate('😺😸😹', 1), '😺'); + assertEquals(truncate('😺😸😹', 5), '😺😸😹'); + assertEquals(truncate('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦', 5), 'πŸ‘¨β€πŸ‘©β€πŸ‘§'); + }, + }; +}; + +const readKeyTest = () => { + const { KeyCommand } = _Ansi; + const { readKey, ctrlKey } = Fn; + + const encoder = new TextEncoder(); + + const testKeyMap = (codes: string[], expected: string) => { + codes.forEach((code) => { + assertEquals(readKey(encoder.encode(code)), expected); + }); + }; + + return { + 'empty input': () => { + assertEquals(readKey(new Uint8Array(0)), ''); + }, + 'passthrough': () => { + // Ignore unhandled escape sequences + assertEquals(readKey(encoder.encode('\x1b[]')), '\x1b[]'); + + // Pass explicitly mapped values right through + assertEquals( + readKey(encoder.encode(KeyCommand.ArrowUp)), + KeyCommand.ArrowUp, + ); + assertEquals( + readKey(encoder.encode(KeyCommand.Home)), + KeyCommand.Home, + ); + assertEquals( + readKey(encoder.encode(KeyCommand.Delete)), + KeyCommand.Delete, + ); + + // And pass through whatever else + assertEquals(readKey(encoder.encode('foobaz')), 'foobaz'); + }, + + 'Esc': () => testKeyMap(['\x1b', ctrlKey('l')], KeyCommand.Escape), + 'Backspace': () => + testKeyMap( + [ctrlKey('h'), '\x7f'], + KeyCommand.Backspace, + ), + 'Home': () => + testKeyMap(['\x1b[1~', '\x1b[7~', '\x1b[H', '\x1bOH'], KeyCommand.Home), + 'End': () => + testKeyMap(['\x1b[4~', '\x1b[8~', '\x1b[F', '\x1bOF'], KeyCommand.End), + 'Enter': () => testKeyMap(['\n', '\r', '\v'], KeyCommand.Enter), + }; +}; + +const highlightToColorTest = { + 'highlightToColor()': () => { + assertTrue(highlightToColor(HighlightType.Number).length > 0); + assertTrue(highlightToColor(HighlightType.Match).length > 0); + assertTrue(highlightToColor(HighlightType.None).length > 0); + }, +}; + +// ---------------------------------------------------------------------------- +// Tests by module // ---------------------------------------------------------------------------- const ANSITest = () => { @@ -100,6 +299,25 @@ const DocumentTest = { assertTrue(doc.isEmpty()); assertEquals(doc.row(0), null); }, + '.open': async () => { + const oldDoc = Document.default(); + oldDoc.insert(Position.default(), 'foobarbaz'); + assertTrue(oldDoc.dirty); + assertEquals(oldDoc.numRows, 1); + + const doc = await oldDoc.open(THIS_FILE); + assertFalse(doc.dirty); + assertFalse(doc.isEmpty()); + assertTrue(doc.numRows > 1); + }, + '.save': async () => { + const doc = await Document.default().open(THIS_FILE); + doc.insertNewline(Position.default()); + assertTrue(doc.dirty); + + await doc.save('test.file'); + assertFalse(doc.dirty); + }, '.insertRow': () => { const doc = Document.default(); doc.insertRow(undefined, 'foobar'); @@ -130,11 +348,52 @@ const DocumentTest = { assertEquals(doc.numRows, 2); assertTrue(doc.dirty); }, + '.insertNewline': () => { + // Invalid insert location + const doc = Document.default(); + doc.insertNewline(Position.at(0, 3)); + assertFalse(doc.dirty); + assertTrue(doc.isEmpty()); + + // Add new empty row + const doc2 = Document.default(); + doc2.insertNewline(Position.default()); + assertTrue(doc2.dirty); + assertFalse(doc2.isEmpty()); + + // Split an existing line + const doc3 = Document.default(); + doc3.insert(Position.default(), 'foobar'); + doc3.insertNewline(Position.at(3, 0)); + assertEquals(doc3.numRows, 2); + assertEquals(doc3.row(0)?.toString(), 'foo'); + assertEquals(doc3.row(1)?.toString(), 'bar'); + }, '.delete': () => { const doc = Document.default(); doc.insert(Position.default(), 'foobar'); doc.delete(Position.at(3, 0)); assertEquals(doc.row(0)?.toString(), 'fooar'); + + // Merge previous row + const doc2 = Document.default(); + doc2.insertNewline(Position.default()); + doc2.insert(Position.at(0, 1), 'foobar'); + doc2.delete(Position.at(0, 1)); + assertEquals(doc2.row(0)?.toString(), 'foobar'); + + // Merge next row + const doc3 = Document.default(); + doc3.insertNewline(Position.default()); + doc3.insert(Position.at(0, 1), 'foobar'); + doc3.delete(Position.at(0, 0)); + assertEquals(doc3.row(0)?.toString(), 'foobar'); + + // Invalid delete location + const doc4 = Document.default(); + doc4.insert(Position.default(), 'foobar'); + doc4.delete(Position.at(0, 1)); + assertEquals(doc4.row(0)?.toString(), 'foobar'); }, }; @@ -145,6 +404,21 @@ const EditorTest = { const e = new Editor(defaultTerminalSize); assertInstanceOf(e, Editor); }, + '.open': async () => { + const e = new Editor(defaultTerminalSize); + await e.open(THIS_FILE); + assertInstanceOf(e, Editor); + }, + '.processKeyPress - letters': async () => { + const e = new Editor(defaultTerminalSize); + const res = await e.processKeyPress('a'); + assertTrue(res); + }, + '.processKeyPress - ctrl-q': async () => { + const e = new Editor(defaultTerminalSize); + const res = await e.processKeyPress(Fn.ctrlKey('q')); + assertFalse(res); + }, }; // ---------------------------------------------------------------------------- @@ -262,200 +536,21 @@ const RowTest = { // ---------------------------------------------------------------------------- -const fnTest = () => { - const { - some, - none, - arrayInsert, - noop, - posSub, - minSub, - maxAdd, - ord, - strChars, - ctrlKey, - isControl, - isAscii, - isAsciiDigit, - strlen, - truncate, - } = Fn; - - return { - 'some()': () => { - assertFalse(some(null)); - assertFalse(some(void 0)); - assertFalse(some(undefined)); - assertTrue(some(0)); - assertTrue(some(false)); - }, - 'none()': () => { - assertTrue(none(null)); - assertTrue(none(void 0)); - assertTrue(none(undefined)); - assertFalse(none(0)); - assertFalse(none(false)); - }, - 'arrayInsert() strings': () => { - const a = ['😺', '😸', '😹']; - const b = arrayInsert(a, 1, 'x'); - const c = ['😺', 'x', '😸', '😹']; - assertEquals(b, c); - - const d = arrayInsert(c, 17, 'y'); - const e = ['😺', 'x', '😸', '😹', 'y']; - assertEquals(d, e); - - assertEquals(arrayInsert([], 0, 'foo'), ['foo']); - }, - 'arrayInsert() numbers': () => { - const a = [1, 3, 5]; - const b = [1, 3, 4, 5]; - assertEquals(arrayInsert(a, 2, 4), b); - - const c = [1, 2, 3, 4, 5]; - assertEquals(arrayInsert(b, 1, 2), c); - }, - 'noop fn': () => { - assertExists(noop); - assertEquals(noop(), undefined); - }, - 'posSub()': () => { - assertEquals(posSub(14, 15), 0); - assertEquals(posSub(15, 1), 14); - }, - 'minSub()': () => { - assertEquals(minSub(13, 25, -1), -1); - assertEquals(minSub(25, 13, 0), 12); - }, - 'maxAdd()': () => { - assertEquals(maxAdd(99, 99, 75), 75); - assertEquals(maxAdd(25, 74, 101), 99); - }, - 'ord()': () => { - // Invalid output - assertEquals(ord(''), 256); - - // Valid output - assertEquals(ord('a'), 97); - }, - 'strChars() properly splits strings into unicode characters': () => { - assertEquals(strChars('😺😸😹'), ['😺', '😸', '😹']); - }, - 'ctrlKey()': () => { - const ctrl_a = ctrlKey('a'); - assertTrue(isControl(ctrl_a)); - assertEquals(ctrl_a, String.fromCodePoint(0x01)); - - const invalid = ctrlKey('😺'); - assertFalse(isControl(invalid)); - assertEquals(invalid, '😺'); - }, - 'isAscii()': () => { - assertTrue(isAscii('asjyverkjhsdf1928374')); - assertFalse(isAscii('😺acalskjsdf')); - assertFalse(isAscii('ab😺ac')); - }, - 'isAsciiDigit()': () => { - assertTrue(isAsciiDigit('1234567890')); - assertFalse(isAsciiDigit('A1')); - assertFalse(isAsciiDigit('/')); - assertFalse(isAsciiDigit(':')); - }, - 'isControl()': () => { - assertFalse(isControl('abc')); - assertTrue(isControl(String.fromCodePoint(0x01))); - assertFalse(isControl('😺')); - }, - 'strlen()': () => { - // Ascii length - assertEquals(strlen('abc'), 'abc'.length); - - // Get number of visible unicode characters - assertEquals(strlen('😺😸😹'), 3); - assertNotEquals('😺😸😹'.length, strlen('😺😸😹')); - - // Skin tone modifier + base character - assertEquals(strlen('🀰🏼'), 2); - assertNotEquals('🀰🏼'.length, strlen('🀰🏼')); - - // This has 4 sub-characters, and 3 zero-width-joiners - assertEquals(strlen('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦'), 7); - assertNotEquals('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦'.length, strlen('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦')); - }, - 'truncate()': () => { - assertEquals(truncate('😺😸😹', 1), '😺'); - assertEquals(truncate('😺😸😹', 5), '😺😸😹'); - assertEquals(truncate('πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦', 5), 'πŸ‘¨β€πŸ‘©β€πŸ‘§'); - }, - }; -}; - -// ---------------------------------------------------------------------------- - -const readKeyTest = () => { - const { KeyCommand } = _Ansi; - const { readKey, ctrlKey } = Fn; - - const encoder = new TextEncoder(); - - const testKeyMap = (codes: string[], expected: string) => { - codes.forEach((code) => { - assertEquals(readKey(encoder.encode(code)), expected); - }); - }; - - return { - 'empty input': () => { - assertEquals(readKey(new Uint8Array(0)), ''); - }, - 'passthrough': () => { - // Ignore unhandled escape sequences - assertEquals(readKey(encoder.encode('\x1b[]')), '\x1b[]'); - - // Pass explicitly mapped values right through - assertEquals( - readKey(encoder.encode(KeyCommand.ArrowUp)), - KeyCommand.ArrowUp, - ); - assertEquals( - readKey(encoder.encode(KeyCommand.Home)), - KeyCommand.Home, - ); - assertEquals( - readKey(encoder.encode(KeyCommand.Delete)), - KeyCommand.Delete, - ); - - // And pass through whatever else - assertEquals(readKey(encoder.encode('foobaz')), 'foobaz'); - }, - - 'Esc': () => testKeyMap(['\x1b', ctrlKey('l')], KeyCommand.Escape), - 'Backspace': () => - testKeyMap( - [ctrlKey('h'), '\x7f'], - KeyCommand.Backspace, - ), - 'Home': () => - testKeyMap(['\x1b[1~', '\x1b[7~', '\x1b[H', '\x1bOH'], KeyCommand.Home), - 'End': () => - testKeyMap(['\x1b[4~', '\x1b[8~', '\x1b[F', '\x1bOF'], KeyCommand.End), - 'Enter': () => testKeyMap(['\n', '\r', '\v'], KeyCommand.Enter), - }; -}; +const SearchTest = {}; // ---------------------------------------------------------------------------- // Test Suite Setup // ---------------------------------------------------------------------------- testSuite({ + fns: fnTest(), + highlightToColorTest, + 'readKey()': readKeyTest(), 'ANSI utils': ANSITest(), Buffer: BufferTest, Document: DocumentTest, Editor: EditorTest, Position: PositionTest, Row: RowTest, - fns: fnTest(), - 'readKey()': readKeyTest(), + Search: SearchTest, }); diff --git a/src/common/document.ts b/src/common/document.ts index 69ed56d..9d58841 100644 --- a/src/common/document.ts +++ b/src/common/document.ts @@ -58,7 +58,7 @@ export class Document { /** * Save the current document */ - public async save(filename: string) { + public async save(filename: string): Promise { const { file } = await getRuntime(); await file.saveFile(filename, this.rowsToString()); @@ -66,7 +66,7 @@ export class Document { this.dirty = false; } - public resetFind() { + public resetFind(): void { this.#search = new Search(); this.#search.parent = this; } @@ -108,16 +108,23 @@ export class Document { this.dirty = true; } + /** + * Insert a new line, splitting and/or creating a new row as needed + */ public insertNewline(at: Position): void { if (at.y > this.numRows) { return; } + // Just add a simple blank line if (at.y === this.numRows) { this.#rows.push(Row.default()); + this.dirty = true; return; } + // Split the current row, and insert a new + // row with the leftovers const newRow = this.#rows[at.y].split(at.x); newRow.update(); this.#rows = arrayInsert(this.#rows, at.y + 1, newRow); diff --git a/src/common/editor.ts b/src/common/editor.ts index 9058c46..5a5e8f4 100644 --- a/src/common/editor.ts +++ b/src/common/editor.ts @@ -337,6 +337,7 @@ class Editor { /** * Filter out any additional unwanted keyboard input + * * @param input * @private */ diff --git a/src/deno/test_base.ts b/src/deno/test_base.ts index 0ebaeac..c7f1820 100644 --- a/src/deno/test_base.ts +++ b/src/deno/test_base.ts @@ -4,7 +4,6 @@ const { assertEquals, assertExists, assertInstanceOf, - AssertionError, assertNotEquals, assertStrictEquals, } = stdAssert; @@ -24,21 +23,9 @@ const DenoTestBase: ITestBase = { assertInstanceOf, assertNotEquals, assertStrictEquals, - assertTrue: function (actual: boolean): void { - if (actual !== true) { - throw new AssertionError(`actual: "${actual}" expected to be true"`); - } - }, - assertFalse(actual: boolean): void { - if (actual !== false) { - throw new AssertionError(`actual: "${actual}" expected to be false"`); - } - }, - assertNull(actual: boolean): void { - if (actual !== null) { - throw new AssertionError(`actual: "${actual}" expected to be null"`); - } - }, + assertTrue: (actual: boolean) => assertStrictEquals(actual, true), + assertFalse: (actual: boolean) => assertStrictEquals(actual, false), + assertNull: (actual: any) => assertEquals(actual, null), testSuite, };