3937109edb
Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
522 lines
13 KiB
JavaScript
522 lines
13 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 basicNumbers = [
|
|
0, 1, -1, 2, -2, 5, -5, 10, -10, 0.1, -0.1, 0.2, -0.2, 0.5, -0.5,
|
|
123, -123, 1.23, -1.23,
|
|
1234567, -1234567, 99999999, -99999999,
|
|
];
|
|
|
|
const edgeCaseNumbers = [
|
|
-0,
|
|
Number.MAX_SAFE_INTEGER,
|
|
Number.MIN_SAFE_INTEGER,
|
|
Math.PI, Math.E,
|
|
0.000001, -0.000001, 0.00000001, -0.00000001,
|
|
1000000, -1000000, 100000000, -100000000,
|
|
0.1+0.2, // floating point fun
|
|
1/3, 2/3, 0.9999999, 1.0000001
|
|
];
|
|
|
|
// Generate some more numbers to reach the target count
|
|
const generatedNumbers = [];
|
|
for (let i = 1; i <= 20; i++) {
|
|
// integers
|
|
generatedNumbers.push(i * 123);
|
|
generatedNumbers.push(i * -123);
|
|
// floats
|
|
generatedNumbers.push(i / 10);
|
|
generatedNumbers.push(i / -10);
|
|
// small floats
|
|
generatedNumbers.push(i / 1000);
|
|
generatedNumbers.push(i / -1000);
|
|
}
|
|
|
|
const testNumbers = [...new Set([...basicNumbers, ...edgeCaseNumbers, ...generatedNumbers])];
|
|
// We need to filter out Infinity/-Infinity for pressNumber helper as they can't be typed.
|
|
// They can only appear as results, which we test for separately.
|
|
const finiteTestNumbers = testNumbers.filter(n => isFinite(n));
|
|
|
|
// Helper to press a number, handling negatives
|
|
function pressNumber(n) {
|
|
let s = String(n);
|
|
// If string representation is in scientific notation, convert to decimal string
|
|
if (s.includes('e')) {
|
|
s = n.toFixed(20).replace(/0+$/, '').replace(/\.$/, '');
|
|
}
|
|
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 finiteTestNumbers) {
|
|
for (const b of finiteTestNumbers) {
|
|
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: Operations with non-finite results ---
|
|
const smallFiniteTestNumbers = [1, -1, 5, -5, 0, 100, -100]; // for speed
|
|
|
|
// Test operations that result in Infinity, -Infinity, or NaN, and then continue with another operation
|
|
for (const a of smallFiniteTestNumbers) {
|
|
// a / 0 -> Infinity/-Infinity/NaN
|
|
test(`Non-finite chaining: (${a} / 0) + 5`, () => {
|
|
pressNumber(a);
|
|
press('/0=');
|
|
const intermediate = a/0;
|
|
checkDisplay(intermediate);
|
|
press('+5=');
|
|
checkDisplay(intermediate + 5);
|
|
});
|
|
|
|
test(`Non-finite chaining: (${a} / 0) * 5`, () => {
|
|
pressNumber(a);
|
|
press('/0=');
|
|
const intermediate = a/0;
|
|
checkDisplay(intermediate);
|
|
press('*5=');
|
|
checkDisplay(intermediate * 5);
|
|
});
|
|
|
|
test(`Non-finite chaining: (${a} / 0) * 0`, () => {
|
|
pressNumber(a);
|
|
press('/0=');
|
|
const intermediate = a/0;
|
|
checkDisplay(intermediate);
|
|
press('*0=');
|
|
checkDisplay(intermediate * 0); // Should be NaN
|
|
});
|
|
|
|
test(`Non-finite chaining: (${a} / 0) / 5`, () => {
|
|
pressNumber(a);
|
|
press('/0=');
|
|
const intermediate = a/0;
|
|
checkDisplay(intermediate);
|
|
press('/5=');
|
|
checkDisplay(intermediate / 5);
|
|
});
|
|
|
|
test(`Non-finite chaining: (${a} / 0) / 0`, () => {
|
|
pressNumber(a);
|
|
press('/0=');
|
|
const intermediate = a/0;
|
|
checkDisplay(intermediate);
|
|
press('/0=');
|
|
checkDisplay(intermediate / 0);
|
|
});
|
|
}
|
|
// 0/0 = NaN. Test operations on NaN
|
|
test(`Non-finite chaining: (0 / 0) + 5`, () => {
|
|
press('0/0=');
|
|
checkDisplay(NaN);
|
|
press('+5=');
|
|
checkDisplay(NaN + 5); // anything + NaN is NaN
|
|
});
|
|
test(`Non-finite chaining: (0 / 0) * 5`, () => {
|
|
press('0/0=');
|
|
checkDisplay(NaN);
|
|
press('*5=');
|
|
checkDisplay(NaN * 5);
|
|
});
|
|
test(`Non-finite chaining: (0 / 0) / 5`, () => {
|
|
press('0/0=');
|
|
checkDisplay(NaN);
|
|
press('/5=');
|
|
checkDisplay(NaN / 5);
|
|
});
|
|
test(`Non-finite chaining: sqrt(negative) then op`, () => {
|
|
press('9N'); // -9
|
|
press('r'); // sqrt
|
|
checkDisplay(NaN);
|
|
press('+1=');
|
|
checkDisplay(NaN);
|
|
});
|
|
|
|
|
|
// --- 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 },
|
|
};
|
|
|
|
for (const op in unaryOps) {
|
|
for (const a of finiteTestNumbers) {
|
|
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, -1, 5, -5, 0.5, -0.5, 10, -10, 123, -123, 1.23];
|
|
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, 15, 30, 45, 60, 75, 90, 180, 270, 360, -30, -45, -90, -180, 450, 720, Math.PI/6, Math.PI/4, Math.PI/3, Math.PI/2, Math.PI, 3*Math.PI/2, 2*Math.PI, -Math.PI/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');
|
|
});
|
|
|
|
test('Input starting with decimal point', () => {
|
|
press('.5');
|
|
checkDisplay('0.5');
|
|
press('*2=');
|
|
checkDisplay('1');
|
|
});
|
|
|
|
test('Negative zero handling', () => {
|
|
press('0N'); // get -0
|
|
checkDisplay(-0);
|
|
press('*5');
|
|
checkDisplay('5'); // after typing 5, display should be 5
|
|
press('=');
|
|
checkDisplay(-0); // -0 * 5 = -0
|
|
buttonPress('R'); buttonPress('R');
|
|
press('5*0N='); // 5 * -0 = -0
|
|
checkDisplay(-0);
|
|
});
|
|
|
|
test('Pi button', () => {
|
|
press('p');
|
|
checkDisplay(Math.PI);
|
|
press('*2=');
|
|
checkDisplay(Math.PI * 2);
|
|
});
|
|
|
|
test('Pi replaces current number entry', () => {
|
|
press('123p');
|
|
checkDisplay(Math.PI);
|
|
});
|
|
|
|
test('Operation with Pi', () => {
|
|
press('5*p=');
|
|
checkDisplay(5 * Math.PI);
|
|
});
|
|
|
|
// Run all the defined tests
|
|
runTests();
|