From d40c74870e0dd178433635fb11730d9656fe1572 Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Mon, 21 Sep 2026 22:40:21 +0200 Subject: [PATCH] fix(numpad): segno perso e raffica HID concatenata a cifre manuali (#11, #27) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit toggleSign() usciva subito a display vuoto: il segno del calibro, che arriva prima delle cifre, veniva scartato. Ora '-'/'+' impostano il segno in modo esplicito (non toggle) quando arrivano da tastiera/HID; toggleSign() (bottone manuale +/-) funziona anche a vuoto. _trackBurst riconosce l'inizio di una raffica solo dal secondo carattere: prima ripuliva il display in ritardo, concatenando cifre digitate a mano. Ora la raffica ripulisce il display fino al proprio inizio quando la pausa prima di essa è > 300ms. Parte intera oltre 6 cifre: rifiutata con evento numpad-error invece di essere troncata in silenzio. Test node in tests/js/test_numpad.js, lanciato da pytest (tests/test_numpad_js.py, skip se node manca). Co-Authored-By: Claude Sonnet 5 --- src/frontend/flask_app/static/js/numpad.js | 63 ++++++++- .../flask_app/tests/js/test_numpad.js | 124 ++++++++++++++++++ .../flask_app/tests/test_numpad_js.py | 36 +++++ 3 files changed, 216 insertions(+), 7 deletions(-) create mode 100644 src/frontend/flask_app/tests/js/test_numpad.js create mode 100644 src/frontend/flask_app/tests/test_numpad_js.py diff --git a/src/frontend/flask_app/static/js/numpad.js b/src/frontend/flask_app/static/js/numpad.js index fefd37d..90088dd 100644 --- a/src/frontend/flask_app/static/js/numpad.js +++ b/src/frontend/flask_app/static/js/numpad.js @@ -25,6 +25,8 @@ function numpad(options) { _lastKeyTime: 0, // Timestamp of last keystroke _burstCount: 0, // Consecutive fast keystrokes _enterWasFast: false, // Enter arrived in the same burst as the digits + _rejected: false, // Current reading refused (e.g. overflow); ignore the rest of it + _runStart: null, // {value, gap} snapshot taken at the first key of the current run /** * Get the display value with sign @@ -55,6 +57,8 @@ function numpad(options) { * @param {string} d - The digit to add (0-9) */ addDigit(d) { + if (this._rejected) return; + // Validate: don't exceed max digits const parts = this.value.split('.'); @@ -63,7 +67,15 @@ function numpad(options) { if (parts[1] && parts[1].length >= this.maxDecDigits) return; } else { // Check integer part length - if (parts[0] && parts[0].length >= this.maxIntDigits) return; + if (parts[0] && parts[0].length >= this.maxIntDigits) { + // A caliper reading whose integer part does not fit must not be + // quietly cut down to something that does: refuse the whole + // reading instead of saving a truncated number. Manual typing + // (outside a burst) keeps the old, harmless behaviour of simply + // not accepting more digits. + if (this._burstCount >= 2) this._rejectReading('integer_overflow'); + return; + } } // Prevent leading zeros (except "0.") @@ -79,6 +91,8 @@ function numpad(options) { * Add a decimal point to the current value */ addDecimal() { + if (this._rejected) return; + // Don't add decimal if one already exists if (this.hasDecimal) return; @@ -90,10 +104,11 @@ function numpad(options) { }, /** - * Toggle the sign of the current value + * Toggle the sign of the current value. Works on an empty display too - + * the manual +/- key is pressed before the first digit as often as after + * one, and used to be silently dropped in that case. */ toggleSign() { - if (!this.value) return; this.negative = !this.negative; }, @@ -119,6 +134,8 @@ function numpad(options) { this.negative = false; this.hasDecimal = false; this._enterWasFast = false; + this._rejected = false; + this._runStart = null; }, /** @@ -138,6 +155,7 @@ function numpad(options) { * Confirm the current value and dispatch event */ confirm() { + this._rejected = false; if (!this.hasValue) return; const val = this.numericValue; @@ -239,11 +257,32 @@ function numpad(options) { this._enterWasFast = gap < 80; this.confirm(); } - // Minus sign - toggle sign + // Minus / plus - the caliper's own sign character. Unlike the manual + // +/- key (toggleSign(), bound to the on-screen button) these are + // absolute rather than a toggle: a caliper transmission says which + // sign the reading is, it never flips one already on screen. else if (e.key === '-') { e.preventDefault(); - this.toggleSign(); + this.negative = true; } + else if (e.key === '+') { + e.preventDefault(); + this.negative = false; + } + }, + + /** + * Refuse the reading in progress: say why with an event the page can show, + * and wipe the display so the rejected digits do not linger as if valid. + * Further keystrokes of the same burst are ignored until Enter or a clear. + */ + _rejectReading(reason) { + this.$dispatch('numpad-error', { reason: reason }); + this.value = ''; + this.negative = false; + this.hasDecimal = false; + this._enterWasFast = false; + this._rejected = true; }, /** @@ -253,12 +292,22 @@ function numpad(options) { */ _trackBurst() { const now = performance.now(); - const gap = now - this._lastKeyTime; + const gap = this._lastKeyTime > 0 ? now - this._lastKeyTime : Infinity; - if (this._lastKeyTime > 0 && gap < 80) { + if (gap < 80) { this._burstCount++; + // The burst is only recognisable from its second character. If the + // pause before the first one was long enough to be a human keystroke, + // whatever was on display before this run began was typed by hand: + // drop it now instead of letting the caliper's own digits + // concatenate onto it. + if (this._burstCount === 2 && this._runStart && this._runStart.gap > 300) { + this.value = this.value.slice(this._runStart.value.length); + this.hasDecimal = this.value.includes('.'); + } } else { this._burstCount = 1; + this._runStart = { value: this.value, gap: gap }; } this._lastKeyTime = now; diff --git a/src/frontend/flask_app/tests/js/test_numpad.js b/src/frontend/flask_app/tests/js/test_numpad.js new file mode 100644 index 0000000..496507a --- /dev/null +++ b/src/frontend/flask_app/tests/js/test_numpad.js @@ -0,0 +1,124 @@ +/** + * numpad.js behaviour under a simulated USB caliper / physical keyboard, run + * with plain node (no browser, no test framework) against the same + * handleKeydown() path a real wedge device drives. + * + * Covers gitea:Adriano/TieMeasureFlow#11 (sign lost when the caliper sends it + * before the digits) and #27 (leftover manual digits concatenate onto a + * caliper burst; an integer part over the digit limit used to be truncated + * in silence instead of refused). + * + * Run from src/frontend/flask_app: node tests/js/test_numpad.js + */ +'use strict'; +const fs = require('fs'); +const path = require('path'); + +// A fake performance.now(): T advances by the gap the test hands to send()/ +// press(), so "fast" (<80ms, HID) and "slow" (>80ms, human) keystrokes are +// simulated without any real waiting. +global.performance = { now: () => T }; +let T = 1000; + +const numpadSrc = fs.readFileSync( + path.join(__dirname, '..', '..', 'static', 'js', 'numpad.js'), 'utf8', +); +eval(numpadSrc + ';global.numpad = numpad;'); + +function mk() { + const n = numpad({}); + n._events = []; + n.$dispatch = (name, detail) => n._events.push({ name: name, detail: detail }); + return n; +} + +function press(n, key, gap) { + T += gap; + n.handleKeydown({ + key: key, + target: { tagName: 'BODY' }, + preventDefault() {}, + }); +} + +function send(n, str, gap) { + for (const ch of str) press(n, ch === '\r' ? 'Enter' : ch, gap); +} + +function lastEvent(n, name) { + const evs = n._events.filter(e => e.name === name); + return evs.length ? evs[evs.length - 1].detail : null; +} + +let failures = 0; +function check(label, cond, extra) { + if (cond) { + console.log('ok - ' + label); + } else { + failures++; + console.error('NOT OK - ' + label + (extra ? ' (' + extra + ')' : '')); + } +} + +// #11 — the caliper sends the sign before the digits: "-1.25\r" +{ + const n = mk(); + send(n, '-1.25\r', 10); + const c = lastEvent(n, 'numpad-confirm'); + check('#11 raffica negativa "-1.25\\r" -> -1.25', c && c.value === -1.25, JSON.stringify(c)); + check('#11 raffica negativa classificata usb_caliper', c && c.inputMethod === 'usb_caliper'); +} + +// #11 — explicit "+" also arrives as its own character +{ + const n = mk(); + send(n, '+12.34\r', 10); + const c = lastEvent(n, 'numpad-confirm'); + check('#11 raffica positiva "+12.34\\r" -> 12.34', c && c.value === 12.34, JSON.stringify(c)); +} + +// #11 — a manual "-" press followed by clear must not leak into the next reading +{ + const n = mk(); + press(n, '-', 10); + n.clearAll(); + send(n, '1.2\r', 200); + const c = lastEvent(n, 'numpad-confirm'); + check('#11 "-" poi clear poi "1.2\\r" -> 1.2 (segno azzerato)', c && c.value === 1.2, JSON.stringify(c)); +} + +// #27 — a digit left on screen from manual typing must not concatenate onto +// a caliper burst that follows a real pause. +{ + const n = mk(); + n.addDigit('1'); + T += 5000; + send(n, '12.34\r', 10); + const c = lastEvent(n, 'numpad-confirm'); + check('#27 tocco "1", pausa, raffica "12.34\\r" -> 12.34 (non 112.34)', c && c.value === 12.34, JSON.stringify(c)); +} + +// #27 — an integer part over the digit limit is refused, not silently cut down +{ + const n = mk(); + send(n, '1234567.5\r', 10); + const c = lastEvent(n, 'numpad-confirm'); + const err = lastEvent(n, 'numpad-error'); + check('#27 raffica "1234567.5\\r" -> nessun confirm', c === null, JSON.stringify(c)); + check('#27 raffica "1234567.5\\r" -> numpad-error emesso', !!err, JSON.stringify(err)); +} + +// #27 — slow, one-key-at-a-time manual typing is unaffected by any of the above +{ + const n = mk(); + send(n, '12.5\r', 200); + const c = lastEvent(n, 'numpad-confirm'); + check('#27 digitazione manuale lenta "1,2,.,5,Enter" -> 12.5 invariato', c && c.value === 12.5, JSON.stringify(c)); + check('#27 digitazione manuale lenta classificata manual', c && c.inputMethod === 'manual'); +} + +if (failures > 0) { + console.error(failures + ' test falliti su numpad.js'); + process.exit(1); +} +console.log('tutti i test numpad.js superati'); diff --git a/src/frontend/flask_app/tests/test_numpad_js.py b/src/frontend/flask_app/tests/test_numpad_js.py new file mode 100644 index 0000000..40635c6 --- /dev/null +++ b/src/frontend/flask_app/tests/test_numpad_js.py @@ -0,0 +1,36 @@ +"""Runs the plain-node numpad.js behaviour test (tests/js/test_numpad.js). + +There is no browser/JS test harness in this project yet, so this is a thin +pytest wrapper: it shells out to ``node`` and asserts the script's own exit +code. Skips (does not fail) when node is not installed, since numpad.js's +logic still gets exercised by whatever picks this up where node is present. + +Covers gitea:Adriano/TieMeasureFlow#11 and #27. +""" +import shutil +import subprocess +from pathlib import Path + +import pytest + +FLASK_APP_DIR = Path(__file__).resolve().parent.parent +JS_TEST = Path(__file__).resolve().parent / "js" / "test_numpad.js" + + +def test_numpad_js_hid_sign_and_burst_behaviour(): + node = shutil.which("node") + if not node: + pytest.skip("node non installato: test JS di numpad.js saltato") + + result = subprocess.run( + [node, str(JS_TEST)], + cwd=str(FLASK_APP_DIR), + capture_output=True, + text=True, + timeout=30, + ) + + assert result.returncode == 0, ( + f"tests/js/test_numpad.js fallito (exit {result.returncode})\n" + f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" + )