diff --git a/calculator/test.js b/calculator/test.js index e69de29..4d31fbf 100644 --- a/calculator/test.js +++ b/calculator/test.js @@ -0,0 +1,284 @@ +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 = () => {}; + +// Mock 'require' for fonts +const originalRequire = require; +global.require = (name) => { + if (name === "FontDylex7x13") { + return { add: () => {} }; + } + return originalRequire(name); +}; +// --- End Mock --- + + +// --- Load calculator app --- +const calculatorCode = fs.readFileSync(path.join(__dirname, 'calculator.app.js'), 'utf8'); +eval(calculatorCode); +// --- 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();