UI improvements, and control softening

This commit is contained in:
2026-08-27 20:11:37 +02:00
parent 84ec93543b
commit 73c20a0ff2
14 changed files with 374 additions and 38 deletions
+93
View File
@@ -5,6 +5,99 @@ const fs=require('node:fs'),vm=require('node:vm'),path=require('node:path');
const {present}=require('../web/control-state.js');
const armed={connected:true,armed_known:true,armed:true,app_state:'armed',servos:{}};
test('model viewport fills the available space above battery status',()=>{
const css=fs.readFileSync(path.join(__dirname,'../web/styles.css'),'utf8');
assert.match(css,/\.rov-stage \{ flex:1; height:auto; min-height:0; width:100%/);
assert.match(css,/\.telemetry-zone \.model-card \{ flex: 1; min-height: 0;/);
});
test('white wash stays behind controls; right panel is gray and edges reach 50 percent',()=>{
const css=fs.readFileSync(path.join(__dirname,'../web/styles.css'),'utf8');
assert.match(css,/\.workspace-light \{[^}]*z-index:-1/);
assert.match(css,/\.controls-workspace \{[^}]*isolation:isolate/);
assert.match(css,/\.controls-workspace \{[^}]*background:#ededed/);
assert.match(css,/\.sidebar-deck \{[^}]*background:linear-gradient\(160deg,#333,#242424\)/);
assert.match(css,/\.cursor-light::after \{[^}]*rgba\(255,255,255,\.50\)/);
});
test('keyboard easing publishes intermediate demands to the real Python bridge',async()=>{
const h=harness();h.state();h.run('bridgeReady=true');
keyEvent(h,'keydown','w');keyEvent(h,'keydown','r');
for(let i=0;i<10;i++){h.advance(50);await Promise.resolve();await Promise.resolve();await Promise.resolve();}
const sent=h.calls.filter(call=>call[0]==='axes');
assert.ok(sent.some(call=>call[1]>0&&call[1]<1),'surge ramp is transmitted, not just drawn');
assert.ok(sent.some(call=>call[3]>0&&call[3]<1),'vertical ramp is transmitted, not just drawn');
assert.ok(sent.every(call=>call[1]>=0&&call[1]<=1&&call[3]>=0&&call[3]<=1));
});
test('cursor lighting stays decorative and arrows use centered geometry',()=>{
const css=fs.readFileSync(path.join(__dirname,'../web/styles.css'),'utf8');
const js=fs.readFileSync(path.join(__dirname,'../web/cursor-light.js'),'utf8');
assert.doesNotMatch(js,/set_axes|setJoyControl|setHeave|requestControl/);
assert.match(js,/workspace\.append\(wash\)/);
assert.match(js,/pointerleave',clear/);
assert.match(css,/\.cursor-light \{[^}]*pointer-events:none/);
assert.match(css,/range-pill button \{[^}]*place-items:center/);
assert.match(css,/#engage\.danger \.engage-label \{ color:#777/);
});
test('edge lighting uses angle; only visible pills get blue and sidebar is excluded',()=>{
const css=fs.readFileSync(path.join(__dirname,'../web/styles.css'),'utf8');
const js=fs.readFileSync(path.join(__dirname,'../web/cursor-light.js'),'utf8');
assert.match(js,/Math\.atan2/);
assert.match(js,/!host\.closest\('\.sidebar-deck,#engage'\)/);
assert.match(js,/host\.matches\(pillSelector\)/);
assert.match(css,/\.cursor-light\.pill-light::before \{ background:radial-gradient/);
assert.match(css,/\.cursor-light::after \{[^}]*linear-gradient\(var\(--light-angle/);
assert.doesNotMatch(css,/\.cursor-light::after \{[^}]*radial-gradient/);
assert.match(css,/\.rov-stage \{[^}]*border-radius:31px; overflow:hidden/);
});
test('all interface typography uses Helvetica with inherited control fonts',()=>{
const css=fs.readFileSync(path.join(__dirname,'../web/styles.css'),'utf8');
const families=[...css.matchAll(/font-family\s*:\s*([^;}]+)/g)].map(match=>match[1].trim());
assert.ok(families.includes('"Helvetica Neue", Helvetica, Arial, sans-serif'));
assert.ok(families.every(family=>family==='inherit'||family==='"Helvetica Neue", Helvetica, Arial, sans-serif'));
assert.match(css,/button, select, input \{ font: inherit; \}/);
});
function keyEvent(h,type,key,repeat=false){h.fire(type,{key,repeat,target:{tagName:'BODY'},preventDefault(){}})}
test('keyboard ramps joystick and vertical together, then eases back on release',()=>{
const h=harness();h.state();
keyEvent(h,'keydown','w');keyEvent(h,'keydown','d');keyEvent(h,'keydown','r');
assert.equal(h.run('control.forward+control.turn+control.vertical'),0);
h.advance(180);
for(const axis of ['forward','turn','vertical'])assert.ok(h.run(`control.${axis}`)>0&&h.run(`control.${axis}`)<1);
keyEvent(h,'keydown','w',true);h.advance(240);
assert.ok(Math.abs(h.run('control.forward')-Math.SQRT1_2)<1e-6);
assert.ok(Math.abs(h.run('control.turn')-Math.SQRT1_2)<1e-6);
assert.equal(h.run('control.vertical'),1);
keyEvent(h,'keyup','w');keyEvent(h,'keyup','d');keyEvent(h,'keyup','r');
h.advance(180);assert.ok(h.run('control.forward')>0);assert.ok(h.run('control.vertical')>0);
h.advance(250);assert.equal(h.run('control.forward+control.turn+control.vertical'),0);
});
test('keyboard reversal is continuous and focus loss cancels its ramp',()=>{
const h=harness();h.state();keyEvent(h,'keydown','w');keyEvent(h,'keydown','r');h.advance(420);
keyEvent(h,'keyup','w');keyEvent(h,'keyup','r');keyEvent(h,'keydown','s');keyEvent(h,'keydown','f');
assert.equal(h.run('control.forward'),1);assert.equal(h.run('control.vertical'),1);
h.advance(200);assert.ok(Math.abs(h.run('control.forward'))<.3);assert.ok(Math.abs(h.run('control.vertical'))<.3);
h.advance(220);assert.equal(h.run('control.forward'),-1);assert.equal(h.run('control.vertical'),-1);
keyEvent(h,'keyup','s');keyEvent(h,'keyup','f');h.fire('blur');h.advance(500);
assert.equal(h.run('control.forward+control.vertical'),0);
});
test('touch takes ownership from a keyboard ramp without later keyboard overwrite',()=>{
const h=harness();h.state();keyEvent(h,'keydown','w');keyEvent(h,'keydown','r');h.advance(100);
h.el('#joystick-canvas').emit('pointerdown',{clientX:300,clientY:160});
h.el('#heave').emit('pointerdown',{pointerId:2,clientY:300});
keyEvent(h,'keydown','a');h.advance(450);
assert.equal(h.run('control.turn'),1);assert.equal(Math.abs(h.run('control.forward')),0);assert.equal(h.run('control.vertical'),-1);
});
test('hold fill and pending engagement share the same green and glow',()=>{
const css=fs.readFileSync(path.join(__dirname,'../web/styles.css'),'utf8');
assert.match(css,/\.engage-progress-label \{ color:var\(--engage-green\).*filter:var\(--engage-glow\)/);
assert.match(css,/#engage\.pending \.engage-label \{ color:var\(--engage-green\); filter:var\(--engage-glow\)/);
});
test('native pywebview result wrapper can execute alongside app globals',()=>{
const h=harness();
// Window.evaluate_js declares these variables in the page's global scope.
+9
View File
@@ -112,6 +112,15 @@ class WorkerWatchdogTests(unittest.TestCase):
self.assertNeutral()
self.assertEqual(self.worker.axes, Axes())
def test_intermediate_demands_reach_mavlink_with_output_limit(self):
with patch("app.time.monotonic", return_value=10):
self.worker.last_control_at = 9.9
self.worker.set_axes(Axes(forward=.25, vertical=.25), 1)
self.worker._send_control()
# 25% input at the default 20% limit: 5% command, not 100%.
self.assertEqual(self.worker.connection.mav.manual_control_send.call_args.args,
(1, 50, 0, 525, 0, 0))
def test_heartbeat_freeze_neutralizes_even_with_live_ui(self):
with patch("app.time.monotonic", return_value=13.1):
self.worker.set_axes(Axes(forward=1), 1)