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; // 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 wrap the app code in a function that returns the buttonPress function, // so we can capture it and use it in our tests. const calculatorCode = fs.readFileSync(path.join(__dirname, 'calculator.app.js'), 'utf8'); const wrappedCode = `(function(require) { ${calculatorCode}; 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 (mock_display_str !== '0') { console.error(`[FAIL] ${t.name} - Failed to reset state. Display is: "${mock_display_str}"`); 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) { let expectedStr = String(expected); if (expected === Infinity) { expectedStr = 'INF'; } else if (expected === -Infinity) { expectedStr = '-INF'; } // attempt a numeric comparison for float-related issues const expectedNum = parseFloat(expectedStr.replace(/,/g, '')); let actualStrForParsing = mock_display_str.replace(/,/g, ''); if (actualStrForParsing.endsWith('...')) { actualStrForParsing = actualStrForParsing.slice(0, -3); } const actualNum = parseFloat(actualStrForParsing); if (!isNaN(expectedNum) && !isNaN(actualNum)) { if (expectedNum === actualNum) return; // Handles Infinity and exact matches const tolerance = 1e-4; // Increased tolerance for float comparisons if (Math.abs(expectedNum - actualNum) < tolerance) { return; // Close enough for floating point } } // Fallback for strings, formatted numbers, or failed float checks assert.strictEqual(mock_display_str, expectedStr, 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) { // tan(pi/2) is Infinity, which the calculator can handle if (op === 'tan' && (Math.abs(angle - Math.PI/2) < 1e-9 || Math.abs(angle - 3*Math.PI/2) < 1e-9)) { test(`Trig Edge Case (rad): tan(${angle})`, () => { pressNumber(angle); buttonPress('tan'); checkDisplay(Infinity); }); continue; } 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('BB'); 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 with separators', () => { press('1234567'); checkDisplay('1,234,567'); }); // Run all the defined tests runTests();