added some more unit tests

This commit is contained in:
Steven Hugg committed 2026-08-15 12:39:16 +02:00
1 parent 4ae7b5541d
commit 1122de18a5
4 files changed
+1799

No files matched your search

+660
View File
@@ -0,0 +1,660 @@
import assert from "assert";
import { describe, it } from "mocha";
import {
DebugSymbols,
isDebuggable,
Debuggable,
BreakpointList,
BasePlatform,
BaseDebugPlatform,
Base6502Platform,
BaseZ80Platform,
Base6809Platform,
EmuState,
CpuState,
OpcodeMetadata,
Platform,
getToolForFilename_6502,
getToolForFilename_z80,
getToolForFilename_6809,
getOpcodeMetadata_6502,
cpuStateToLongString_6502,
cpuStateToLongString_Z80,
cpuStateToLongString_6809,
inspectSymbol,
lookupSymbol,
dumpStackToString,
} from "../../src/common/baseplatform";
describe('DebugSymbols', function () {
it('should create symbol map with correct structure', function () {
const symbolmap = { main: 0x0000, loop: 0x0010, end: 0x0100 };
const debuginfo = { platform: 'test' };
const symbols = new DebugSymbols(symbolmap, debuginfo);
assert.strictEqual(symbols.symbolmap['main'], 0x0000);
assert.strictEqual(symbols.symbolmap['loop'], 0x0010);
assert.strictEqual(symbols.symbolmap['end'], 0x0100);
});
it('should create inverted address-to-symbol map', function () {
const symbolmap = { main: 0x0000, loop: 0x0010, end: 0x0100 };
const debuginfo = {};
const symbols = new DebugSymbols(symbolmap, debuginfo);
assert.strictEqual(symbols.addr2symbol[0x0000], 'main');
assert.strictEqual(symbols.addr2symbol[0x0010], 'loop');
assert.strictEqual(symbols.addr2symbol[0x0100], 'end');
});
it('should ensure sentinel values in address map', function () {
const symbolmap = { loop: 0x1000 };
const debuginfo = {};
const symbols = new DebugSymbols(symbolmap, debuginfo);
// Empty symbol map gets $00 sentinel
assert.strictEqual(symbols.addr2symbol[0x0], '$00');
assert.strictEqual(symbols.addr2symbol[0x10000], '__END__');
});
it('should store debug info', function () {
const debuginfo = { platform: 'c64', version: '1.0' };
const symbols = new DebugSymbols({}, debuginfo);
assert.deepStrictEqual(symbols.debuginfo, debuginfo);
});
it('should handle empty symbol map', function () {
const symbols = new DebugSymbols({}, {});
assert.deepStrictEqual(symbols.symbolmap, {});
assert.ok(symbols.addr2symbol[0x0] !== undefined);
});
it('should handle duplicate addresses with last symbol winning', function () {
const symbolmap = { sym1: 0x100, sym2: 0x100 };
const symbols = new DebugSymbols(symbolmap, {});
// One of them should be in the inverted map
assert.ok(symbols.addr2symbol[0x100] === 'sym1' || symbols.addr2symbol[0x100] === 'sym2');
});
});
describe('isDebuggable type guard', function () {
it('should identify debuggable objects', function () {
const debuggable: Debuggable = {
getDebugCategories: () => ['CPU', 'Memory'],
getDebugInfo: (category: string) => 'debug info'
};
assert.ok(isDebuggable(debuggable));
});
it('should reject non-debuggable objects', function () {
assert.ok(!isDebuggable({}));
assert.ok(!isDebuggable(null));
assert.ok(!isDebuggable(undefined));
assert.ok(!isDebuggable({ foo: 'bar' }));
});
it('should accept partial implementations', function () {
const partial = {
getDebugCategories: () => ['CPU']
};
assert.ok(isDebuggable(partial));
});
});
describe('BreakpointList', function () {
it('should return null when no breakpoints exist', function () {
const list = new BreakpointList();
const cond = list.getDebugCondition();
assert.strictEqual(cond, null);
});
it('should create debug condition for single breakpoint', function () {
const list = new BreakpointList();
let hitCount = 0;
list.id2bp['bp1'] = { cond: () => { hitCount++; return true; } };
const cond = list.getDebugCondition();
assert.ok(cond !== null);
assert.ok(cond());
assert.strictEqual(hitCount, 1);
});
it('should evaluate multiple breakpoints with OR logic', function () {
const list = new BreakpointList();
let hits1 = 0, hits2 = 0;
list.id2bp['bp1'] = { cond: () => { hits1++; return false; } };
list.id2bp['bp2'] = { cond: () => { hits2++; return true; } };
const cond = list.getDebugCondition();
const result = cond();
assert.ok(result);
assert.strictEqual(hits1, 1);
assert.strictEqual(hits2, 1);
});
it('should remove breakpoint', function () {
const list = new BreakpointList();
list.id2bp['bp1'] = { cond: () => true };
delete list.id2bp['bp1'];
const cond = list.getDebugCondition();
assert.strictEqual(cond, null);
});
});
describe('Tool Filename Detection', function () {
describe('6502 tools', function () {
it('should detect C language', function () {
assert.strictEqual(getToolForFilename_6502('test.c'), 'cc65');
assert.strictEqual(getToolForFilename_6502('program.c'), 'cc65');
});
it('should detect assembly language', function () {
assert.strictEqual(getToolForFilename_6502('test.s'), 'ca65');
assert.strictEqual(getToolForFilename_6502('code.ca65'), 'ca65');
assert.strictEqual(getToolForFilename_6502('test.dasm'), 'dasm');
});
it('should detect various assembler formats', function () {
assert.strictEqual(getToolForFilename_6502('test.acme'), 'acme');
assert.strictEqual(getToolForFilename_6502('test.xa'), 'xa');
assert.strictEqual(getToolForFilename_6502('test.wiz'), 'wiz');
});
it('should detect special formats', function () {
assert.strictEqual(getToolForFilename_6502('test.ecs'), 'ecs');
assert.strictEqual(getToolForFilename_6502('test.cpp'), 'oscar64');
assert.strictEqual(getToolForFilename_6502('test.cc'), 'oscar64');
assert.strictEqual(getToolForFilename_6502('test.o64'), 'oscar64');
});
it('should default to dasm for unknown extensions', function () {
assert.strictEqual(getToolForFilename_6502('test.a'), 'dasm');
assert.strictEqual(getToolForFilename_6502('test.asm'), 'dasm');
assert.strictEqual(getToolForFilename_6502('unknown'), 'dasm');
});
it('should detect llvm-mos for special C files', function () {
assert.strictEqual(getToolForFilename_6502('test-llvm.c'), 'remote:llvm-mos');
});
});
describe('Z80 tools', function () {
it('should detect C language', function () {
assert.strictEqual(getToolForFilename_z80('test.c'), 'sdcc');
assert.strictEqual(getToolForFilename_z80('code.h'), 'sdcc');
});
it('should detect assembly language', function () {
assert.strictEqual(getToolForFilename_z80('test.s'), 'sdasz80');
assert.strictEqual(getToolForFilename_z80('test.sgb'), 'sdasgb');
assert.strictEqual(getToolForFilename_z80('test.ns'), 'naken');
});
it('should detect special formats', function () {
assert.strictEqual(getToolForFilename_z80('test.scc'), 'sccz80');
assert.strictEqual(getToolForFilename_z80('test.z'), 'zmac');
assert.strictEqual(getToolForFilename_z80('test.wiz'), 'wiz');
});
it('should default to zmac for unknown extensions', function () {
assert.strictEqual(getToolForFilename_z80('unknown'), 'zmac');
});
});
describe('6809 tools', function () {
it('should detect C language', function () {
assert.strictEqual(getToolForFilename_6809('test.c'), 'cmoc');
assert.strictEqual(getToolForFilename_6809('code.h'), 'cmoc');
});
it('should detect assembly language', function () {
assert.strictEqual(getToolForFilename_6809('test.xasm'), 'xasm6809');
assert.strictEqual(getToolForFilename_6809('test.lwasm'), 'lwasm');
});
it('should default to cmoc', function () {
assert.strictEqual(getToolForFilename_6809('unknown'), 'cmoc');
});
});
});
describe('Opcode Metadata', function () {
it('should return valid metadata for 6502 opcodes', function () {
// Test NOP (0xEA)
const nop = getOpcodeMetadata_6502(0xEA, 0);
assert.strictEqual(nop.opcode, 0xEA);
assert.ok(nop.minCycles > 0);
assert.ok(nop.maxCycles >= nop.minCycles);
assert.ok(nop.insnlength > 0);
});
it('should return consistent metadata for same opcode', function () {
const meta1 = getOpcodeMetadata_6502(0x69, 0x1000); // ADC immediate
const meta2 = getOpcodeMetadata_6502(0x69, 0x2000);
assert.strictEqual(meta1.opcode, meta2.opcode);
assert.strictEqual(meta1.minCycles, meta2.minCycles);
assert.strictEqual(meta1.maxCycles, meta2.maxCycles);
assert.strictEqual(meta1.insnlength, meta2.insnlength);
});
it('should handle all 256 6502 opcodes', function () {
for (let op = 0; op < 256; op++) {
const meta = getOpcodeMetadata_6502(op, 0);
assert.ok(meta.minCycles >= 0);
assert.ok(meta.maxCycles >= meta.minCycles);
assert.ok(meta.insnlength >= 0);
}
});
});
describe('CPU State Formatting', function () {
describe('6502 CPU state', function () {
it('should format basic CPU state', function () {
const state: any = {
PC: 0x1234,
SP: 0xFF,
A: 0x42,
X: 0x10,
Y: 0x20,
N: false, V: false, D: false, Z: false, C: false, I: false, R: true
};
const output = cpuStateToLongString_6502(state);
assert.ok(output.includes('1234')); // PC
assert.ok(output.includes('FF')); // SP
});
it('should show flag status', function () {
const state1: any = {
PC: 0,
SP: 0,
N: true, V: false, D: false, Z: true, C: false, I: false, R: true
};
const output = cpuStateToLongString_6502(state1);
assert.ok(output.includes('N'));
assert.ok(output.includes('Z'));
});
it('should show BUSY status when R is false', function () {
const state: any = { PC: 0, SP: 0, R: false };
const output = cpuStateToLongString_6502(state);
assert.ok(output.includes('BUSY'));
});
});
describe('Z80 CPU state', function () {
it('should format Z80 state', function () {
const state: any = {
PC: 0x4000,
SP: 0x9000,
AF: 0x1234,
BC: 0x5678,
DE: 0x9ABC,
HL: 0xDEF0,
IX: 0x0001,
IY: 0x0002,
IR: 0x3344,
iff1: true,
iff2: false
};
const output = cpuStateToLongString_Z80(state);
assert.ok(output.includes('4000')); // PC
assert.ok(output.includes('9000')); // SP
assert.ok(output.includes('1234')); // AF
});
});
describe('6809 CPU state', function () {
it('should format 6809 state', function () {
const state: any = {
PC: 0x1000,
SP: 0x2000,
DP: 0x00,
A: 0x11,
B: 0x22,
X: 0x3333,
Y: 0x4444,
U: 0x5555,
CC: 0xF0
};
const output = cpuStateToLongString_6809(state);
assert.ok(output.includes('1000')); // PC
assert.ok(output.includes('2000')); // SP
assert.ok(output.includes('11')); // A
});
});
});
describe('Symbol lookup', function () {
it('should find symbol at address', function () {
const platform: any = {
debugSymbols: new DebugSymbols({ main: 0x1000, loop: 0x1010 }, {}),
readAddress: (addr: number) => addr & 0xFF
};
const result = lookupSymbol(platform, 0x1010, false);
assert.ok(result.includes('loop'));
});
it('should find symbol before address when extra is true', function () {
const platform: any = {
debugSymbols: new DebugSymbols({ main: 0x1000 }, {}),
readAddress: (addr: number) => 0
};
const result = lookupSymbol(platform, 0x1005, true);
assert.ok(result.includes('main'));
assert.ok(result.includes('$05'));
});
it('should return empty string when no symbol found', function () {
const platform: any = {
debugSymbols: new DebugSymbols({}, {}),
readAddress: (addr: number) => 0
};
const result = lookupSymbol(platform, 0x5000, false);
assert.strictEqual(result, '');
});
it('should handle missing debug symbols', function () {
const platform: any = { debugSymbols: null };
const result = lookupSymbol(platform, 0x1000, false);
assert.strictEqual(result, '');
});
});
describe('Stack dumping', function () {
it('should dump stack memory', function () {
const mem = new Uint8Array([
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x20, 0x10, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D,
]);
const platform: any = {
debugSymbols: new DebugSymbols({ func: 0x100E }, {}),
readAddress: (addr: number) => addr < mem.length ? mem[addr] : 0
};
const result = dumpStackToString(platform, mem, 0, 15, 9, 0x20);
assert.ok(result.includes('$'));
assert.ok(result.length > 0);
});
it('should handle big endian byte order', function () {
const mem = new Uint8Array([0x10, 0x20, 0x30, 0x40]);
const platform: any = {
debugSymbols: new DebugSymbols({}, {}),
readAddress: (addr: number) => mem[addr]
};
const result = dumpStackToString(platform, mem, 0, 3, 2, 0, true);
assert.ok(result.includes('$'));
});
it('should show JSR addresses', function () {
const mem = new Uint8Array(256);
// Set up JSR at 0x100 (6502 JSR is 0x20)
mem[0x100] = 0x20;
// Stack has return address pointing near 0x100
mem[0x50] = 0xFE;
mem[0x51] = 0x01;
const platform: any = {
debugSymbols: new DebugSymbols({ func: 0x0100 }, {}),
readAddress: (addr: number) => {
if (addr < mem.length) return mem[addr];
return 0;
}
};
const result = dumpStackToString(platform, mem, 0, 255, 0x50, 0x20);
// The function returns a string with stack content
assert.ok(result.includes('$'));
});
});
describe('BasePlatform', function () {
class TestPlatform extends BasePlatform {
loadState(state: EmuState): void { }
saveState(): EmuState { return {}; }
pause(): void { }
resume(): void { }
advance(novideo?: boolean): number { return 0; }
}
it('should initialize with empty internal files', function () {
const platform = new TestPlatform();
assert.deepStrictEqual(platform.internalFiles, {});
});
it('should set and get recorder', function () {
const platform = new TestPlatform();
const recorder: any = { frameRequested: () => false };
platform.setRecorder(recorder);
assert.strictEqual(platform.recorder, recorder);
});
it('should read and write files', function () {
const platform = new TestPlatform();
const data: any = { data: [1, 2, 3] };
platform.writeFile('test.txt', data);
const read = platform.readFile('test.txt');
assert.deepStrictEqual(read, data);
});
it('should return undefined for non-existent file', function () {
const platform = new TestPlatform();
const read = platform.readFile('nonexistent.txt');
assert.strictEqual(read, undefined);
});
it('should generate debug tree', function () {
const platform = new TestPlatform();
platform.debugSymbols = new DebugSymbols({ test: 0x1000 }, { info: 'test' });
const tree: any = platform.getDebugTree();
assert.ok(tree.state);
assert.deepStrictEqual(tree.debuginfo, { info: 'test' });
});
});
describe('BaseDebugPlatform', function () {
class TestDebugPlatform extends BaseDebugPlatform {
loadState(state: EmuState): void { }
saveState(): EmuState { return { c: { PC: 0 } }; }
pause(): void { }
resume(): void { }
advance(novideo?: boolean): number { return 0; }
getCPUState(): CpuState { return { PC: 0x1000, SP: 0x100 }; }
getSP(): number { return 0x100; }
getPC(): number { return 0x1000; }
isStable(): boolean { return true; }
}
it('should manage breakpoints', function () {
const platform = new TestDebugPlatform();
const cond = () => true;
platform.setBreakpoint('bp1', cond);
assert.ok(platform.hasBreakpoint('bp1'));
platform.clearBreakpoint('bp1');
assert.ok(!platform.hasBreakpoint('bp1'));
});
it('should clear all debug state', function () {
const platform = new TestDebugPlatform();
platform.debugSavedState = { c: { PC: 0 } };
platform.debugBreakState = { c: { PC: 100 } };
platform.onBreakpointHit = () => { };
platform.clearDebug();
assert.strictEqual(platform.debugSavedState, null);
assert.strictEqual(platform.debugBreakState, null);
assert.strictEqual(platform.onBreakpointHit, null);
});
it('should track frame count', function () {
const platform = new TestDebugPlatform();
assert.strictEqual(platform.frameCount, 0);
platform.postFrame();
assert.strictEqual(platform.frameCount, 1);
platform.postFrame();
assert.strictEqual(platform.frameCount, 2);
});
it('should detect breakpoint hits', function () {
const platform = new TestDebugPlatform();
assert.ok(!platform.wasBreakpointHit());
platform.debugBreakState = { c: { PC: 0 } };
assert.ok(platform.wasBreakpointHit());
});
});
describe('Base6502Platform', function () {
class TestNES6502 extends Base6502Platform {
loadState(state: EmuState): void { }
saveState(): EmuState { return { c: { PC: 0, SP: 0 } }; }
pause(): void { }
resume(): void { }
advance(novideo?: boolean): number { return 0; }
getCPUState(): CpuState { return { PC: 0x8000, SP: 0xFF }; }
getSP(): number { return 0xFF; }
getPC(): number { return 0x8000; }
isStable(): boolean { return true; }
readAddress(addr: number): number { return 0; }
}
it('should get default extensions', function () {
const platform = new TestNES6502();
const exts = platform.getDefaultExtensions();
assert.ok(exts.includes('.c'));
assert.ok(exts.includes('.ca65'));
assert.ok(exts.includes('.dasm'));
});
it('should get 6502 opcode metadata', function () {
const platform = new TestNES6502();
const meta = platform.getOpcodeMetadata(0x69, 0);
assert.ok(meta.minCycles > 0);
});
it('should get origin PC from reset vector', function () {
const platform = new TestNES6502();
const originPC = platform.getOriginPC();
assert.ok(typeof originPC === 'number');
});
it('should apply PC delta correction', function () {
const platform = new TestNES6502();
const state: CpuState = { PC: 0x1000 };
const fixed = platform.fixPC(state);
assert.strictEqual(fixed.PC, 0x0FFF); // PC - 1
const unfixed = platform.unfixPC(fixed);
assert.strictEqual(unfixed.PC, 0x1000);
});
it('should handle PC wrapping at 16-bit boundary', function () {
const platform = new TestNES6502();
const state: CpuState = { PC: 0x0000 };
const fixed = platform.fixPC(state);
assert.strictEqual(fixed.PC, 0xFFFF);
});
it('should get debug categories', function () {
const platform = new TestNES6502();
const cats = platform.getDebugCategories();
assert.ok(cats.includes('CPU'));
assert.ok(cats.includes('Stack'));
});
});
describe('inspectSymbol', function () {
it('should inspect symbol value at address', function () {
const platform: any = {
debugSymbols: new DebugSymbols({ myvar: 0x100 }, {}),
readAddress: (addr: number) => addr === 0x100 ? 0x42 : 0x00
};
const result = inspectSymbol(platform, 'myvar');
assert.ok(result.includes('100'));
assert.ok(result.includes('42'));
});
it('should handle two-byte values', function () {
const platform: any = {
debugSymbols: new DebugSymbols({ word: 0x200 }, {}),
readAddress: (addr: number) => {
if (addr === 0x200) return 0x34;
if (addr === 0x201) return 0x12;
return 0;
}
};
const result = inspectSymbol(platform, 'word');
assert.ok(result.includes('200'));
});
it('should look for C and asm symbol variants', function () {
const platform: any = {
debugSymbols: new DebugSymbols({ _myvar: 0x300 }, {}),
readAddress: (addr: number) => addr === 0x300 ? 0x99 : 0
};
const result = inspectSymbol(platform, 'myvar');
assert.ok(result.includes('99'));
});
it('should return undefined for missing symbol', function () {
const platform: any = {
debugSymbols: new DebugSymbols({}, {}),
readAddress: (addr: number) => 0
};
const result = inspectSymbol(platform, 'nonexistent');
assert.ok(result === undefined || result === null);
});
it('should handle missing debug symbols', function () {
const platform: any = { debugSymbols: null };
const result = inspectSymbol(platform, 'symbol');
assert.strictEqual(result, undefined);
});
it('should handle missing readAddress method', function () {
const platform: any = {
debugSymbols: new DebugSymbols({ test: 0x100 }, {}),
readAddress: undefined
};
const result = inspectSymbol(platform, 'test');
assert.ok(result === null);
});
});
+256
View File
@@ -0,0 +1,256 @@
import assert from "assert";
import { describe, it } from "mocha";
import {
lpad, rpad, byte2signed, getFilenameForPath, getFolderForPath,
getFilenamePrefix, hex, tobin, toradix, arrayCompare, invertMap,
stringToByteArray, byteArrayToString, byteArrayToUTF8, removeBOM,
isProbablyBinary, clamp, safeident, rgb2bgr, RGBA, findIntegerFactors,
replaceAll, highlightDifferences, decodeQueryString, parseBool,
safe_extend, printFlags, byteToASCII, coerceToArray
} from "../../src/common/util";
describe('String Padding Functions', function () {
it('lpad should pad strings on the left', function () {
assert.strictEqual(lpad('5', 3), ' 5');
assert.strictEqual(lpad('hello', 8), ' hello');
assert.strictEqual(lpad('x', 1), 'x');
assert.strictEqual(lpad('123', 5), ' 123');
});
it('rpad should pad strings on the right', function () {
assert.strictEqual(rpad('5', 3), '5 ');
assert.strictEqual(rpad('hello', 8), 'hello ');
assert.strictEqual(rpad('x', 1), 'x');
assert.strictEqual(rpad('123', 5), '123 ');
});
});
describe('Number Conversion Functions', function () {
it('hex should format numbers in hexadecimal', function () {
assert.strictEqual(hex(255), 'FF');
assert.strictEqual(hex(0), '00');
assert.strictEqual(hex(255, 4), '00FF');
assert.strictEqual(hex(65535, 4), 'FFFF');
assert.strictEqual(hex(0x12345678, 8), '12345678');
assert.strictEqual(hex(15, 2), '0F');
});
it('tobin should format numbers in binary', function () {
assert.strictEqual(tobin(0), '00000000');
assert.strictEqual(tobin(255), '11111111');
assert.strictEqual(tobin(5), '00000101');
assert.strictEqual(tobin(15, 4), '1111');
assert.strictEqual(tobin(128), '10000000');
});
it('toradix should convert to various bases', function () {
assert.strictEqual(toradix(10, 4, 16), '000A');
assert.strictEqual(toradix(255, 8, 16), '000000FF');
assert.strictEqual(toradix(7, 3, 2), '111');
assert.strictEqual(toradix(100, 4, 10), '0100');
});
it('byte2signed should convert unsigned to signed bytes', function () {
assert.strictEqual(byte2signed(0), 0);
assert.strictEqual(byte2signed(127), 127);
assert.strictEqual(byte2signed(128), -128);
assert.strictEqual(byte2signed(255), -1);
assert.strictEqual(byte2signed(200), -56);
assert.strictEqual(byte2signed(0x80), -128);
});
});
describe('File Path Functions', function () {
it('getFilenameForPath should extract filename from path', function () {
assert.strictEqual(getFilenameForPath('path/to/file.txt'), 'file.txt');
assert.strictEqual(getFilenameForPath('file.txt'), 'file.txt');
assert.strictEqual(getFilenameForPath('/a/b/c/d.js'), 'd.js');
assert.strictEqual(getFilenameForPath('a/b'), 'b');
});
it('getFolderForPath should extract folder from path', function () {
assert.strictEqual(getFolderForPath('path/to/file.txt'), 'path/to');
assert.strictEqual(getFolderForPath('file.txt'), '');
assert.strictEqual(getFolderForPath('/a/b/c/d.js'), '/a/b/c');
assert.strictEqual(getFolderForPath('a/b/c'), 'a/b');
});
it('getFilenamePrefix should extract name without extension', function () {
assert.strictEqual(getFilenamePrefix('file.txt'), 'file');
assert.strictEqual(getFilenamePrefix('archive.tar.gz'), 'archive.tar');
assert.strictEqual(getFilenamePrefix('noext'), 'noext');
assert.strictEqual(getFilenamePrefix('.hidden'), '.hidden');
});
});
describe('Array/Collection Functions', function () {
it('arrayCompare should compare arrays', function () {
assert.ok(arrayCompare([1, 2, 3], [1, 2, 3]));
assert.ok(!arrayCompare([1, 2, 3], [1, 2, 4]));
assert.ok(!arrayCompare([1, 2], [1, 2, 3]));
assert.ok(arrayCompare([], []));
assert.ok(!arrayCompare([1], []));
assert.ok(arrayCompare(null, null));
assert.ok(!arrayCompare(null, [1]));
assert.ok(!arrayCompare([1], null));
const arr1 = new Uint8Array([1, 2, 3]);
const arr2 = new Uint8Array([1, 2, 3]);
assert.ok(arrayCompare(arr1, arr2));
});
it('invertMap should invert key-value pairs', function () {
const map = { a: '1', b: '2', c: '3' };
const inv = invertMap(map);
assert.strictEqual(inv['1'], 'a');
assert.strictEqual(inv['2'], 'b');
assert.strictEqual(inv['3'], 'c');
});
it('coerceToArray should convert various types to arrays', function () {
assert.deepStrictEqual(coerceToArray([1, 2, 3]), [1, 2, 3]);
assert.deepStrictEqual(coerceToArray({ a: 1, b: 2 }), [1, 2]);
});
});
describe('Byte Array Conversion Functions', function () {
it('stringToByteArray should convert string to bytes', function () {
const bytes = stringToByteArray('hello');
assert.strictEqual(bytes.length, 5);
assert.strictEqual(bytes[0], 104); // 'h'
assert.strictEqual(bytes[4], 111); // 'o'
});
it('byteArrayToString should convert bytes to string', function () {
const bytes = [72, 101, 108, 108, 111]; // "Hello"
assert.strictEqual(byteArrayToString(bytes), 'Hello');
assert.strictEqual(byteArrayToString(new Uint8Array([65, 66, 67])), 'ABC');
});
it('byteArrayToUTF8 should decode UTF-8', function () {
const ascii = byteArrayToUTF8([72, 101, 108, 108, 111]);
assert.strictEqual(ascii, 'Hello');
});
it('removeBOM should strip BOM if present', function () {
assert.strictEqual(removeBOM('hello'), 'hello');
assert.strictEqual(removeBOM('hello'), 'hello');
});
});
describe('Binary Detection', function () {
it('isProbablyBinary should detect binary data', function () {
assert.ok(!isProbablyBinary('test.txt', [104, 101, 108, 108, 111])); // "hello"
assert.ok(isProbablyBinary('test.bin', [1, 2, 3]));
assert.ok(isProbablyBinary('test.chr', [255, 255]));
assert.ok(isProbablyBinary('test.dat', [0]));
assert.ok(!isProbablyBinary('test.c', [47, 47, 105, 102])); // "//if"
assert.ok(isProbablyBinary(null, [0, 0, 0, 0])); // null bytes
});
});
describe('Color Functions', function () {
it('rgb2bgr should swap red and blue channels', function () {
assert.strictEqual(rgb2bgr(0xFF0000), 0x0000FF);
assert.strictEqual(rgb2bgr(0x00FF00), 0x00FF00);
assert.strictEqual(rgb2bgr(0x0000FF), 0xFF0000);
assert.strictEqual(rgb2bgr(0xFFFFFF), 0xFFFFFF);
});
it('RGBA should create color value', function () {
const white = RGBA(255, 255, 255);
assert.strictEqual(white & 0xFFFFFF, 0xFFFFFF);
const black = RGBA(0, 0, 0);
assert.strictEqual(black & 0xFFFFFF, 0x000000);
});
it('clamp should constrain values', function () {
assert.strictEqual(clamp(0, 10, 5), 5);
assert.strictEqual(clamp(0, 10, -5), 0);
assert.strictEqual(clamp(0, 10, 15), 10);
assert.strictEqual(clamp(1, 1, 5), 1);
});
});
describe('String Manipulation', function () {
it('safeident should create valid identifiers', function () {
assert.strictEqual(safeident('valid_name'), 'valid_name');
assert.strictEqual(safeident('123abc'), '_123abc');
assert.strictEqual(safeident('-name'), '__name');
assert.strictEqual(safeident('name-with-dashes'), 'name_with_dashes');
assert.strictEqual(safeident('name@#$%'), 'name_');
});
it('replaceAll should replace all occurrences', function () {
assert.strictEqual(replaceAll('aaa', 'a', 'b'), 'bbb');
assert.strictEqual(replaceAll('hello world', ' ', '-'), 'hello-world');
assert.strictEqual(replaceAll('test', 'x', 'y'), 'test');
assert.strictEqual(replaceAll('', 'a', 'b'), '');
});
it('byteToASCII should convert bytes to readable chars', function () {
assert.strictEqual(byteToASCII(65), 'A');
assert.strictEqual(byteToASCII(32), ' ');
assert.strictEqual(byteToASCII(0), '␀'); // NULL control char
});
});
describe('Query String Functions', function () {
it('decodeQueryString should parse query parameters', function () {
const qs1 = decodeQueryString('?key=value&foo=bar');
assert.strictEqual(qs1['key'], 'value');
assert.strictEqual(qs1['foo'], 'bar');
const qs2 = decodeQueryString('a=1&b=2');
assert.strictEqual(qs2['a'], '1');
assert.strictEqual(qs2['b'], '2');
const qs3 = decodeQueryString('key=value+with+spaces');
assert.ok(qs3['key'].includes(' '));
});
it('parseBool should parse boolean strings', function () {
assert.ok(parseBool('true'));
assert.ok(parseBool('1'));
assert.ok(parseBool('yes'));
assert.ok(!parseBool('false'));
assert.ok(!parseBool('0'));
assert.ok(!parseBool(''));
});
});
describe('Complex Utility Functions', function () {
it('findIntegerFactors should find factor pairs', function () {
const f1 = findIntegerFactors(64, 1, 1, 1);
assert.ok(f1.a >= f1.b);
assert.strictEqual(f1.a * f1.b >= 64, true);
const f2 = findIntegerFactors(100, 5, 5, 1);
assert.ok(f2.a >= 5);
assert.ok(f2.b >= 5);
});
it('safe_extend should copy primitive values and arrays', function () {
const dest = { x: 1 };
const src = { y: 2, arr: [1, 2, 3] };
safe_extend(false, dest, src);
assert.strictEqual(dest['x'], 1);
assert.strictEqual(dest['y'], 2);
assert.deepStrictEqual(dest['arr'], [1, 2, 3]);
assert.ok(dest['arr'] !== src['arr']); // should be copy
});
it('printFlags should format flag values', function () {
const names = ['CARRY', 'ZERO', 'IRQ_DISABLE', 'DECIMAL'];
const result = printFlags(0x01, names, false);
assert.ok(result.includes('CARRY'));
const result2 = printFlags(0x00, names, false);
assert.ok(result2.includes('-'));
});
it('highlightDifferences should mark changes in text', function () {
const diff = highlightDifferences('hello world', 'hello there');
assert.ok(diff.includes('hilite'));
});
});
+361
View File
@@ -0,0 +1,361 @@
import assert from "assert";
import { describe, it } from "mocha";
import {
rgbToPixel, DMG_PALETTE, DMG_PALETTE_RGB, DMG_PALETTE_RGB24, dmgShadeFromRgb
} from "../../src/common/gbpalette";
import { SourceFile } from "../../src/common/workertypes";
import { ProbeRecorder, ProbeFlags } from "../../src/common/probe";
describe('Game Boy Palette Functions', function () {
it('rgbToPixel should convert RGB to packed pixel format', function () {
const white = rgbToPixel(255, 255, 255);
assert.strictEqual(white & 0xFFFFFF, 0xFFFFFF);
assert.strictEqual((white >> 24) & 0xFF, 0xFF); // alpha
const black = rgbToPixel(0, 0, 0);
assert.strictEqual(black & 0xFFFFFF, 0x000000);
const red = rgbToPixel(255, 0, 0);
assert.strictEqual(red & 0xFF, 255); // red channel
assert.strictEqual((red >> 8) & 0xFF, 0); // green channel
assert.strictEqual((red >> 16) & 0xFF, 0); // blue channel
});
it('DMG_PALETTE should contain 4 shades', function () {
assert.strictEqual(DMG_PALETTE.length, 4);
// Should be in light to dark order
assert.ok(DMG_PALETTE[0] > DMG_PALETTE[3]); // lightest > darkest numerically
});
it('DMG_PALETTE_RGB should match DMG_PALETTE', function () {
assert.strictEqual(DMG_PALETTE_RGB.length, 4);
for (let i = 0; i < 4; i++) {
const [r, g, b] = DMG_PALETTE_RGB[i];
const expected = rgbToPixel(r, g, b);
assert.strictEqual(DMG_PALETTE[i], expected);
}
});
it('DMG_PALETTE_RGB24 should be RGB values without alpha', function () {
assert.strictEqual(DMG_PALETTE_RGB24.length, 4);
assert.strictEqual(DMG_PALETTE_RGB24[0], 0xd0d884); // lightest
assert.strictEqual(DMG_PALETTE_RGB24[3], 0x2d3122); // darkest
});
it('dmgShadeFromRgb should map greyscale to shade index', function () {
// Very light colors should map to shade 0
assert.strictEqual(dmgShadeFromRgb(255, 255, 255), 0);
assert.strictEqual(dmgShadeFromRgb(200, 200, 200), 0);
// Medium-light should be shade 1
assert.strictEqual(dmgShadeFromRgb(128, 128, 128), 1);
// Medium-dark should be shade 2
assert.strictEqual(dmgShadeFromRgb(80, 80, 80), 2);
// Very dark should be shade 3
assert.strictEqual(dmgShadeFromRgb(0, 0, 0), 3);
assert.strictEqual(dmgShadeFromRgb(30, 30, 30), 3);
});
it('dmgShadeFromRgb should work with non-greyscale colors', function () {
// Red (255,0,0) avg=85 → 2
assert.strictEqual(dmgShadeFromRgb(255, 0, 0), 2);
// Red (128,0,0) avg≈43 → 3
assert.strictEqual(dmgShadeFromRgb(128, 0, 0), 3);
// Green (0,255,0) avg=85 → 2
assert.strictEqual(dmgShadeFromRgb(0, 255, 0), 2);
// Green (0,128,0) avg≈43 → 3
assert.strictEqual(dmgShadeFromRgb(0, 128, 0), 3);
// Blue (0,0,255) avg=85 → 2
assert.strictEqual(dmgShadeFromRgb(0, 0, 255), 2);
// Blue (0,0,128) avg≈43 → 3
assert.strictEqual(dmgShadeFromRgb(0, 0, 128), 3);
});
it('dmgShadeFromRgb thresholds should be accurate', function () {
// Test boundary at 192
assert.strictEqual(dmgShadeFromRgb(192, 192, 192), 0);
assert.strictEqual(dmgShadeFromRgb(191, 191, 191), 1);
// Test boundary at 128
assert.strictEqual(dmgShadeFromRgb(128, 128, 128), 1);
assert.strictEqual(dmgShadeFromRgb(127, 127, 127), 2);
// Test boundary at 64
assert.strictEqual(dmgShadeFromRgb(64, 64, 64), 2);
assert.strictEqual(dmgShadeFromRgb(63, 63, 63), 3);
});
});
describe('SourceFile Class', function () {
it('should create SourceFile with lines and text', function () {
const lines = [
{ line: 1, offset: 0, insns: 'LDA #$00' },
{ line: 2, offset: 2, insns: 'STA $80' },
{ line: 3, offset: 4, insns: 'RTS' },
];
const text = 'LDA #$00\nSTA $80\nRTS\n';
const sf = new SourceFile(lines, text);
assert.strictEqual(sf.lines.length, 3);
assert.strictEqual(sf.text, text);
});
it('should build offset to line mapping', function () {
const lines = [
{ line: 1, offset: 0, insns: 'LDA #$00' },
{ line: 2, offset: 2, insns: 'STA $80' },
{ line: 3, offset: 4, insns: 'RTS' },
];
const sf = new SourceFile(lines, '');
assert.ok(sf.offset2loc[0]);
assert.ok(sf.offset2loc[2]);
assert.ok(sf.offset2loc[4]);
});
it('should build line to offset mapping', function () {
const lines = [
{ line: 1, offset: 0 },
{ line: 2, offset: 2 },
{ line: 3, offset: 4 },
];
const sf = new SourceFile(lines, '');
assert.strictEqual(sf.line2offset[1], 0);
assert.strictEqual(sf.line2offset[2], 2);
assert.strictEqual(sf.line2offset[3], 4);
});
it('should handle negative offsets gracefully', function () {
const lines = [
{ line: 1, offset: -1 },
{ line: 2, offset: 0 },
{ line: 3, offset: 2 },
];
const sf = new SourceFile(lines, '');
// Negative offsets should not be added to mapping
assert.ok(!sf.offset2loc[-1]);
assert.ok(sf.offset2loc[0]);
});
it('findLineForOffset should locate source line by address', function () {
const lines = [
{ line: 1, offset: 0, insns: 'LDA #$00' },
{ line: 2, offset: 2, insns: 'STA $80' },
{ line: 3, offset: 4, insns: 'RTS' },
];
const sf = new SourceFile(lines, '');
const line0 = sf.findLineForOffset(0, 0);
assert.strictEqual(line0.line, 1);
assert.strictEqual(line0.offset, 0);
const line2 = sf.findLineForOffset(2, 0);
assert.strictEqual(line2.line, 2);
assert.strictEqual(line2.offset, 2);
});
it('findLineForOffset should look behind for nearest line', function () {
const lines = [
{ line: 1, offset: 0, insns: 'LDA #$00' },
{ line: 2, offset: 2, insns: 'STA $80' },
{ line: 3, offset: 4, insns: 'RTS' },
];
const sf = new SourceFile(lines, '');
// Looking for offset 3 with lookbehind=1 should find offset 2
const line = sf.findLineForOffset(3, 1);
assert.strictEqual(line.line, 2);
assert.strictEqual(line.offset, 2);
});
it('findLineForOffset should return null if not found', function () {
const lines = [
{ line: 1, offset: 0 },
{ line: 2, offset: 2 },
];
const sf = new SourceFile(lines, '');
// Looking for offset 10 should not find anything
const line = sf.findLineForOffset(10, 0);
assert.strictEqual(line, null);
});
it('lineCount should return number of lines', function () {
const lines = [
{ line: 1, offset: 0 },
{ line: 2, offset: 2 },
{ line: 3, offset: 4 },
];
const sf = new SourceFile(lines, '');
assert.strictEqual(sf.lineCount(), 3);
});
it('should handle duplicate offsets (first wins)', function () {
const lines = [
{ line: 1, offset: 0, insns: 'LDA #$00' },
{ line: 2, offset: 0, insns: 'STA $80' }, // Same offset
{ line: 3, offset: 2, insns: 'RTS' },
];
const sf = new SourceFile(lines, '');
// First line at offset 0 should be returned
const line = sf.findLineForOffset(0, 0);
assert.strictEqual(line.line, 1);
});
});
describe('ProbeRecorder Class', function () {
class MockMachine {
probe: any;
connectProbe(probe) {
this.probe = probe;
}
}
it('should create ProbeRecorder with buffer', function () {
const machine = new MockMachine();
const pr = new ProbeRecorder(machine, 1024);
assert.ok(pr.buf);
assert.strictEqual(pr.buf.length, 1024);
assert.strictEqual(pr.idx, 0);
});
it('should log data to buffer', function () {
const machine = new MockMachine();
const pr = new ProbeRecorder(machine, 100);
pr.log(0x12000080); // MEM_READ at address 0x80
assert.strictEqual(pr.idx, 1);
assert.strictEqual(pr.buf[0], 0x12000080);
pr.log(0x13000081); // MEM_WRITE at address 0x81
assert.strictEqual(pr.idx, 2);
assert.strictEqual(pr.buf[1], 0x13000081);
});
it('should not exceed buffer length', function () {
const machine = new MockMachine();
const pr = new ProbeRecorder(machine, 2);
pr.log(0x12000080);
pr.log(0x13000081);
pr.log(0x14000082); // Should not write (buffer full)
assert.strictEqual(pr.idx, 2);
assert.strictEqual(pr.buf[0], 0x12000080);
assert.strictEqual(pr.buf[1], 0x13000081);
});
it('should retrieve last operation', function () {
const machine = new MockMachine();
const pr = new ProbeRecorder(machine, 100);
pr.log(0x12000080);
pr.log(0x13000081);
const lastOp = pr.lastOp();
assert.strictEqual(lastOp, 0x13000000); // MEM_WRITE flag
});
it('should retrieve last address', function () {
const machine = new MockMachine();
const pr = new ProbeRecorder(machine, 100);
pr.log(0x12000080);
pr.log(0x13AABBCC);
const lastAddr = pr.lastAddr();
assert.strictEqual(lastAddr, 0xAABBCC);
});
it('should return -1 for lastOp on empty buffer', function () {
const machine = new MockMachine();
const pr = new ProbeRecorder(machine, 100);
const lastOp = pr.lastOp();
assert.strictEqual(lastOp, -1);
});
it('should clear buffer', function () {
const machine = new MockMachine();
const pr = new ProbeRecorder(machine, 100);
pr.log(0x12000080);
pr.log(0x13000081);
assert.strictEqual(pr.idx, 2);
pr.clear();
assert.strictEqual(pr.idx, 0);
});
it('should reset buffer', function () {
const machine = new MockMachine();
const pr = new ProbeRecorder(machine, 100);
pr.log(0x12000080);
assert.strictEqual(pr.buf.length, 100);
pr.reset(200);
assert.strictEqual(pr.buf.length, 200);
assert.strictEqual(pr.idx, 0);
});
it('should relog last entry', function () {
const machine = new MockMachine();
const pr = new ProbeRecorder(machine, 100);
pr.log(0x12000080);
pr.relog(0x13000080);
assert.strictEqual(pr.idx, 1);
assert.strictEqual(pr.buf[0], 0x13000080);
});
it('should track scanline', function () {
const machine = new MockMachine();
const pr = new ProbeRecorder(machine, 100);
assert.strictEqual(pr.sl, 0);
pr.sl = 100;
assert.strictEqual(pr.sl, 100);
});
it('should track stack pointer', function () {
const machine = new MockMachine();
const pr = new ProbeRecorder(machine, 100);
assert.strictEqual(pr.cur_sp, -1);
pr.cur_sp = 0xFF;
assert.strictEqual(pr.cur_sp, 0xFF);
});
});
describe('ProbeFlags Enumeration', function () {
it('should have correct flag values', function () {
assert.strictEqual(ProbeFlags.CLOCKS, 0x00000000);
assert.strictEqual(ProbeFlags.EXECUTE, 0x01000000);
assert.strictEqual(ProbeFlags.INTERRUPT, 0x08000000);
assert.strictEqual(ProbeFlags.MEM_READ, 0x12000000);
assert.strictEqual(ProbeFlags.MEM_WRITE, 0x13000000);
assert.strictEqual(ProbeFlags.FRAME, 0x7f000000);
});
it('should extract operation from probe value', function () {
const probeValue = 0x12000080;
const opcode = probeValue & 0xFF000000;
assert.strictEqual(opcode, ProbeFlags.MEM_READ);
const address = probeValue & 0xFFFFFF;
assert.strictEqual(address, 0x80);
});
});
+522
View File
@@ -0,0 +1,522 @@
import assert from "assert";
import { describe, it } from "mocha";
import {
noise, getNoiseSeed, setNoiseSeed, KeyFlags, RAM, EmuHalt,
AnimationTimer, dumpRAM, Keys, makeKeycodeMap, newKeyboardHandler,
_setKeyboardEvents, padBytes, AddressDecoder, newAddressDecoder,
getMousePos, drawCrosshair, gtia_ntsc_to_rgb,
} from "../../src/common/emu";
describe('Noise PRNG', function () {
it('should produce a deterministic sequence from seed 1', function () {
setNoiseSeed(1);
const seq = [];
for (let i = 0; i < 8; i++) seq.push(noise());
assert.deepStrictEqual(seq, [33, 1, 34, 25, 250, 201, 8, 180]);
});
it('should track and restore the seed', function () {
setNoiseSeed(0x1234);
assert.strictEqual(getNoiseSeed(), 0x1234);
noise();
assert.notStrictEqual(getNoiseSeed(), 0x1234);
});
it('should return 0 forever for seed 0', function () {
setNoiseSeed(0);
assert.strictEqual(noise(), 0);
assert.strictEqual(noise(), 0);
assert.strictEqual(noise(), 0);
assert.strictEqual(getNoiseSeed(), 0);
});
it('should always return a byte', function () {
setNoiseSeed(1);
for (let i = 0; i < 100; i++) {
const n = noise();
assert.ok(n >= 0 && n <= 255);
}
});
});
describe('KeyFlags Enumeration', function () {
it('should have correct flag values', function () {
assert.strictEqual(KeyFlags.KeyDown, 1);
assert.strictEqual(KeyFlags.Shift, 2);
assert.strictEqual(KeyFlags.Ctrl, 4);
assert.strictEqual(KeyFlags.Alt, 8);
assert.strictEqual(KeyFlags.Meta, 16);
assert.strictEqual(KeyFlags.KeyUp, 64);
assert.strictEqual(KeyFlags.KeyPress, 128);
});
});
describe('RAM Class', function () {
it('should allocate zero-initialized memory', function () {
const ram = new RAM(16);
assert.ok(ram.mem instanceof Uint8Array);
assert.strictEqual(ram.mem.length, 16);
assert.strictEqual(ram.mem[0], 0);
assert.strictEqual(ram.mem[15], 0);
});
it('should allow reading and writing', function () {
const ram = new RAM(4);
ram.mem[2] = 0xAA;
assert.strictEqual(ram.mem[2], 0xAA);
});
});
describe('EmuHalt', function () {
it('should be an Error subclass with default properties', function () {
const e = new EmuHalt("Halt!");
assert.ok(e instanceof Error);
assert.ok(e instanceof EmuHalt);
assert.strictEqual(e.message, "Halt!");
assert.ok(e.squelchError);
assert.strictEqual(e.$loc, undefined);
});
it('should carry a source location', function () {
const e = new EmuHalt("bad opcode", { line: 42 } as any);
assert.deepStrictEqual(e.$loc, { line: 42 });
});
it('should be catchable via instanceof', function () {
let caught = null;
try {
throw new EmuHalt("boom");
} catch (err) {
caught = err;
}
assert.ok(caught instanceof EmuHalt);
assert.ok(caught instanceof Error);
assert.strictEqual(caught.message, "boom");
});
});
describe('dumpRAM', function () {
it('should format a Uint8Array dump', function () {
const ram = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);
assert.strictEqual(dumpRAM(ram, 0, 16),
"$00: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F\n");
});
it('should show the base offset in addresses', function () {
const ram = new Uint8Array([1, 2, 3]);
assert.strictEqual(dumpRAM(ram, 0x8000, 8),
"$8000: 01 02 03\n");
});
it('should format a Uint16Array dump', function () {
const ram = new Uint16Array([0x1234, 0x5678, 0x9ABC, 0xDEF0, 0x1111, 0x2222, 0x3333, 0x4444]);
assert.strictEqual(dumpRAM(ram, 0, 16),
"$00: 1234 5678 9ABC DEF0 1111 2222 3333 4444\n$08:\n");
});
it('should format a Float32Array dump with precision', function () {
const ram = new Float32Array([1.5, 2.25, 3.125, 0.5]);
assert.strictEqual(dumpRAM(ram, 0, 8),
"$00: 1.5000000 2.2500000 3.1250000 0.50000000\n$04:\n");
});
it('should handle plain arrays (BYTES_PER_ELEMENT fallback)', function () {
assert.strictEqual(dumpRAM([0x0A, 0x0B], 0, 2), "$00: 0A 0B\n");
});
});
describe('Keys Table', function () {
it('should define directional and action keys', function () {
assert.strictEqual(Keys.UP.c, 38);
assert.strictEqual(Keys.DOWN.c, 40);
assert.strictEqual(Keys.LEFT.c, 37);
assert.strictEqual(Keys.RIGHT.c, 39);
assert.strictEqual(Keys.A.c, 32); // Space
assert.strictEqual(Keys.B.c, 16); // Shift
assert.strictEqual(Keys.SELECT.c, 220);
assert.strictEqual(Keys.START.c, 13);
assert.strictEqual(Keys.ANYKEY.c, 0);
});
it('should annotate gamepad mapping metadata', function () {
assert.strictEqual(Keys.UP.plyr, 0);
assert.strictEqual(Keys.UP.button, 12);
assert.strictEqual(Keys.UP.yaxis, -1);
assert.strictEqual(Keys.RIGHT.xaxis, 1);
assert.strictEqual(Keys.A.button, 0);
assert.strictEqual(Keys.P2_UP.plyr, 1);
assert.strictEqual(Keys.P2_UP.c, 87); // W
});
it('should define function keys and misc keys', function () {
assert.strictEqual(Keys.VK_ESCAPE.c, 27);
assert.strictEqual(Keys.VK_F1.c, 112);
assert.strictEqual(Keys.VK_F12.c, 123);
assert.strictEqual(Keys.VK_ENTER.c, 13);
assert.strictEqual(Keys.VK_SPACE.c, 32);
});
it('should give every key a name and numeric code', function () {
const entries = Object.keys(Keys);
assert.ok(entries.length >= 100);
for (const name of entries) {
assert.ok(name.length > 0);
const def = Keys[name];
assert.strictEqual(typeof def.c, 'number');
assert.ok(def.n.length > 0, `key ${name} should have a name`);
}
});
});
describe('makeKeycodeMap', function () {
it('should build a map keyed by keycode', function () {
const map = makeKeycodeMap([[Keys.A, 0, 0x01], [Keys.B, 0, 0x02]]);
assert.ok(map instanceof Map);
assert.strictEqual(map[32].index, 0);
assert.strictEqual(map[32].mask, 0x01);
assert.strictEqual(map[32].def, Keys.A);
assert.strictEqual(map[16].index, 0);
assert.strictEqual(map[16].mask, 0x02);
assert.strictEqual(map[9999], undefined);
});
});
describe('newKeyboardHandler', function () {
it('should set switches on keydown and clear on keyup', function () {
const map = makeKeycodeMap([[Keys.A, 0, 0x01]]);
const switches = new Uint8Array(2);
const handler = newKeyboardHandler(switches, map);
handler(32, 0, KeyFlags.KeyDown); // A (Space)
assert.strictEqual(switches[0], 0x01);
handler(32, 0, KeyFlags.KeyDown); // idempotent
assert.strictEqual(switches[0], 0x01);
handler(32, 0, KeyFlags.KeyUp);
assert.strictEqual(switches[0], 0x00);
});
it('should support multiple keys and switch indices', function () {
const map = makeKeycodeMap([
[Keys.A, 0, 0x01],
[Keys.B, 0, 0x02],
[Keys.UP, 1, 0x80],
]);
const switches = new Uint8Array(2);
const handler = newKeyboardHandler(switches, map);
handler(32, 0, KeyFlags.KeyDown);
handler(16, 0, KeyFlags.KeyDown);
handler(38, 0, KeyFlags.KeyDown);
assert.strictEqual(switches[0], 0x03);
assert.strictEqual(switches[1], 0x80);
handler(32, 0, KeyFlags.KeyUp);
assert.strictEqual(switches[0], 0x02);
});
it('should support active-low (negative mask) switches', function () {
const map = makeKeycodeMap([[Keys.START, 0, -0x01]]);
const switches = new Uint8Array(1);
const handler = newKeyboardHandler(switches, map);
switches[0] = 0x01;
handler(13, 0, KeyFlags.KeyDown); // active-low press clears the bit
assert.strictEqual(switches[0], 0x00);
handler(13, 0, KeyFlags.KeyUp); // release sets the bit
assert.strictEqual(switches[0], 0x01);
});
it('should fall back to ANYKEY entry for unknown keys', function () {
const map = makeKeycodeMap([[Keys.ANYKEY, 0, 0x40]]);
const switches = new Uint8Array(1);
const handler = newKeyboardHandler(switches, map);
handler(9999, 0, KeyFlags.KeyDown);
assert.strictEqual(switches[0], 0x40);
handler(9999, 0, KeyFlags.KeyUp);
assert.strictEqual(switches[0], 0x00);
});
it('should ignore unmapped keys when no ANYKEY entry exists', function () {
const map = makeKeycodeMap([[Keys.A, 0, 0x01]]);
const switches = new Uint8Array(1);
const handler = newKeyboardHandler(switches, map);
handler(9999, 0, KeyFlags.KeyDown);
assert.strictEqual(switches[0], 0x00);
});
it('should invoke the callback function with key info', function () {
const map = makeKeycodeMap([[Keys.A, 0, 0x01]]);
const switches = new Uint8Array(1);
const calls = [];
const handler = newKeyboardHandler(switches, map, (o, key, code, flags) => {
calls.push([o && o.def.n, key, code, flags]);
});
handler(32, 65, KeyFlags.KeyDown | KeyFlags.Shift);
assert.deepStrictEqual(calls, [['Space', 32, 65, KeyFlags.KeyDown | KeyFlags.Shift]]);
});
it('should invoke callback for unmapped keys only with alwaysfunc', function () {
const map = makeKeycodeMap([[Keys.A, 0, 0x01]]);
const switches = new Uint8Array(1);
const calls = [];
const handler = newKeyboardHandler(switches, map, (o) => {
calls.push(o ? o.def.n : null);
}, true);
handler(9999, 0, KeyFlags.KeyDown);
assert.deepStrictEqual(calls, [null]);
assert.strictEqual(switches[0], 0x00);
});
it('should pass through to callback when no map is given', function () {
const switches = new Uint8Array(1);
const calls = [];
const handler = newKeyboardHandler(switches, null, (o, key, code, flags) => {
calls.push([o, key, code, flags]);
});
handler(65, 65, KeyFlags.KeyDown);
assert.deepStrictEqual(calls, [[null, 65, 65, KeyFlags.KeyDown]]);
});
});
describe('_setKeyboardEvents', function () {
it('should wire keydown and keyup handlers', function () {
const calls = [];
const canvas: any = { onkeydown: null, onkeyup: null };
_setKeyboardEvents(canvas, (which, keyCode, flags) => {
calls.push([which, keyCode, flags]);
});
canvas.onkeydown({ which: 65, keyCode: 65, shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, preventDefault() { } });
assert.deepStrictEqual(calls[0], [65, 65, KeyFlags.KeyDown]);
canvas.onkeyup({ which: 65, keyCode: 65, shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, preventDefault() { } });
assert.deepStrictEqual(calls[1], [65, 65, KeyFlags.KeyUp | KeyFlags.Shift]);
});
it('should pass through modifier flags', function () {
const calls = [];
const canvas: any = { onkeydown: null, onkeyup: null };
_setKeyboardEvents(canvas, (which, keyCode, flags) => {
calls.push(flags);
});
canvas.onkeydown({ which: 13, keyCode: 13, shiftKey: true, ctrlKey: true, altKey: true, metaKey: true, preventDefault() { } });
assert.strictEqual(calls[0], KeyFlags.KeyDown | KeyFlags.Shift | KeyFlags.Ctrl | KeyFlags.Alt | KeyFlags.Meta);
});
it('should preventDefault on unmodified keydown only', function () {
const prevented = [];
const canvas: any = { onkeydown: null, onkeyup: null };
_setKeyboardEvents(canvas, () => { });
canvas.onkeydown({ which: 32, keyCode: 32, shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, preventDefault() { prevented.push('plain'); } });
canvas.onkeydown({ which: 32, keyCode: 32, shiftKey: true, ctrlKey: false, altKey: false, metaKey: false, preventDefault() { prevented.push('shift'); } });
assert.deepStrictEqual(prevented, ['plain']);
});
});
describe('padBytes', function () {
it('should pad at the end by default', function () {
const out = padBytes([1, 2, 3], 8);
assert.ok(out instanceof Uint8Array);
assert.deepStrictEqual(Array.from(out), [1, 2, 3, 0, 0, 0, 0, 0]);
});
it('should pad at the start when requested', function () {
const out = padBytes(new Uint8Array([1, 2, 3]), 8, true);
assert.deepStrictEqual(Array.from(out), [0, 0, 0, 0, 0, 1, 2, 3]);
});
it('should return unchanged data when length matches', function () {
const out = padBytes([0xAA, 0xBB], 2);
assert.deepStrictEqual(Array.from(out), [0xAA, 0xBB]);
});
it('should throw when data is too long', function () {
assert.throws(() => padBytes([1, 2, 3], 2), /Data too long, 3 > 2/);
});
});
describe('AddressDecoder', function () {
it('should decode reads from a table', function () {
const ram = new Uint8Array(256);
ram[0x10] = 0x5A;
const decode = newAddressDecoder([[0x0000, 0x00FF, 0, (a) => ram[a]]]);
assert.strictEqual(decode(0x10), 0x5A);
assert.strictEqual(decode(0x99), 0x00);
});
it('should return default value for out-of-range addresses', function () {
const ram = new Uint8Array(256);
const decode = newAddressDecoder([[0x0000, 0x00FF, 0, (a) => ram[a]]]);
assert.strictEqual(decode(0x1000), 0);
});
it('should apply address masks', function () {
const ram = new Uint8Array(0x800);
ram[0x000] = 0x11;
ram[0x7FF] = 0x22;
const decode = newAddressDecoder([[0x0000, 0x0FFF, 0x07FF, (a) => ram[a]]]);
assert.strictEqual(decode(0x0000), 0x11);
assert.strictEqual(decode(0x0800), 0x11); // 0x0800 & 0x07FF = 0x0000
assert.strictEqual(decode(0x0FFF), 0x22); // 0x0FFF & 0x07FF = 0x07FF
});
it('should support write functions and mask the result', function () {
const ram = new Uint8Array(0x800);
const decode = newAddressDecoder([[0x0000, 0x0FFF, 0x07FF,
(a, v) => { if (v !== undefined) ram[a] = v; return ram[a]; }]]);
decode(0x0801, 0x99); // writes to 0x0801 & 0x07FF = 0x0001
assert.strictEqual(ram[0x001], 0x99);
assert.strictEqual(ram[0x801], undefined);
assert.strictEqual(decode(0x0801), 0x99);
});
it('should apply a global mask before decoding', function () {
const ram = new Uint8Array(0x100);
ram[0x00] = 0x11;
ram[0x20] = 0x22;
const decode = newAddressDecoder([[0x0000, 0x00FF, 0, (a) => ram[a]]],
{ gmask: 0x0FFF, defaultval: 0xFF });
assert.strictEqual(decode(0x1000), 0x11); // 0x1000 & 0x0FFF = 0x0000
assert.strictEqual(decode(0x0020), 0x22);
assert.strictEqual(decode(0x1234), 0xFF); // masked to 0x0234, out of range
});
it('should handle an empty table with a default value', function () {
const decode = newAddressDecoder([], { defaultval: 0xEE });
assert.strictEqual(decode(0x0000), 0xEE);
const decode2 = newAddressDecoder([]);
assert.strictEqual(decode2(0x0000), 0);
});
it('should be usable as a constructor directly', function () {
const decode = new (AddressDecoder as any)([[0x0000, 0xFFFF, 0, () => 0x42]]);
assert.strictEqual(decode(0x1234), 0x42);
});
});
describe('AnimationTimer', function () {
it('should invoke callback at scheduled intervals', function () {
let count = 0;
const timer = new AnimationTimer(100, () => count++); // 10ms per frame
timer.running = true;
timer.nextFrame(50); // ts > nextts(0)
assert.strictEqual(count, 1);
timer.nextFrame(60); // ts(60) < nextts(100)
assert.strictEqual(count, 2);
timer.nextFrame(150); // ts > nextts(100)
assert.strictEqual(count, 3); //
timer.running = false;
});
it('should not invoke callback when not running', function () {
let count = 0;
const timer = new AnimationTimer(100, () => count++);
timer.nextFrame(50);
assert.strictEqual(count, 0);
timer.running = true;
timer.nextFrame(60);
assert.strictEqual(count, 1);
timer.running = false;
});
it('should catch up after large gaps', function () {
let count = 0;
const timer = new AnimationTimer(100, () => count++);
timer.running = true;
timer.nextts = 500;
timer.nextFrame(3000); // > 1s gap: should catch up
assert.strictEqual(count, 1);
assert.strictEqual(timer.nextts, 3000);
timer.running = false;
});
it('should set frame timing properties', function () {
const timer = new AnimationTimer(60, () => { });
assert.strictEqual(timer.frameRate, 60);
assert.strictEqual(timer.intervalMsec, 1000 / 60);
});
it('should start/stop and report running state', function () {
const timer = new AnimationTimer(60, () => { });
assert.strictEqual(timer.isRunning(), false);
timer.start();
assert.strictEqual(timer.isRunning(), true);
timer.stop();
assert.strictEqual(timer.isRunning(), false);
});
});
describe('drawCrosshair', function () {
it('should no-op without setLineDash (unit testing support)', function () {
let called = false;
const ctx: any = { fillRect: () => { called = true; } };
drawCrosshair(ctx, 10, 20, 2);
assert.strictEqual(called, false);
});
it('should draw vertical and horizontal lines', function () {
const calls: any[][] = [];
const ctx: any = {
fillStyle: '', strokeStyle: '', lineWidth: 0,
setLineDash: (d) => calls.push(['setLineDash', String(d)]),
fillRect: (x, y, w, h) => calls.push(['fillRect', x, y, w, h]),
beginPath: () => calls.push(['beginPath']),
moveTo: (x, y) => calls.push(['moveTo', x, y]),
lineTo: (x, y) => calls.push(['lineTo', x, y]),
stroke: () => calls.push(['stroke']),
};
drawCrosshair(ctx, 10, 20, 2);
assert.ok(calls.some(c => c[0] === 'fillRect' && c[1] === 8), 'vertical bar at x-2');
assert.ok(calls.some(c => c[0] === 'fillRect' && c[2] === 18), 'horizontal bar at y-2');
assert.ok(calls.some(c => c[0] === 'moveTo' && c[1] === 10), 'vertical line at x');
assert.ok(calls.some(c => c[0] === 'moveTo' && c[2] === 20), 'horizontal line at y');
assert.ok(calls.some(c => c[0] === 'setLineDash' && c[1] === '4,4'), 'dash pattern 2*width');
assert.ok(calls.some(c => c[0] === 'stroke'), 'stroke called');
});
});
describe('getMousePos', function () {
const realWindow = (global as any).window;
after(function () {
if (realWindow === undefined) delete (global as any).window;
else (global as any).window = realWindow;
});
function stubWindow() {
(global as any).window = {
getComputedStyle: () => ({
borderLeftWidth: '0px', borderRightWidth: '0px',
borderTopWidth: '0px', borderBottomWidth: '0px',
paddingLeft: '0px', paddingRight: '0px',
paddingTop: '0px', paddingBottom: '0px',
})
};
}
it('should clamp out-of-bounds coordinates', function () {
stubWindow();
const canvas: any = {
width: 100, height: 100,
getBoundingClientRect: () => ({ left: 0, top: 0, width: 100, height: 100 })
};
const pos = getMousePos(canvas, { clientX: 10000, clientY: -100 });
assert.deepStrictEqual(pos, { x: 100, y: 0 });
});
it('should account for borders and padding', function () {
(global as any).window = {
getComputedStyle: () => ({
borderLeftWidth: '2px', borderRightWidth: '2px',
borderTopWidth: '2px', borderBottomWidth: '2px',
paddingLeft: '2px', paddingRight: '2px',
paddingTop: '2px', paddingBottom: '2px',
})
};
const canvas: any = {
width: 100, height: 100,
getBoundingClientRect: () => ({ left: 10, top: 10, width: 108, height: 108 })
};
// content origin = rect.left + border(2) + padding(2) = 14
let pos = getMousePos(canvas, { clientX: 14, clientY: 14 });
assert.deepStrictEqual(pos, { x: 0, y: 0 });
// content far edge
pos = getMousePos(canvas, { clientX: 14 + 99, clientY: 14 + 99 });
assert.ok(pos.x >= 99 && pos.x <= 100);
});
});