const fs = require('fs'); const path = require('path'); const assert = require('assert'); // --- Mock Bangle.js environment --- let mock_display_str = ""; let mock_ui_callbacks = {}; // Mock 'g' (graphics) global.g = { _col: 0, _bg: 0, _font: "6x8", _font_size: 1, clear: () => {}, clearRect: () => {}, 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 * 8 * (global.g._font_size || 1), // simple approximation getWidth: () => 176, getHeight: () => 176, drawString: (s) => { mock_display_str = String(s); }, getFontHeight: () => 8 * (global.g._font_size || 1) }; global.Graphics = {}; // Mock 'process' global.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 --- const calculatorCode = fs.readFileSync(path.join(__dirname, 'calculator.app.js'), 'utf8'); (function(require) { eval(calculatorCode); })((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) { assert.strictEqual(mock_display_str, String(expected), message); } // --- End Test Framework --- // --- Test Cases --- test('Addition', () => { press('1+2='); checkDisplay('3'); }); test('Subtraction', () => { press('9-5='); checkDisplay('4'); }); test('Multiplication', () => { press('3*4='); checkDisplay('12'); }); test('Division', () => { press('10/2='); checkDisplay('5'); }); test('Chained operations (no precedence)', () => { press('2+3*4='); // (2+3)*4 = 20 checkDisplay('20'); }); test('Decimal numbers', () => { press('1.5+2.5='); checkDisplay('4'); }); 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('Negative result', () => { press('5-10='); checkDisplay('-5'); }); test('Negative input', () => { press('5*'); press('2'); press('N'); // make it -2 press('='); checkDisplay('-10'); }); test('Division by zero', () => { press('5/0='); checkDisplay('INF'); }); test('Percentage', () => { press('200*5%='); checkDisplay('10'); }); test('Square root', () => { press('16'); press('r'); checkDisplay('4'); press('='); // pressing equals after unary op checkDisplay('4'); }); test('Square', () => { press('5'); press('s'); checkDisplay('25'); press('='); checkDisplay('25'); }); test('Inverse', () => { press('4'); press('i'); checkDisplay('0.25'); }); test('Backspace', () => { press('123B'); 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'); }); test('Operator change', () => { press('10+-*/2='); checkDisplay('5'); // 10/2=5 }); test('Long number with separators', () => { press('1234567'); checkDisplay('1,234,567'); }); test('Sine (deg)', () => { press('30'); press('sin'); checkDisplay('0.5'); press('R'); press('R'); press('90'); press('sin'); checkDisplay('1'); press('R'); press('R'); press('180'); press('sin'); checkDisplay('0'); }); test('Cosine (deg)', () => { press('60'); press('cos'); checkDisplay('0.5'); press('R'); press('R'); press('0'); press('cos'); checkDisplay('1'); press('R'); press('R'); press('90'); press('cos'); checkDisplay('0'); }); test('Angle mode switch to radians and back', () => { press('angleMode'); // now rad // The buttonPress function doesn't return the state, but we can check the object it modifies assert.strictEqual(scientificOperators.angleMode.val, 'rad'); press('angleMode'); // back to deg assert.strictEqual(scientificOperators.angleMode.val, 'deg'); }); // Run all the defined tests runTests();