Files
bangle/calculator/test.js
T
2026-06-13 16:13:48 -06:00

374 lines
9.3 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const assert = require('assert');
// --- Mock Bangle.js environment ---
let mock_display_str = "";
let mock_ui_callbacks = {};
let drawn_since_clear = false;
global.last_display_num = null;
// Mock 'g' (graphics)
global.g = {
_col: 0,
_bg: 0,
_font: "6x8",
_font_size: 1,
clear: () => { drawn_since_clear = false; },
clearRect: () => { drawn_since_clear = false; },
setColor: (c) => { global.g._col = c; return global.g; },
setBgColor: (c) => { global.g._bg = c; return global.g; },
fillRect: () => {},
setFont: function(font, size) {
if (font && font.includes(":")) {
const parts = font.split(":");
this._font = parts[0];
this._font_size = parseInt(parts[1], 10);
} else {
this._font = font;
if (size) this._font_size = size;
}
return this;
},
setFontAlign: () => {},
stringWidth: (s) => s.length * 4.7 * (global.g._font_size || 1), // approximation to match watch
getWidth: () => 176,
getHeight: () => 176,
drawString: (s) => {
if (!drawn_since_clear) {
mock_display_str = String(s);
drawn_since_clear = true;
}
},
getFontHeight: () => 8 * (global.g._font_size || 1)
};
global.Graphics = {};
// Mock Bangle.js-specific process.env properties
process.env.HWVERSION = 2; // Simulate Bangle.js 2
// Mock 'Bangle'
global.Bangle = {
setUI: (callbacks) => { mock_ui_callbacks = callbacks; },
http: () => new Promise(resolve => resolve({resp: "{}"})),
buzz: () => {},
};
// Mock 'load' (to exit app)
global.load = () => {};
// --- End Mock ---
// --- Load calculator app ---
// We inject a "spy" to capture the raw value passed to displayOutput,
// making our tests independent of UI formatting.
let calculatorCode = fs.readFileSync(path.join(__dirname, 'calculator.app.js'), 'utf8');
// 1. Rename the original function so we can call it.
calculatorCode = calculatorCode.replace(
'function displayOutput(num)',
'function _original_displayOutput(num)'
);
// 2. Prepend our spy that captures the value and then calls the original.
const spyCode = `
var displayOutput = function(num) {
global.last_display_num = num;
_original_displayOutput(num);
};
`;
const processedCode = spyCode + calculatorCode;
const wrappedCode = `(function(require) { ${processedCode}; return { buttonPress, scientificOperators }; })`;
const getAppFns = eval(wrappedCode);
const { buttonPress, scientificOperators } = getAppFns((name) => {
if (name === "FontDylex7x13") {
return { add: () => {} };
}
return require(name);
});
// --- End Load ---
// --- Test Framework ---
const test_suite = [];
let tests_passed = 0;
let tests_failed = 0;
function test(name, fn) {
test_suite.push({ name, fn });
}
function runTests() {
test_suite.forEach(t => {
// Reset state before each test
buttonPress('R'); // C
buttonPress('R'); // AC
if (global.last_display_num !== 0) {
console.error(`[FAIL] ${t.name} - Failed to reset state. Last number is: "${global.last_display_num}"`);
tests_failed++;
return;
}
try {
t.fn();
console.log(`[PASS] ${t.name}`);
tests_passed++;
} catch (e) {
console.error(`[FAIL] ${t.name}`);
console.error(e);
tests_failed++;
}
});
console.log(`\nTests finished. Passed: ${tests_passed}, Failed: ${tests_failed}.`);
if (tests_failed > 0) {
process.exit(1);
}
}
function press(buttons) {
for (const button of buttons) {
buttonPress(button);
}
}
function checkDisplay(expected, message) {
const actual = global.last_display_num;
if (typeof expected === 'number' && isNaN(expected)) {
assert.ok(typeof actual === 'number' && isNaN(actual), message || `Expected NaN, got ${actual}`);
return;
}
// Coerce to numbers for comparison to handle string vs number differences ('5' vs 5)
// and use a tolerance for floating point values.
const actualNum = parseFloat(actual);
const expectedNum = parseFloat(expected);
if (!isNaN(actualNum) && !isNaN(expectedNum)) {
const tolerance = 1e-9;
if (Math.abs(expectedNum - actualNum) < tolerance || actualNum === expectedNum) { // second part handles Infinity
return; // The numbers are close enough.
}
}
// Fallback to strict equality for non-numeric strings or when numeric check fails
assert.strictEqual(actual, expected, message);
}
// --- End Test Framework ---
// --- Test Cases ---
const testNumbers = [
0, 1, -1, 5, -5, 0.5, -0.5, 123, -123, 1.23, -1.23,
1234567, -1234567, 99999999, -99999999,
1e-6, -1e-6, Math.PI, Math.E
];
// Helper to press a number, handling negatives
function pressNumber(n) {
const s = String(n);
if (s.startsWith('-')) {
press(s.substring(1));
buttonPress('N');
} else {
press(s);
}
}
// --- Generated Tests: Binary Operations ---
const binaryOps = {
'+': (a, b) => a + b,
'-': (a, b) => a - b,
'*': (a, b) => a * b,
'/': (a, b) => a / b,
};
for (const op in binaryOps) {
for (const a of testNumbers) {
for (const b of testNumbers) {
if (op === '/' && b === 0) {
test(`Binary Edge Case: ${a} / 0`, () => {
pressNumber(a);
buttonPress('/');
pressNumber(0);
buttonPress('=');
checkDisplay(a === 0 ? NaN : (a > 0 ? Infinity : -Infinity));
});
continue;
}
test(`Binary: ${a} ${op} ${b}`, () => {
pressNumber(a);
buttonPress(op);
pressNumber(b);
buttonPress('=');
const expected = binaryOps[op](a, b);
checkDisplay(expected);
});
}
}
}
// --- Generated Tests: Unary Operations ---
const unaryOps = {
'r': { name: 'sqrt', fn: Math.sqrt },
's': { name: 'x^2', fn: (a) => a * a },
'i': { name: '1/x', fn: (a) => 1 / a },
'%': { name: '%', fn: (a) => a / 100 },
};
for (const op in unaryOps) {
for (const a of testNumbers) {
if (op === 'r' && a < 0) {
test(`Unary Edge Case: sqrt(${a})`, () => {
pressNumber(a);
buttonPress('r');
checkDisplay(NaN);
});
continue;
}
test(`Unary: ${unaryOps[op].name}(${a})`, () => {
pressNumber(a);
buttonPress(op);
const expected = unaryOps[op].fn(a);
checkDisplay(expected);
});
}
}
// --- Generated Tests: Chaining Operations ---
// To keep runtime sane, we'll pick a smaller subset for chaining
const chainNumbers = [0, 1, -5, 0.5, 10, 123];
const chainOps = ['+', '-', '*', '/'];
for (const a of chainNumbers) {
for (const b of chainNumbers) {
for (const c of chainNumbers) {
for (const op1 of chainOps) {
for (const op2 of chainOps) {
test(`Chaining: ${a} ${op1} ${b} ${op2} ${c}`, () => {
pressNumber(a);
buttonPress(op1);
pressNumber(b);
buttonPress(op2); // This will execute the first operation
pressNumber(c);
buttonPress('='); // This will execute the second operation
// Calculator does sequential evaluation: (a op1 b) op2 c
const expected = binaryOps[op2](binaryOps[op1](a, b), c);
checkDisplay(expected);
});
}
}
}
}
}
// --- Generated Tests: Trigonometry ---
const trigOps = {
'sin': { fn: Math.sin, name: 'sin' },
'cos': { fn: Math.cos, name: 'cos' },
'tan': { fn: Math.tan, name: 'tan' },
};
const trigAngles = [0, 30, 45, 60, 90, 180, 270, 360, Math.PI/6, Math.PI/4, Math.PI/2, Math.PI, 3*Math.PI/2, 2*Math.PI];
// Test in DEG mode (default)
for (const op in trigOps) {
for (const angle of trigAngles) {
test(`Trig (deg): ${trigOps[op].name}(${angle})`, () => {
pressNumber(angle);
buttonPress(op);
const expected = trigOps[op].fn(angle * Math.PI / 180);
checkDisplay(expected);
});
}
}
// Test in RAD mode
test('Switch to RAD mode', () => {
buttonPress('angleMode');
assert.strictEqual(scientificOperators.angleMode.val, 'rad');
});
for (const op in trigOps) {
for (const angle of trigAngles) {
test(`Trig (rad): ${trigOps[op].name}(${angle})`, () => {
pressNumber(angle);
buttonPress(op);
const expected = trigOps[op].fn(angle);
checkDisplay(expected);
});
}
}
test('Switch back to DEG mode for subsequent tests', () => {
buttonPress('angleMode');
assert.strictEqual(scientificOperators.angleMode.val, 'deg');
});
// --- Specific Functionality Tests ---
test('Clear and All Clear', () => {
press('123+');
checkDisplay('123');
press('R'); // Clear
checkDisplay('0');
press('456');
checkDisplay('456');
press('=');
checkDisplay('579'); // 123 + 456
press('R'); // Clear
checkDisplay('0');
buttonPress('R'); // All Clear
press('+1=');
checkDisplay('1');
});
test('Backspace', () => {
press('123.45B');
checkDisplay('123.4');
press('B');
checkDisplay('123.');
press('B');
checkDisplay('123');
press('B');
checkDisplay('12');
press('B');
checkDisplay('1');
press('B');
checkDisplay('0');
press('B');
checkDisplay('0');
});
test('Repeated equals', () => {
press('2+3=');
checkDisplay('5');
press('=');
checkDisplay('8');
press('=');
checkDisplay('11');
press('R'); press('R');
press('10-2=');
checkDisplay('8');
press('=');
checkDisplay('6');
});
test('Operator change', () => {
press('10+-*/2=');
checkDisplay('5'); // 10/2=5
});
test('Long number input', () => {
press('1234567');
checkDisplay('1234567');
});
// Run all the defined tests
runTests();