scroll/src/common/all_test.ts

602 lines
16 KiB
JavaScript
Raw Normal View History

import Ansi, * as _Ansi from './ansi.ts';
import Buffer from './buffer.ts';
2023-11-21 15:14:08 -05:00
import Document from './document.ts';
import Editor from './editor.ts';
2024-07-16 15:57:41 -04:00
import { FileType } from './filetype/mod.ts';
2024-07-02 16:27:18 -04:00
import { highlightToColor, HighlightType } from './highlight.ts';
import Option, { None, Some } from './option.ts';
import Position from './position.ts';
2023-11-21 15:14:08 -05:00
import Row from './row.ts';
import * as Fn from './fns.ts';
2023-11-30 16:14:52 -05:00
import { defaultTerminalSize, SCROLL_TAB_SIZE } from './config.ts';
2024-07-12 10:58:23 -04:00
import { getTestRunner } from './runtime/mod.ts';
import { SearchDirection } from './types.ts';
const {
2024-07-09 16:12:28 -04:00
assertEquals,
assertEquivalent,
assertExists,
assertInstanceOf,
assertNotEquals,
assertFalse,
assertTrue,
2024-07-12 10:58:23 -04:00
assertSome,
assertNone,
testSuite,
} = await getTestRunner();
2024-07-02 16:27:18 -04:00
const THIS_FILE = './src/common/all_test.ts';
// ----------------------------------------------------------------------------
// Helper Function Tests
// ----------------------------------------------------------------------------
const fnTest = () => {
const {
arrayInsert,
noop,
posSub,
minSub,
maxAdd,
ord,
strChars,
ctrlKey,
isControl,
isAscii,
isAsciiDigit,
strlen,
truncate,
} = Fn;
return {
'arrayInsert() strings': () => {
const a = ['😺', '😸', '😹'];
const b = arrayInsert(a, 1, 'x');
const c = ['😺', 'x', '😸', '😹'];
assertEquivalent(b, c);
2024-07-02 16:27:18 -04:00
const d = arrayInsert(c, 17, 'y');
const e = ['😺', 'x', '😸', '😹', 'y'];
assertEquivalent(d, e);
2024-07-02 16:27:18 -04:00
assertEquivalent(arrayInsert([], 0, 'foo'), ['foo']);
2024-07-02 16:27:18 -04:00
},
'arrayInsert() numbers': () => {
const a = [1, 3, 5];
const b = [1, 3, 4, 5];
assertEquivalent(arrayInsert(a, 2, 4), b);
2024-07-02 16:27:18 -04:00
const c = [1, 2, 3, 4, 5];
assertEquivalent(arrayInsert(b, 1, 2), c);
2024-07-02 16:27:18 -04:00
},
'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': () => {
assertEquivalent(strChars('😺😸😹'), ['😺', '😸', '😹']);
2024-07-02 16:27:18 -04:00
},
'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 { AnsiColor, Ground } = _Ansi;
return {
'color()': () => {
assertEquals(Ansi.color(AnsiColor.FgBlue), '\x1b[34m');
},
'color256()': () => {
assertEquals(Ansi.color256(128, Ground.Back), '\x1b[48;5;128m');
assertEquals(Ansi.color256(128, Ground.Fore), '\x1b[38;5;128m');
},
'rgb()': () => {
assertEquals(Ansi.rgb(32, 64, 128, Ground.Back), '\x1b[48;2;32;64;128m');
assertEquals(Ansi.rgb(32, 64, 128, Ground.Fore), '\x1b[38;2;32;64;128m');
},
'moveCursor()': () => {
assertEquals(Ansi.moveCursor(1, 2), '\x1b[2;3H');
},
'moveCursorForward()': () => {
assertEquals(Ansi.moveCursorForward(2), '\x1b[2C');
},
'moveCursorDown()': () => {
assertEquals(Ansi.moveCursorDown(7), '\x1b[7B');
},
};
};
// ----------------------------------------------------------------------------
const BufferTest = {
'new Buffer': () => {
const b = new Buffer();
assertInstanceOf(b, Buffer);
assertEquals(b.strlen(), 0);
},
'.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);
},
'.append': () => {
const b = new Buffer();
b.append('foobar');
assertEquals(b.strlen(), 6);
b.clear();
b.append('foobar', 3);
assertEquals(b.strlen(), 3);
},
'.flush': async () => {
const b = new Buffer();
b.appendLine('foobarbaz' + Ansi.ClearLine);
assertEquals(b.strlen(), 14);
await b.flush();
assertEquals(b.strlen(), 0);
},
};
// ----------------------------------------------------------------------------
const DocumentTest = {
'.default': () => {
const doc = Document.default();
assertEquals(doc.numRows, 0);
assertTrue(doc.isEmpty());
assertEquivalent(doc.row(0), None);
},
2024-07-02 16:27:18 -04:00
'.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);
},
'.find': async () => {
const doc = await Document.default().open(THIS_FILE);
const query1 = doc.find(
'dessert',
Position.default(),
SearchDirection.Forward,
);
assertTrue(query1.isSome());
2024-07-10 12:11:27 -04:00
// const pos1 = query1.unwrap();
//
// const query2 = doc.find(
// 'dessert',
// Position.at(pos1.x, 400),
// SearchDirection.Backward,
// );
// assertTrue(query2.isSome());
// const pos2 = query2.unwrap();
// assertEquivalent(pos2, pos1);
},
'.insertRow': () => {
const doc = Document.default();
doc.insertRow(undefined, 'foobar');
assertEquals(doc.numRows, 1);
assertFalse(doc.isEmpty());
assertInstanceOf(doc.row(0).unwrap(), Row);
},
'.insert': () => {
const doc = Document.default();
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).unwrap();
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);
},
2024-07-02 16:27:18 -04:00
'.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).unwrap().toString(), 'foo');
assertEquals(doc3.row(1).unwrap().toString(), 'bar');
2024-07-02 16:27:18 -04:00
},
'.delete': () => {
const doc = Document.default();
doc.insert(Position.default(), 'foobar');
doc.delete(Position.at(3, 0));
assertEquals(doc.row(0).unwrap().toString(), 'fooar');
2024-07-02 16:27:18 -04:00
// Merge next row
2024-07-02 16:27:18 -04:00
const doc2 = Document.default();
doc2.insertNewline(Position.default());
doc2.insert(Position.at(0, 1), 'foobar');
doc2.delete(Position.at(0, 0));
assertEquals(doc2.row(0).unwrap().toString(), 'foobar');
2024-07-02 16:27:18 -04:00
// Invalid delete location
2024-07-02 16:27:18 -04:00
const doc3 = Document.default();
doc3.insert(Position.default(), 'foobar');
doc3.delete(Position.at(0, 3));
assertEquals(doc3.row(0).unwrap().toString(), 'foobar');
},
};
// ----------------------------------------------------------------------------
const EditorTest = {
'new Editor': () => {
const e = new Editor(defaultTerminalSize);
assertInstanceOf(e, Editor);
},
2024-07-02 16:27:18 -04:00
'.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 () => {
2024-07-02 16:59:46 -04:00
// Dirty file (Need to clear confirmation messages)
2024-07-02 16:27:18 -04:00
const e = new Editor(defaultTerminalSize);
2024-07-02 16:59:46 -04:00
await e.processKeyPress('d');
assertTrue(await e.processKeyPress(Fn.ctrlKey('q')));
assertTrue(await e.processKeyPress(Fn.ctrlKey('q')));
assertTrue(await e.processKeyPress(Fn.ctrlKey('q')));
assertFalse(await e.processKeyPress(Fn.ctrlKey('q')));
// Clean file
const e2 = new Editor(defaultTerminalSize);
const res = await e2.processKeyPress(Fn.ctrlKey('q'));
2024-07-02 16:27:18 -04:00
assertFalse(res);
},
};
// ----------------------------------------------------------------------------
const OptionTest = {
'Option.from()': () => {
2024-07-12 10:58:23 -04:00
assertNone(Option.from(null));
assertNone(Option.from());
assertEquivalent(Option.from(undefined), None);
2024-07-12 10:58:23 -04:00
assertSome(Option.from('foo'));
assertSome(Option.from(234));
assertSome(Option.from({}));
assertSome(Some([1, 2, 3]));
assertEquivalent(Option.from(Some('foo')), Some('foo'));
assertEquivalent(Some(Some('bar')), Some('bar'));
},
'.toString': () => {
assertEquals(Some({}).toString(), 'Some ({})');
assertEquals(Some([1, 2, 3]).toString(), 'Some ([1,2,3])');
assertEquals(None.toString(), 'None');
},
};
// ----------------------------------------------------------------------------
const PositionTest = {
'.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);
},
'.from': () => {
const p1 = Position.at(1, 2);
const p2 = Position.from(p1);
p1.x = 2;
p1.y = 4;
assertEquals(p1.x, 2);
assertEquals(p1.y, 4);
assertEquals(p2.x, 1);
assertEquals(p2.y, 2);
},
};
// ----------------------------------------------------------------------------
const RowTest = {
'.default': () => {
const row = Row.default();
assertEquals(row.toString(), '');
},
'.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(), '😺😸😹');
},
'.append': () => {
const row = Row.from('foo');
2024-07-16 15:57:41 -04:00
row.append('bar', FileType.default());
assertEquals(row.toString(), 'foobar');
},
'.delete': () => {
const row = Row.from('foof');
row.delete(3);
assertEquals(row.toString(), 'foo');
row.delete(4);
assertEquals(row.toString(), 'foo');
},
'.split': () => {
// When you split a row, it's from the cursor position
// (Kind of like if the string were one-indexed)
const row = Row.from('foobar');
const row2 = Row.from('bar');
2024-07-16 15:57:41 -04:00
assertEquals(row.split(3, FileType.default()).toString(), row2.toString());
},
'.find': () => {
const normalRow = Row.from('For whom the bell tolls');
assertEquivalent(
normalRow.find('who', 0, SearchDirection.Forward),
Some(4),
);
assertEquals(normalRow.find('foo', 0, SearchDirection.Forward), None);
const emojiRow = Row.from('😺😸😹');
assertEquivalent(emojiRow.find('😹', 0, SearchDirection.Forward), Some(2));
assertEquals(emojiRow.find('🤰🏼', 10, SearchDirection.Forward), None);
},
'.find backwards': () => {
const normalRow = Row.from('For whom the bell tolls');
assertEquivalent(
normalRow.find('who', 23, SearchDirection.Backward),
Some(4),
);
assertEquals(normalRow.find('foo', 10, SearchDirection.Backward), None);
const emojiRow = Row.from('😺😸😹');
assertEquivalent(emojiRow.find('😸', 2, SearchDirection.Backward), Some(1));
assertEquals(emojiRow.find('🤰🏼', 10, SearchDirection.Backward), None);
},
'.byteIndexToCharIndex': () => {
// Each 'character' is two bytes
const row = Row.from('😺😸😹👨‍👩‍👧‍👦');
assertEquals(row.byteIndexToCharIndex(4), 2);
assertEquals(row.byteIndexToCharIndex(2), 1);
assertEquals(row.byteIndexToCharIndex(0), 0);
// Return count on nonsense index
assertEquals(Fn.strlen(row.toString()), 10);
assertEquals(row.byteIndexToCharIndex(72), 10);
const row2 = Row.from('foobar');
assertEquals(row2.byteIndexToCharIndex(2), 2);
},
'.charIndexToByteIndex': () => {
// Each 'character' is two bytes
const row = Row.from('😺😸😹👨‍👩‍👧‍👦');
assertEquals(row.charIndexToByteIndex(2), 4);
assertEquals(row.charIndexToByteIndex(1), 2);
assertEquals(row.charIndexToByteIndex(0), 0);
},
'.cxToRx, .rxToCx': () => {
const row = Row.from('foo\tbar\tbaz');
2024-07-16 15:57:41 -04:00
row.update(None, FileType.default());
assertNotEquals(row.chars, row.rchars);
assertNotEquals(row.size, row.rsize);
assertEquals(row.size, 11);
assertEquals(row.rsize, row.size + (SCROLL_TAB_SIZE * 2) - 2);
const cx = 11;
const aRx = row.cxToRx(cx);
const rx = 11;
const aCx = row.rxToCx(aRx);
assertEquals(aCx, cx);
assertEquals(aRx, rx);
},
};
// ----------------------------------------------------------------------------
// Test Suite Setup
// ----------------------------------------------------------------------------
testSuite({
2024-07-02 16:27:18 -04:00
fns: fnTest(),
highlightToColorTest,
'readKey()': readKeyTest(),
'ANSI utils': ANSITest(),
Buffer: BufferTest,
Document: DocumentTest,
Editor: EditorTest,
Option: OptionTest,
Position: PositionTest,
Row: RowTest,
2023-11-21 15:14:08 -05:00
});