diff --git a/docs/frame-plugin-author.md b/docs/frame-plugin-author.md index fbca363..15ff7a4 100644 --- a/docs/frame-plugin-author.md +++ b/docs/frame-plugin-author.md @@ -63,7 +63,7 @@ none of these frontend controls has passed a worn-headset check yet. | Official `@decky/rollup` JSX (`SP_JSX.jsx`, `jsxs`, `Fragment`) | Creates React elements in the isolated panel | Sample bundle smoke test; host unit test | | `PanelSection`, rows, buttons, toggles, dropdowns | Local touch-sized controls; disabled controls stay inert during a backend call | Local browser and host unit test | | `TextField`, `SliderField`, grouped `Dropdown` options | Native text, range and select controls in the panel; disabled state, numeric slider callback and basic input types | Host unit test; worn VR use unverified | -| Scrollable panel content | Visible Scroll plugin up/down buttons move the last region reached by a trusted pointer, or the document if that region disappears. Plugins may still use ordinary CSS overflow containers. | Parent/iframe message tests; physical controller scroll remains unverified | +| Scrollable panel content | Visible Scroll plugin up/down buttons move the last region reached by a trusted pointer, or the document if that region disappears. Hold either button to repeat; keyboard activation still moves one step. Plugins may use ordinary CSS overflow containers. | Parent/iframe message tests and a real Frame synthetic OpenVR pointer hold; physical controller scroll and hold remain unverified | | `Tabs`, `SidebarNavigation`, spinners, dialog/menu wrappers, progress bars | Local tab navigation and ordinary HTML controls; no Steam navigation or controller focus tree | Host tests and catalog frontend smoke checks in local Chromium | | `SteamUIStore` / `DFL.Router.RunningApps`, `MainRunningApp` | Bounded detached Steam CEF reads while the panel is open; refresh about every two seconds; no MobX observation. Failed reads clear stale context. | Locked real Frame empty-list reads; nonempty local Chromium fixtures. Real running-game selection unverified. | | `GameSessions.RegisterForAppLifetimeNotifications` | Actual Steam callbacks, bounded queue and disposable `.unregister()` handle; no events inferred from snapshot changes; lease cleanup after lost panels | Real Frame registration/poll/close; launch/exit, retry, expiry and duplicate tests use explicit fixtures. Real game transitions unverified. | diff --git a/docs/framey-vr-check.md b/docs/framey-vr-check.md index 1469947..8f74312 100644 --- a/docs/framey-vr-check.md +++ b/docs/framey-vr-check.md @@ -61,8 +61,10 @@ No wheel event or offset change means this synthetic injection path remains unverified; it does not establish whether a physical controller can scroll. The exercise then clicks the visible **Scroll plugin down/up** buttons through the same native OpenVR pointer transport and requires the test region to move -down and return to the top. This proves the button path only; it does not -identify a physical controller or repair native wheel delivery. +down and return to the top. It also holds each button through that transport, +requires Down to move farther than a single click, and requires Up to return +to the top. These prove the synthetic button paths only; they do not identify +a physical controller or repair native wheel delivery. Temporary controls exercise the shipped `DFL.SliderField` and `DFL.TextField`; their values remain UI-only. Text uses CDP `Input.insertText` after OpenVR pointer focus. This is synthetic input, @@ -119,7 +121,9 @@ the same-plugin navigation regression without a headset. 3. **1:15–2:15:** Use the controller to open Settings, return to Panel, invoke **Read saved setting**, close/reopen the plugin, and confirm the value remains. Try controller scroll, a slider and a harmless text field in an - installed compatible plugin; check SteamVR's actual keyboard and Back. + installed compatible plugin. Hold **Scroll plugin down** and then **Scroll + plugin up** with the controller; record continuous movement separately from + native thumbstick or wheel scrolling. Check SteamVR's actual keyboard and Back. 4. **2:15–3:00:** Restore the original sample setting. Switch away and back, close/reopen the Frame Control panel, and confirm controller input still works. Record comfort/legibility, controller model, any haptic behavior, diff --git a/scripts/framey_vr_input.py b/scripts/framey_vr_input.py index bcbf7b3..c99ac31 100644 --- a/scripts/framey_vr_input.py +++ b/scripts/framey_vr_input.py @@ -71,13 +71,19 @@ def exercise(profile, plugin_id, folder): if not hit: raise RuntimeError('Native click hit another element: ' + str(panel.eval('({tag:__vrLastTarget?.tagName,text:__vrLastTarget?.textContent?.slice(0,100)})'))) - def click_parent(selector): + def parent_point(selector): parent.eval(f'document.querySelector({json.dumps(selector)}).scrollIntoView({{block:"center"}})') time.sleep(.3) pos = parent.eval(f'''(()=>{{let r=document.querySelector({json.dumps(selector)}).getBoundingClientRect(); return {{x:r.x+r.width/2,y:r.y+r.height/2,w:innerWidth,h:innerHeight}}}})()''') sx, sy = vr.snapshot()['input']['scale'] x, y = pos['x'] * sx / pos['w'], sy - pos['y'] * sy / pos['h'] + if not 0 <= x <= sx or not 0 <= y <= sy: + raise RuntimeError('Parent control is outside the visible viewport') + return x, y + + def click_parent(selector): + x, y = parent_point(selector) count = parent.eval('window.__vrParentClickCount') vr.post_mouse(300, x, y) vr.post_mouse(301, x, y, 1) @@ -86,6 +92,21 @@ def exercise(profile, plugin_id, folder): if not parent.eval(f'!!window.__vrParentLastTarget?.closest({json.dumps(selector)})'): raise RuntimeError('Native click missed parent control ' + selector) + def hold_parent(selector, seconds): + x, y = parent_point(selector) + count = parent.eval('window.__vrParentClickCount') + vr.post_mouse(300, x, y) + try: + vr.post_mouse(301, x, y, 1) + time.sleep(seconds) + offset = panel.eval('document.getElementById("vr-scroll-probe").scrollTop') + finally: + vr.post_mouse(302, x, y, 1) + wait(lambda:parent.eval('window.__vrParentClickCount'), lambda n:n > count, seconds=3) + if not parent.eval(f'!!window.__vrParentLastTarget?.closest({json.dumps(selector)})'): + raise RuntimeError('Native hold missed parent control ' + selector) + return offset + try: parent.eval(f'(async()=>{{await refreshDecky();openFrameyPanel({plugin_id},"Framey VR Check");return true}})()') wait(lambda:parent.eval('byId("frameyPanelStatus").innerText'), lambda s:s == 'Plugin panel ready') @@ -172,6 +193,10 @@ def exercise(profile, plugin_id, folder): result['buttonScrollAfterUp'] = wait( lambda:panel.eval('document.getElementById("vr-scroll-probe").scrollTop'), lambda n:n == 0) result['buttonScrollMoved'] = result['buttonScrollAfterDown'] > 0 and result['buttonScrollAfterUp'] == 0 + result['holdScrollAfterDown'] = hold_parent('#frameyScrollDown', 1.2) + result['holdScrollAfterUp'] = hold_parent('#frameyScrollUp', 2.0) + result['holdScrollMoved'] = (result['holdScrollAfterDown'] > result['buttonScrollAfterDown'] + and result['holdScrollAfterUp'] == 0) panel.eval('document.getElementById("vr-scroll-probe").remove()') panel.eval('document.getElementById("vr-controls").remove()') # Scroll saved status into the visible iframe for compositor evidence. @@ -179,7 +204,8 @@ def exercise(profile, plugin_id, folder): result['finalOverlay'] = vr.snapshot() result['passed'] = (result['trustedOverlayPointer'] and result['diskSetting'] == {'enabled':True} and result['reopenedChecked'] and result['sliderValue'] != '10' - and result['textValue'] == 'framey-ci' and result['buttonScrollMoved']) + and result['textValue'] == 'framey-ci' and result['buttonScrollMoved'] + and result['holdScrollMoved']) return result except Exception as exc: result['error'] = str(exc) diff --git a/tests/page/framey_event_parent.mjs b/tests/page/framey_event_parent.mjs index 2962bf5..dc70915 100644 --- a/tests/page/framey_event_parent.mjs +++ b/tests/page/framey_event_parent.mjs @@ -5,6 +5,7 @@ import vm from 'node:vm'; const html=readFileSync(new URL('../../ui/headset.html',import.meta.url),'utf8'); const source=html.match(/