More tests
This commit is contained in:
parent
301196352a
commit
8b5fb17603
9
coverage.sh
Executable file
9
coverage.sh
Executable file
@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
rm -fr /cov_profile/
|
||||
deno test --allow-all --coverage=cov_profile
|
||||
deno coverage cov_profile --lcov > cov_profile/cov_profile.lcov
|
||||
genhtml -o cov_profile cov_profile/cov_profile.lcov
|
||||
rm cov_profile/*.json
|
||||
open cov_profile/index.html
|
||||
|
||||
|
2
justfile
2
justfile
@ -48,7 +48,7 @@ deno-check:
|
||||
|
||||
# Test with deno
|
||||
deno-test:
|
||||
deno test --allow-all
|
||||
deno test --allow-all --unstable
|
||||
|
||||
# Create test coverage report with deno
|
||||
deno-coverage:
|
||||
|
@ -42,6 +42,7 @@ const BunFFI: IFFI = {
|
||||
tcsetattr,
|
||||
cfmakeraw,
|
||||
getPointer: ptr,
|
||||
close: cStdLib.close,
|
||||
};
|
||||
|
||||
export default BunFFI;
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { IFIO } from '../common/runtime.ts';
|
||||
import { IFileIO } from '../common/runtime.ts';
|
||||
|
||||
import { appendFileSync, readFileSync } from 'node:fs';
|
||||
import { appendFile } from 'node:fs/promises';
|
||||
|
||||
const BunFileIO: IFIO = {
|
||||
const BunFileIO: IFileIO = {
|
||||
openFile: async (path: string): Promise<string> => {
|
||||
const file = await Bun.file(path);
|
||||
return await file.text();
|
||||
|
@ -4,6 +4,11 @@
|
||||
import { ITerminal, ITerminalSize } from '../common/mod.ts';
|
||||
import Ansi from '../common/ansi.ts';
|
||||
|
||||
const defaultTerminalSize: ITerminalSize = {
|
||||
rows: 24,
|
||||
cols: 80,
|
||||
};
|
||||
|
||||
const BunTerminalIO: ITerminal = {
|
||||
// Deno only returns arguments passed to the script, so
|
||||
// remove the bun runtime executable, and entry script arguments
|
||||
@ -32,8 +37,8 @@ const BunTerminalIO: ITerminal = {
|
||||
const rawCode = (new TextDecoder()).decode(chunk);
|
||||
const res = rawCode.trim().replace(/^.\[([0-9]+;[0-9]+)R$/, '$1');
|
||||
const [srows, scols] = res.split(';');
|
||||
const rows = parseInt(srows, 10);
|
||||
const cols = parseInt(scols, 10);
|
||||
const rows = parseInt(srows, 10) ?? 24;
|
||||
const cols = parseInt(scols, 10) ?? 80;
|
||||
|
||||
// Clear the screen
|
||||
await write(Ansi.ClearScreen + Ansi.ResetCursor);
|
||||
@ -44,10 +49,7 @@ const BunTerminalIO: ITerminal = {
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
rows: 24,
|
||||
cols: 80,
|
||||
};
|
||||
return defaultTerminalSize;
|
||||
},
|
||||
writeStdout: async function write(s: string): Promise<void> {
|
||||
const buffer = new TextEncoder().encode(s);
|
||||
|
@ -43,11 +43,13 @@ export const Ansi = {
|
||||
moveCursorDown: (row: number): string => ANSI_PREFIX + `${row}B`,
|
||||
};
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
export function readKey(raw: Uint8Array): string {
|
||||
const parsed = decoder.decode(raw);
|
||||
|
||||
/**
|
||||
* Convert input from ANSI escape sequences into a form
|
||||
* that can be more easily mapped to editor commands
|
||||
*
|
||||
* @param parsed - the decoded chunk of input
|
||||
*/
|
||||
export function readKey(parsed: string): string {
|
||||
// Return the input if it's unambiguous
|
||||
if (parsed in KeyCommand) {
|
||||
return parsed;
|
||||
|
33
src/common/ansi_test.ts
Normal file
33
src/common/ansi_test.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { getTestRunner } from './mod.ts';
|
||||
import Ansi, { KeyCommand, readKey } from './ansi.ts';
|
||||
|
||||
getTestRunner().then((t) => {
|
||||
t.test('Ansi.moveCursor', () => {
|
||||
t.assertEquals(Ansi.moveCursor(1, 2), '\x1b[2;3H');
|
||||
});
|
||||
|
||||
t.test('Ansi.moveCursorForward', () => {
|
||||
t.assertEquals(Ansi.moveCursorForward(2), '\x1b[2C');
|
||||
});
|
||||
|
||||
t.test('Ansi.moveCursorDown', () => {
|
||||
t.assertEquals(Ansi.moveCursorDown(7), '\x1b[7B');
|
||||
});
|
||||
|
||||
t.test('readKey', () => {
|
||||
// Ignore unhandled escape sequences
|
||||
t.assertEquals(readKey('\x1b[]'), '\x1b[]');
|
||||
|
||||
// Pass explicitly mapped values right through
|
||||
t.assertEquals(readKey(KeyCommand.ArrowUp), KeyCommand.ArrowUp);
|
||||
t.assertEquals(readKey(KeyCommand.Home), KeyCommand.Home);
|
||||
|
||||
['\x1bOH', '\x1b[7~', '\x1b[1~', '\x1b[H'].forEach((code) => {
|
||||
t.assertEquals(readKey(code), KeyCommand.Home);
|
||||
});
|
||||
|
||||
['\x1bOF', '\x1b[8~', '\x1b[4~', '\x1b[F'].forEach((code) => {
|
||||
t.assertEquals(readKey(code), KeyCommand.End);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,5 +1,5 @@
|
||||
import { strlen, truncate } from './utils.ts';
|
||||
import { getRuntime } from './runtime.ts';
|
||||
import { getRuntime, importForRuntime } from './runtime.ts';
|
||||
|
||||
class Buffer {
|
||||
#b = '';
|
||||
@ -12,7 +12,7 @@ class Buffer {
|
||||
}
|
||||
|
||||
public appendLine(s = ''): void {
|
||||
this.#b += (s ?? '') + '\r\n';
|
||||
this.#b += s + '\r\n';
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
@ -23,7 +23,7 @@ class Buffer {
|
||||
* Output the contents of the buffer into stdout
|
||||
*/
|
||||
public async flush() {
|
||||
const { term } = await getRuntime();
|
||||
const term = await importForRuntime('terminal_io');
|
||||
await term.writeStdout(this.#b);
|
||||
this.clear();
|
||||
}
|
||||
|
45
src/common/buffer_test.ts
Normal file
45
src/common/buffer_test.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { getTestRunner } from './runtime.ts';
|
||||
import Buffer from './buffer.ts';
|
||||
|
||||
getTestRunner().then((t) => {
|
||||
t.test('Buffer exists', () => {
|
||||
const b = new Buffer();
|
||||
t.assertInstanceOf(b, Buffer);
|
||||
t.assertEquals(b.strlen(), 0);
|
||||
});
|
||||
|
||||
t.test('Buffer.appendLine', () => {
|
||||
const b = new Buffer();
|
||||
|
||||
// Carriage return and line feed
|
||||
b.appendLine();
|
||||
t.assertEquals(b.strlen(), 2);
|
||||
|
||||
b.clear();
|
||||
t.assertEquals(b.strlen(), 0);
|
||||
|
||||
b.appendLine('foo');
|
||||
t.assertEquals(b.strlen(), 5);
|
||||
});
|
||||
|
||||
t.test('Buffer.append', () => {
|
||||
const b = new Buffer();
|
||||
|
||||
b.append('foobar');
|
||||
t.assertEquals(b.strlen(), 6);
|
||||
b.clear();
|
||||
|
||||
b.append('foobar', 3);
|
||||
t.assertEquals(b.strlen(), 3);
|
||||
});
|
||||
|
||||
t.test('Buffer.flush', async () => {
|
||||
const b = new Buffer();
|
||||
b.append('foobarbaz');
|
||||
t.assertEquals(b.strlen(), 9);
|
||||
|
||||
await b.flush();
|
||||
|
||||
t.assertEquals(b.strlen(), 0);
|
||||
});
|
||||
});
|
@ -1,7 +1,7 @@
|
||||
import Ansi, { KeyCommand } from './ansi.ts';
|
||||
import Buffer from './buffer.ts';
|
||||
import Document from './document.ts';
|
||||
import { IPoint, ITerminalSize, VERSION } from './mod.ts';
|
||||
import { IPoint, ITerminalSize, logToFile, VERSION } from './mod.ts';
|
||||
import { ctrl_key } from './utils.ts';
|
||||
|
||||
export class Editor {
|
||||
@ -152,12 +152,17 @@ export class Editor {
|
||||
|
||||
private drawFileRow(y: number): void {
|
||||
const row = this.#document.row(y);
|
||||
let len = row?.chars.length ?? 0;
|
||||
if (row === null) {
|
||||
logToFile(`Warning: trying to draw non-existent row '${y}'`);
|
||||
return this.drawPlaceholderRow(y);
|
||||
}
|
||||
|
||||
let len = row.chars.length ?? 0;
|
||||
if (len > this.#screen.cols) {
|
||||
len = this.#screen.cols;
|
||||
}
|
||||
|
||||
this.#buffer.append(row!.toString(), len);
|
||||
this.#buffer.append(row.toString(), len);
|
||||
this.#buffer.appendLine(Ansi.ClearLine);
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ import { getTermios } from './termios.ts';
|
||||
import { Editor } from './editor.ts';
|
||||
|
||||
export async function main() {
|
||||
const decoder = new TextDecoder();
|
||||
const runTime = await getRuntime();
|
||||
const { term, file, onExit, onEvent } = runTime;
|
||||
|
||||
@ -12,14 +13,13 @@ export async function main() {
|
||||
t.enableRawMode();
|
||||
onExit(() => {
|
||||
t.disableRawMode();
|
||||
t.cleanup();
|
||||
});
|
||||
|
||||
// Setup error handler to log to file
|
||||
onEvent('error', (error) => {
|
||||
t.disableRawMode();
|
||||
if (error instanceof ErrorEvent) {
|
||||
file.appendFileSync('./scroll.err', JSON.stringify(error, null, 2));
|
||||
}
|
||||
file.appendFileSync('./scroll.err', JSON.stringify(error, null, 2));
|
||||
});
|
||||
|
||||
const terminalSize = await term.getTerminalSize();
|
||||
@ -39,7 +39,7 @@ export async function main() {
|
||||
// The main event loop
|
||||
for await (const chunk of term.inputLoop()) {
|
||||
// Process input
|
||||
const char = readKey(chunk);
|
||||
const char = readKey(decoder.decode(chunk));
|
||||
const shouldLoop = editor.processKeyPress(char);
|
||||
if (!shouldLoop) {
|
||||
return 0;
|
||||
|
@ -1,14 +1,25 @@
|
||||
import { getTermios } from './termios.ts';
|
||||
import { noop } from './utils.ts';
|
||||
import { ITestBase } from './types.ts';
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Runtime adapter interfaces
|
||||
// ----------------------------------------------------------------------------
|
||||
export enum RunTimeType {
|
||||
Bun = 'bun',
|
||||
Deno = 'deno',
|
||||
Unknown = 'common',
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Runtime adapter interfaces
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The size of terminal in rows and columns
|
||||
*/
|
||||
export interface ITerminalSize {
|
||||
rows: number;
|
||||
cols: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The native functions for getting/setting terminal settings
|
||||
*/
|
||||
@ -33,14 +44,11 @@ export interface IFFI {
|
||||
*/
|
||||
// deno-lint-ignore no-explicit-any
|
||||
getPointer(ta: any): unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* The size of terminal in rows and columns
|
||||
*/
|
||||
export interface ITerminalSize {
|
||||
rows: number;
|
||||
cols: number;
|
||||
/**
|
||||
* Closes the FFI handle
|
||||
*/
|
||||
close(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -51,10 +59,11 @@ export interface ITerminal {
|
||||
* The arguments passed to the program on launch
|
||||
*/
|
||||
argv: string[];
|
||||
|
||||
/**
|
||||
* The generator function returning chunks of input from the stdin stream
|
||||
*/
|
||||
inputLoop(): AsyncGenerator<Uint8Array, void, unknown>;
|
||||
inputLoop(): AsyncGenerator<Uint8Array, void>;
|
||||
|
||||
/**
|
||||
* Get the size of the terminal
|
||||
@ -70,7 +79,7 @@ export interface ITerminal {
|
||||
/**
|
||||
* Runtime-specific file handling
|
||||
*/
|
||||
export interface IFIO {
|
||||
export interface IFileIO {
|
||||
openFile(path: string): Promise<string>;
|
||||
openFileSync(path: string): string;
|
||||
appendFile(path: string, contents: string): Promise<void>;
|
||||
@ -99,7 +108,7 @@ export interface IRuntime {
|
||||
/**
|
||||
* Runtime-specific file system io
|
||||
*/
|
||||
file: IFIO;
|
||||
file: IFileIO;
|
||||
|
||||
/**
|
||||
* Set up an event handler
|
||||
@ -132,6 +141,15 @@ export interface IRuntime {
|
||||
|
||||
let scrollRuntime: IRuntime | null = null;
|
||||
|
||||
export function logToFile(s: unknown) {
|
||||
importForRuntime('file_io').then((f) => {
|
||||
const raw = (typeof s === 'string') ? s : JSON.stringify(s, null, 2);
|
||||
const output = raw + '\n';
|
||||
|
||||
f.appendFile('./scroll.log', output).then(noop);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill program, displaying an error message
|
||||
* @param s
|
||||
@ -148,7 +166,7 @@ export function die(s: string | Error): void {
|
||||
/**
|
||||
* Determine which Typescript runtime we are operating under
|
||||
*/
|
||||
export function getRuntimeType(): RunTimeType {
|
||||
export function runtimeType(): RunTimeType {
|
||||
let runtime = RunTimeType.Unknown;
|
||||
|
||||
if ('Deno' in globalThis) {
|
||||
@ -166,7 +184,7 @@ export function getRuntimeType(): RunTimeType {
|
||||
*/
|
||||
export async function getRuntime(): Promise<IRuntime> {
|
||||
if (scrollRuntime === null) {
|
||||
const runtime = getRuntimeType();
|
||||
const runtime = runtimeType();
|
||||
const path = `../${runtime}/mod.ts`;
|
||||
|
||||
const pkg = await import(path);
|
||||
@ -178,16 +196,30 @@ export async function getRuntime(): Promise<IRuntime> {
|
||||
return Promise.resolve(scrollRuntime!);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the common test interface object
|
||||
*/
|
||||
export async function getTestRunner(): Promise<ITestBase> {
|
||||
const runtime = runtimeType();
|
||||
const path = `../${runtime}/test_base.ts`;
|
||||
const pkg = await import(path);
|
||||
if ('default' in pkg) {
|
||||
return pkg.default;
|
||||
}
|
||||
|
||||
return pkg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a runtime-specific module
|
||||
*
|
||||
* eg. to load "src/bun/mod.ts", if the runtime is bun,
|
||||
* e.g. to load "src/bun/mod.ts", if the runtime is bun,
|
||||
* you can use like so `await importForRuntime('index')`;
|
||||
*
|
||||
* @param path - the path within the runtime module
|
||||
*/
|
||||
export const importForRuntime = async (path: string) => {
|
||||
const runtime = getRuntimeType();
|
||||
const runtime = runtimeType();
|
||||
const suffix = '.ts';
|
||||
const base = `../${runtime}/`;
|
||||
|
||||
@ -198,20 +230,10 @@ export const importForRuntime = async (path: string) => {
|
||||
const cleanedPath = pathParts.join('/');
|
||||
const importPath = base + cleanedPath + suffix;
|
||||
|
||||
return await import(importPath);
|
||||
};
|
||||
|
||||
/**
|
||||
* Import the default export for a runtime-specific module
|
||||
* (this is just a simple wrapper of `importForRuntime`)
|
||||
*
|
||||
* @param path - the path within the runtime module
|
||||
*/
|
||||
export const importDefaultForRuntime = async (path: string) => {
|
||||
const pkg = await importForRuntime(path);
|
||||
const pkg = await import(importPath);
|
||||
if ('default' in pkg) {
|
||||
return pkg.default;
|
||||
}
|
||||
|
||||
return null;
|
||||
return pkg;
|
||||
};
|
||||
|
@ -31,13 +31,13 @@ class Termios {
|
||||
* The data for the termios struct we are manipulating
|
||||
* @private
|
||||
*/
|
||||
readonly #termios: Uint8Array;
|
||||
#termios: Uint8Array;
|
||||
|
||||
/**
|
||||
* The pointer to the termios struct
|
||||
* @private
|
||||
*/
|
||||
readonly #ptr: any;
|
||||
#ptr;
|
||||
|
||||
constructor(ffi: IFFI) {
|
||||
this.#ffi = ffi;
|
||||
@ -51,6 +51,13 @@ class Termios {
|
||||
this.#ptr = ffi.getPointer(this.#termios);
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
this.#ptr = null;
|
||||
this.#cookedTermios = new Uint8Array(0);
|
||||
this.#termios = new Uint8Array(0);
|
||||
this.#ffi.close();
|
||||
}
|
||||
|
||||
enableRawMode() {
|
||||
if (this.#inRawMode) {
|
||||
throw new Error('Can not enable raw mode when in raw mode');
|
||||
|
@ -1,11 +1,30 @@
|
||||
export function noop() {}
|
||||
// ----------------------------------------------------------------------------
|
||||
// Misc
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
export const noop = () => {};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Strings
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get the codepoint of the first byte of a string. If the string
|
||||
* is empty, this will return 256
|
||||
*
|
||||
* @param s - the string
|
||||
*/
|
||||
export function ord(s: string): number {
|
||||
if (s.length > 0) {
|
||||
return s.codePointAt(0)!;
|
||||
}
|
||||
|
||||
return 256;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a string by graphemes, not just bytes
|
||||
*
|
||||
* @param s - the string to split into 'characters'
|
||||
*/
|
||||
export function chars(s: string): string[] {
|
||||
@ -14,6 +33,7 @@ export function chars(s: string): string[] {
|
||||
|
||||
/**
|
||||
* Get the 'character length' of a string, not its UTF16 byte count
|
||||
*
|
||||
* @param s - the string to check
|
||||
*/
|
||||
export function strlen(s: string): number {
|
||||
@ -21,19 +41,12 @@ export function strlen(s: string): number {
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the character part of ascii?
|
||||
* Are all the characters in the string in ASCII range?
|
||||
*
|
||||
* @param char - a one character string to check
|
||||
* @param char - string to check
|
||||
*/
|
||||
export function is_ascii(char: string): boolean {
|
||||
return chars(char).every((char) => {
|
||||
const point = char.codePointAt(0);
|
||||
if (point === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return point < 0x80;
|
||||
});
|
||||
return chars(char).every((char) => ord(char) < 0x80);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -42,22 +55,19 @@ export function is_ascii(char: string): boolean {
|
||||
* @param char - a one character string to check
|
||||
*/
|
||||
export function is_control(char: string): boolean {
|
||||
const code = char.codePointAt(0);
|
||||
if (code === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const code = ord(char);
|
||||
return is_ascii(char) && (code === 0x7f || code < 0x20);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the key code for a ctrl chord
|
||||
*
|
||||
* @param char - a one character string
|
||||
*/
|
||||
export function ctrl_key(char: string): string {
|
||||
// This is the normal use case, of course
|
||||
if (is_ascii(char)) {
|
||||
const point = char.codePointAt(0)!;
|
||||
const point = ord(char);
|
||||
return String.fromCodePoint(point & 0x1f);
|
||||
}
|
||||
|
||||
|
@ -1,59 +1,74 @@
|
||||
import { importDefaultForRuntime, ITestBase } from './mod.ts';
|
||||
import { getTestRunner } from './mod.ts';
|
||||
import {
|
||||
chars,
|
||||
ctrl_key,
|
||||
is_ascii,
|
||||
is_control,
|
||||
noop,
|
||||
ord,
|
||||
strlen,
|
||||
truncate,
|
||||
} from './utils.ts';
|
||||
|
||||
const t: ITestBase = await importDefaultForRuntime('test_base');
|
||||
getTestRunner().then((t) => {
|
||||
t.test('noop fn', () => {
|
||||
t.assertExists(noop);
|
||||
t.assertEquals(noop(), undefined);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// Strings
|
||||
// ---------------------------------------------------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------
|
||||
// Strings
|
||||
// ---------------------------------------------------------------------------
|
||||
t.test('ord fn returns 256 on invalid string', () => {
|
||||
t.assertEquals(ord(''), 256);
|
||||
});
|
||||
|
||||
t.test('chars fn properly splits strings into unicode characters', () => {
|
||||
t.assertEquals(chars('😺😸😹'), ['😺', '😸', '😹']);
|
||||
});
|
||||
|
||||
t.test('ctrl_key fn returns expected values', () => {
|
||||
const ctrl_a = ctrl_key('a');
|
||||
t.assertTrue(is_control(ctrl_a));
|
||||
t.assertEquals(ctrl_a, String.fromCodePoint(0x01));
|
||||
|
||||
const invalid = ctrl_key('😺');
|
||||
t.assertFalse(is_control(invalid));
|
||||
t.assertEquals(invalid, '😺');
|
||||
});
|
||||
|
||||
t.test('is_ascii properly discerns ascii chars', () => {
|
||||
t.assertTrue(is_ascii('asjyverkjhsdf1928374'));
|
||||
t.assertFalse(is_ascii('😺acalskjsdf'));
|
||||
});
|
||||
|
||||
t.test('is_control fn works as expected', () => {
|
||||
t.assertFalse(is_control('abc'));
|
||||
t.assertTrue(is_control(String.fromCodePoint(0x01)));
|
||||
t.assertFalse(is_control('😺'));
|
||||
});
|
||||
|
||||
t.test('strlen fn returns expected length for multibyte characters', () => {
|
||||
t.assertEquals(strlen('😺😸😹'), 3);
|
||||
t.assertNotEquals('😺😸😹'.length, strlen('😺😸😹'));
|
||||
|
||||
// Skin tone modifier + base character
|
||||
t.assertEquals(strlen('🤰🏼'), 2);
|
||||
t.assertNotEquals('🤰🏼'.length, strlen('🤰🏼'));
|
||||
|
||||
// This has 4 sub-characters, and 3 zero-width-joiners
|
||||
t.assertEquals(strlen('👨👩👧👦'), 7);
|
||||
t.assertNotEquals('👨👩👧👦'.length, strlen('👨👩👧👦'));
|
||||
});
|
||||
|
||||
t.test('truncate shortens strings', () => {
|
||||
t.assertEquals(truncate('😺😸😹', 1), '😺');
|
||||
t.assertEquals(truncate('😺😸😹', 5), '😺😸😹');
|
||||
t.assertEquals(truncate('👨👩👧👦', 5), '👨👩👧');
|
||||
t.test('chars fn properly splits strings into unicode characters', () => {
|
||||
t.assertEquals(chars('😺😸😹'), ['😺', '😸', '😹']);
|
||||
});
|
||||
|
||||
t.test('ctrl_key fn returns expected values', () => {
|
||||
const ctrl_a = ctrl_key('a');
|
||||
t.assertTrue(is_control(ctrl_a));
|
||||
t.assertEquals(ctrl_a, String.fromCodePoint(0x01));
|
||||
|
||||
const invalid = ctrl_key('😺');
|
||||
t.assertFalse(is_control(invalid));
|
||||
t.assertEquals(invalid, '😺');
|
||||
});
|
||||
|
||||
t.test('is_ascii properly discerns ascii chars', () => {
|
||||
t.assertTrue(is_ascii('asjyverkjhsdf1928374'));
|
||||
t.assertFalse(is_ascii('😺acalskjsdf'));
|
||||
t.assertFalse(is_ascii('ab😺ac'));
|
||||
});
|
||||
|
||||
t.test('is_control fn works as expected', () => {
|
||||
t.assertFalse(is_control('abc'));
|
||||
t.assertTrue(is_control(String.fromCodePoint(0x01)));
|
||||
t.assertFalse(is_control('😺'));
|
||||
});
|
||||
|
||||
t.test('strlen fn returns expected length for ascii strings', () => {
|
||||
t.assertEquals(strlen('abc'), 'abc'.length);
|
||||
});
|
||||
|
||||
t.test('strlen fn returns expected length for multibyte characters', () => {
|
||||
t.assertEquals(strlen('😺😸😹'), 3);
|
||||
t.assertNotEquals('😺😸😹'.length, strlen('😺😸😹'));
|
||||
|
||||
// Skin tone modifier + base character
|
||||
t.assertEquals(strlen('🤰🏼'), 2);
|
||||
t.assertNotEquals('🤰🏼'.length, strlen('🤰🏼'));
|
||||
|
||||
// This has 4 sub-characters, and 3 zero-width-joiners
|
||||
t.assertEquals(strlen('👨👩👧👦'), 7);
|
||||
t.assertNotEquals('👨👩👧👦'.length, strlen('👨👩👧👦'));
|
||||
});
|
||||
|
||||
t.test('truncate shortens strings', () => {
|
||||
t.assertEquals(truncate('😺😸😹', 1), '😺');
|
||||
t.assertEquals(truncate('😺😸😹', 5), '😺😸😹');
|
||||
t.assertEquals(truncate('👨👩👧👦', 5), '👨👩👧');
|
||||
});
|
||||
});
|
||||
|
1
src/deno/deps.ts
Normal file
1
src/deno/deps.ts
Normal file
@ -0,0 +1 @@
|
||||
export * as stdAssert from 'https://deno.land/std@0.205.0/assert/mod.ts';
|
@ -39,6 +39,7 @@ const DenoFFI: IFFI = {
|
||||
tcsetattr,
|
||||
cfmakeraw,
|
||||
getPointer: Deno.UnsafePointer.of,
|
||||
close: cStdLib.close,
|
||||
};
|
||||
|
||||
export default DenoFFI;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { IFIO } from '../common/runtime.ts';
|
||||
import { IFileIO } from '../common/runtime.ts';
|
||||
|
||||
const DenoFileIO: IFIO = {
|
||||
const DenoFileIO: IFileIO = {
|
||||
openFile: async function (path: string): Promise<string> {
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
const data = await Deno.readFile(path);
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { ITestBase } from '../common/mod.ts';
|
||||
|
||||
import {
|
||||
import { stdAssert } from './deps.ts';
|
||||
const {
|
||||
assertEquals,
|
||||
assertExists,
|
||||
assertInstanceOf,
|
||||
AssertionError,
|
||||
assertNotEquals,
|
||||
assertStrictEquals,
|
||||
} from 'https://deno.land/std/assert/mod.ts';
|
||||
} = stdAssert;
|
||||
|
||||
class TestBase implements ITestBase {
|
||||
test(name: string, fn: () => void): void {
|
||||
|
Loading…
Reference in New Issue
Block a user