Increase test coverage
All checks were successful
timw4mail/scroll/pipeline/head This commit looks good

This commit is contained in:
Timothy Warren 2024-07-02 16:27:18 -04:00
parent 82bcc72d21
commit cf80dce335
4 changed files with 293 additions and 203 deletions

View File

@ -2,6 +2,7 @@ import Ansi, * as _Ansi from './ansi.ts';
import Buffer from './buffer.ts'; import Buffer from './buffer.ts';
import Document from './document.ts'; import Document from './document.ts';
import Editor from './editor.ts'; import Editor from './editor.ts';
import { highlightToColor, HighlightType } from './highlight.ts';
import Position from './position.ts'; import Position from './position.ts';
import Row from './row.ts'; import Row from './row.ts';
@ -10,7 +11,8 @@ import { defaultTerminalSize, SCROLL_TAB_SIZE } from './config.ts';
import { getTestRunner } from './runtime.ts'; import { getTestRunner } from './runtime.ts';
const { const {
assertEquals, assertStrictEquals: assertEquals,
assertEquals: assertLooseEquals,
assertExists, assertExists,
assertInstanceOf, assertInstanceOf,
assertNotEquals, assertNotEquals,
@ -20,6 +22,203 @@ const {
testSuite, testSuite,
} = await getTestRunner(); } = 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 = () => { const ANSITest = () => {
@ -100,6 +299,25 @@ const DocumentTest = {
assertTrue(doc.isEmpty()); assertTrue(doc.isEmpty());
assertEquals(doc.row(0), null); 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': () => { '.insertRow': () => {
const doc = Document.default(); const doc = Document.default();
doc.insertRow(undefined, 'foobar'); doc.insertRow(undefined, 'foobar');
@ -130,11 +348,52 @@ const DocumentTest = {
assertEquals(doc.numRows, 2); assertEquals(doc.numRows, 2);
assertTrue(doc.dirty); 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': () => { '.delete': () => {
const doc = Document.default(); const doc = Document.default();
doc.insert(Position.default(), 'foobar'); doc.insert(Position.default(), 'foobar');
doc.delete(Position.at(3, 0)); doc.delete(Position.at(3, 0));
assertEquals(doc.row(0)?.toString(), 'fooar'); 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); const e = new Editor(defaultTerminalSize);
assertInstanceOf(e, Editor); 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 SearchTest = {};
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),
};
};
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Test Suite Setup // Test Suite Setup
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
testSuite({ testSuite({
fns: fnTest(),
highlightToColorTest,
'readKey()': readKeyTest(),
'ANSI utils': ANSITest(), 'ANSI utils': ANSITest(),
Buffer: BufferTest, Buffer: BufferTest,
Document: DocumentTest, Document: DocumentTest,
Editor: EditorTest, Editor: EditorTest,
Position: PositionTest, Position: PositionTest,
Row: RowTest, Row: RowTest,
fns: fnTest(), Search: SearchTest,
'readKey()': readKeyTest(),
}); });

View File

@ -58,7 +58,7 @@ export class Document {
/** /**
* Save the current document * Save the current document
*/ */
public async save(filename: string) { public async save(filename: string): Promise<void> {
const { file } = await getRuntime(); const { file } = await getRuntime();
await file.saveFile(filename, this.rowsToString()); await file.saveFile(filename, this.rowsToString());
@ -66,7 +66,7 @@ export class Document {
this.dirty = false; this.dirty = false;
} }
public resetFind() { public resetFind(): void {
this.#search = new Search(); this.#search = new Search();
this.#search.parent = this; this.#search.parent = this;
} }
@ -108,16 +108,23 @@ export class Document {
this.dirty = true; this.dirty = true;
} }
/**
* Insert a new line, splitting and/or creating a new row as needed
*/
public insertNewline(at: Position): void { public insertNewline(at: Position): void {
if (at.y > this.numRows) { if (at.y > this.numRows) {
return; return;
} }
// Just add a simple blank line
if (at.y === this.numRows) { if (at.y === this.numRows) {
this.#rows.push(Row.default()); this.#rows.push(Row.default());
this.dirty = true;
return; return;
} }
// Split the current row, and insert a new
// row with the leftovers
const newRow = this.#rows[at.y].split(at.x); const newRow = this.#rows[at.y].split(at.x);
newRow.update(); newRow.update();
this.#rows = arrayInsert(this.#rows, at.y + 1, newRow); this.#rows = arrayInsert(this.#rows, at.y + 1, newRow);

View File

@ -337,6 +337,7 @@ class Editor {
/** /**
* Filter out any additional unwanted keyboard input * Filter out any additional unwanted keyboard input
*
* @param input * @param input
* @private * @private
*/ */

View File

@ -4,7 +4,6 @@ const {
assertEquals, assertEquals,
assertExists, assertExists,
assertInstanceOf, assertInstanceOf,
AssertionError,
assertNotEquals, assertNotEquals,
assertStrictEquals, assertStrictEquals,
} = stdAssert; } = stdAssert;
@ -24,21 +23,9 @@ const DenoTestBase: ITestBase = {
assertInstanceOf, assertInstanceOf,
assertNotEquals, assertNotEquals,
assertStrictEquals, assertStrictEquals,
assertTrue: function (actual: boolean): void { assertTrue: (actual: boolean) => assertStrictEquals(actual, true),
if (actual !== true) { assertFalse: (actual: boolean) => assertStrictEquals(actual, false),
throw new AssertionError(`actual: "${actual}" expected to be true"`); assertNull: (actual: any) => assertEquals(actual, null),
}
},
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"`);
}
},
testSuite, testSuite,
}; };