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 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,8 @@ function numpad(options) {
|
|||||||
_lastKeyTime: 0, // Timestamp of last keystroke
|
_lastKeyTime: 0, // Timestamp of last keystroke
|
||||||
_burstCount: 0, // Consecutive fast keystrokes
|
_burstCount: 0, // Consecutive fast keystrokes
|
||||||
_enterWasFast: false, // Enter arrived in the same burst as the digits
|
_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
|
* Get the display value with sign
|
||||||
@@ -55,6 +57,8 @@ function numpad(options) {
|
|||||||
* @param {string} d - The digit to add (0-9)
|
* @param {string} d - The digit to add (0-9)
|
||||||
*/
|
*/
|
||||||
addDigit(d) {
|
addDigit(d) {
|
||||||
|
if (this._rejected) return;
|
||||||
|
|
||||||
// Validate: don't exceed max digits
|
// Validate: don't exceed max digits
|
||||||
const parts = this.value.split('.');
|
const parts = this.value.split('.');
|
||||||
|
|
||||||
@@ -63,7 +67,15 @@ function numpad(options) {
|
|||||||
if (parts[1] && parts[1].length >= this.maxDecDigits) return;
|
if (parts[1] && parts[1].length >= this.maxDecDigits) return;
|
||||||
} else {
|
} else {
|
||||||
// Check integer part length
|
// 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.")
|
// Prevent leading zeros (except "0.")
|
||||||
@@ -79,6 +91,8 @@ function numpad(options) {
|
|||||||
* Add a decimal point to the current value
|
* Add a decimal point to the current value
|
||||||
*/
|
*/
|
||||||
addDecimal() {
|
addDecimal() {
|
||||||
|
if (this._rejected) return;
|
||||||
|
|
||||||
// Don't add decimal if one already exists
|
// Don't add decimal if one already exists
|
||||||
if (this.hasDecimal) return;
|
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() {
|
toggleSign() {
|
||||||
if (!this.value) return;
|
|
||||||
this.negative = !this.negative;
|
this.negative = !this.negative;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -119,6 +134,8 @@ function numpad(options) {
|
|||||||
this.negative = false;
|
this.negative = false;
|
||||||
this.hasDecimal = false;
|
this.hasDecimal = false;
|
||||||
this._enterWasFast = 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 the current value and dispatch event
|
||||||
*/
|
*/
|
||||||
confirm() {
|
confirm() {
|
||||||
|
this._rejected = false;
|
||||||
if (!this.hasValue) return;
|
if (!this.hasValue) return;
|
||||||
|
|
||||||
const val = this.numericValue;
|
const val = this.numericValue;
|
||||||
@@ -239,11 +257,32 @@ function numpad(options) {
|
|||||||
this._enterWasFast = gap < 80;
|
this._enterWasFast = gap < 80;
|
||||||
this.confirm();
|
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 === '-') {
|
else if (e.key === '-') {
|
||||||
e.preventDefault();
|
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() {
|
_trackBurst() {
|
||||||
const now = performance.now();
|
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++;
|
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 {
|
} else {
|
||||||
this._burstCount = 1;
|
this._burstCount = 1;
|
||||||
|
this._runStart = { value: this.value, gap: gap };
|
||||||
}
|
}
|
||||||
|
|
||||||
this._lastKeyTime = now;
|
this._lastKeyTime = now;
|
||||||
|
|||||||
@@ -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');
|
||||||
@@ -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}"
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user