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
|
||||
_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;
|
||||
|
||||
@@ -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