diff --git a/docs/evidence/framey-events-backend-2026-10-26.json b/docs/evidence/framey-events-backend-2026-10-26.json new file mode 100644 index 0000000..f11646b --- /dev/null +++ b/docs/evidence/framey-events-backend-2026-10-26.json @@ -0,0 +1,418 @@ +{ + "checkedAtUtc": "2026-10-26T00:12:47.296778+00:00", + "method": "Local Chromium actual Framey headset page and opaque official plugin iframe; production method/event bridge relayed by SSH to real ARM64 Decky 3.3.0 in a private removable user service. Test relay restricts RPC methods and timer durations. Backend code is unmodified, no fake plugin replies. Only file paths to private install and outer status/data APIs differ. Not worn VR.", + "decky": { + "version": "v3.3.0", + "sha256": "8ef5dcf36cbbbb6e97201a6baed5d79c4cc44ec27789fc9af1c5b6ae8f0b25b6" + }, + "hostSourceSha256": { + "framey-host.js": "3bad52546c0b78c20e6479c91899f1f3864b74687c6fce37e776abededfb5f1c", + "framey-events.js": "89046f4c89bdab0f578b910170a044d5ada540db0f7b71d6a370c68b4b008c3f", + "framey_panel.py": "8ca4b3ba5f0d51c51bb3601f9ccd24c4af397fbf8f63c383a3042e1ae3f7b39b", + "headset.html": "ebaf1711639a929ec7287c01dbbbe8eb08f000b852fdc877f31c9d99e5fe69fc" + }, + "before": { + "battery": [ + 100 + ], + "decky": "inactive", + "panel": "inactive" + }, + "after": { + "battery": [ + 100 + ], + "decky": "inactive", + "panel": "inactive" + }, + "powerSettingsChanged": false, + "plugins": [ + { + "id": 19, + "name": "Notebook", + "version": "0.1.5", + "sha256": "c35b09de4bba4b4157fbe84cb7b0d20fc3924f78fa3c48ae17686b4314c1cc0c", + "calls": [ + { + "id": 19, + "method": "listPages", + "args": [ + { + "appid": 0 + } + ], + "result": { + "result": { + "result": [], + "success": true + } + } + }, + { + "id": 19, + "method": "loadLastSelectedPage", + "args": [ + { + "appid": 0 + } + ], + "result": { + "result": { + "result": 0, + "success": true + } + } + }, + { + "id": 19, + "method": "loadPage", + "args": [ + { + "appid": 0, + "page": 0 + } + ], + "result": { + "result": { + "result": { + "page": 0, + "timestamp": 0, + "empty": true, + "data": "" + }, + "success": true + } + } + }, + { + "id": 19, + "method": "savePage", + "args": [ + { + "appid": 0, + "page": 0, + "data": "[{\"drawMode\":true,\"strokeColor\":\"white\",\"strokeWidth\":4,\"paths\":[{\"x\":583.3984375,\"y\":155.997802734375}]}]" + } + ], + "result": { + "result": { + "result": { + "timestamp": 1792973680303 + }, + "success": true + } + } + }, + { + "id": 19, + "method": "listPages", + "args": [ + { + "appid": 0 + } + ], + "result": { + "result": { + "result": [ + { + "page": 0, + "timestamp": 1792973680303, + "empty": false + } + ], + "success": true + } + } + }, + { + "id": 19, + "method": "loadLastSelectedPage", + "args": [ + { + "appid": 0 + } + ], + "result": { + "result": { + "result": 0, + "success": true + } + } + }, + { + "id": 19, + "method": "loadPage", + "args": [ + { + "appid": 0, + "page": 0 + } + ], + "result": { + "result": { + "result": { + "page": 0, + "timestamp": 1792973680303, + "empty": false, + "data": "[{\"drawMode\":true,\"strokeColor\":\"white\",\"strokeWidth\":4,\"paths\":[{\"x\":583.3984375,\"y\":155.997802734375}]}]" + }, + "success": true + } + } + } + ], + "events": [], + "checks": { + "pointerStrokeSaved": true, + "sameStrokeLoadedAfterBackendRestart": true, + "defaultAppId": 0, + "existingNotesTouched": false + } + }, + { + "id": 96, + "name": "Simple Timer", + "version": "1.0.0", + "sha256": "973d927e9d9a1feec04679c564af7e389a729b6735d2a988ffc2cb46ce4eb312", + "calls": [ + { + "id": 96, + "method": "load_remaining_seconds", + "args": [], + "result": { + "result": null + } + }, + { + "id": 96, + "method": "load_subtle_mode", + "args": [], + "result": { + "result": null + } + }, + { + "id": 96, + "method": "load_recents", + "args": [], + "result": { + "result": null + } + }, + { + "id": 96, + "method": "start_timer", + "args": [ + 300 + ], + "result": { + "result": null + } + }, + { + "id": 96, + "method": "cancel_timer", + "args": [], + "result": { + "result": null + } + }, + { + "id": 96, + "method": "start_timer", + "args": [ + 5 + ], + "result": { + "result": null + } + }, + { + "id": 96, + "method": "load_remaining_seconds", + "args": [], + "result": { + "result": null + } + }, + { + "id": 96, + "method": "load_recents", + "args": [], + "result": { + "result": null + } + }, + { + "id": 96, + "method": "load_subtle_mode", + "args": [], + "result": { + "result": null + } + } + ], + "events": [ + { + "sequence": 1, + "event": "simple_timer_seconds_updated", + "args": [ + 0 + ] + }, + { + "sequence": 2, + "event": "simple_timer_subtle", + "args": [ + true + ] + }, + { + "sequence": 3, + "event": "simple_timer_refresh_recents", + "args": [ + [] + ] + }, + { + "sequence": 4, + "event": "simple_timer_refresh_recents", + "args": [ + [ + 300 + ] + ] + }, + { + "sequence": 5, + "event": "simple_timer_seconds_updated", + "args": [ + 300 + ] + }, + { + "sequence": 6, + "event": "simple_timer_seconds_updated", + "args": [ + 294.9945251941681 + ] + }, + { + "sequence": 7, + "event": "simple_timer_seconds_updated", + "args": [ + 289.99201130867004 + ] + }, + { + "sequence": 8, + "event": "simple_timer_seconds_updated", + "args": [ + 284.9877219200134 + ] + }, + { + "sequence": 9, + "event": "simple_timer_seconds_updated", + "args": [ + 0 + ] + }, + { + "sequence": 10, + "event": "simple_timer_refresh_recents", + "args": [ + [ + 5, + 300 + ] + ] + }, + { + "sequence": 11, + "event": "simple_timer_seconds_updated", + "args": [ + 5 + ] + }, + { + "sequence": 12, + "event": "simple_timer_seconds_updated", + "args": [ + -0.005679607391357422 + ] + }, + { + "sequence": 13, + "event": "simple_timer_seconds_updated", + "args": [ + 0 + ] + }, + { + "sequence": 14, + "event": "simple_timer_event", + "args": [ + "Your session has ended!", + true + ] + }, + { + "sequence": 1, + "event": "simple_timer_seconds_updated", + "args": [ + 0 + ] + }, + { + "sequence": 2, + "event": "simple_timer_refresh_recents", + "args": [ + [ + 5, + 300 + ] + ] + }, + { + "sequence": 3, + "event": "simple_timer_subtle", + "args": [ + true + ] + } + ], + "checks": { + "uiStart300Seconds": true, + "uiCancel": true, + "shortTimer5Seconds": true, + "completionToastVisible": true, + "recentTimersAndSubtleModeSurvivedRestart": true, + "subscriptionsAfterPanelClose": 0 + }, + "upstreamError": "Simple Timer 1.0.0 _main after restarting with an expired timer raises AttributeError: 'Plugin' object has no attribute 'timer_task' (main.py:94,129). Frontend read requests still load recent timers and subtle mode." + } + ], + "cleanup": { + "baseAbsent": true, + "unitAbsent": true, + "serviceActive": false, + "independentlyRechecked": true, + "lockReleased": true + }, + "regressions": { + "unitTests": 1606, + "unitSkips": 16, + "browserPlugins": 116, + "browserReady": 96, + "browserFailed": 20, + "fakeFrameInstalled": 60, + "fakeFrameRejected": 56 + }, + "limitations": [ + "Timer sound asset and suspend action unsupported; no suspend/power action run.", + "Closing a Framey panel removes its event channel; it is not a global background notification service.", + "Notebook default notebook only (appid 0): no per-game auto-selection or background artwork.", + "No worn VR, physical controller, keyboard, or optical check." + ] +} diff --git a/docs/frame-plugin-author.md b/docs/frame-plugin-author.md index 205f81c..c2e235e 100644 --- a/docs/frame-plugin-author.md +++ b/docs/frame-plugin-author.md @@ -67,7 +67,8 @@ none of these frontend controls has passed a worn-headset check yet. | `SteamClient` subscriptions and `DFL.Router.MainRunningApp` | Explicit inert registration methods return disposable handles; no events or attached running game. Mutating Steam/power methods are unavailable. | Host contract tests; these are safe fallbacks, not live Steam functionality | | `DialogCheckbox`, `DialogButtonPrimary` | Native checkbox and button controls in the panel | Host unit test; Storage Cleaner empty-data browser check | | Optional `settings`, `frameyQuickActions` | Separate settings tab; up to eight actions with result text | Host unit test; these fields are Framey additions | -| `serverAPI.call` / `callable` | Calls only that plugin's Decky Python backend via the headset service | Fake Frame e2e for setting read/write across CEF calls, update, and reinstall; host unit test. Real Decky persistence remains unverified. | +| `serverAPI.call` / `callable` | Calls only that plugin's Decky Python backend via the headset service | Fake Frame e2e; real ARM64 Notebook save/reopen across backend restart and Simple Timer settings/recents persistence via local Chromium. No worn-VR claim. | +| API v1/v2 `addEventListener` / `removeEventListener` | Plugin-scoped real backend events while panel is open; bounded queue, about one-second polling, subscription expiry | Real ARM64 Simple Timer countdown, start/cancel and completion toast via local Chromium; scope/bounds/reconnect tests. No closed-panel notifications. | | `toaster.toast` | Visible toast inside the plugin panel; plain text or React nodes for title, body, icon, logo and subtext; `dismiss` and optional `onClick` | Host unit test; no Steam system notification or worn VR check | | `localStorage` | Synchronous, per-plugin cache backed by the Frame Control panel origin. Up to 64 keys, 64 KiB per value and 128 KiB total; `clear()` removes only this plugin's cache. Falls back to memory if panel storage is denied. | Parent/iframe round-trip and reopen in the local host test; real headset persistence remains unverified. | | IndexedDB / localForage | IndexedDB is deliberately absent inside the opaque iframe so feature detection chooses the bounded localStorage driver. Plugins requiring IndexedDB directly remain unsupported. | HLTB official frontend in local Chromium: its previous `IDBFactory.open` SecurityError disappeared. No live HLTB data request was run. | @@ -75,7 +76,7 @@ none of these frontend controls has passed a worn-headset check yet. | `routerHook.addPatch`, global components | No-op; cannot patch Steam routes | Not applicable | | `findModuleChild`, `Navigation.CloseSideMenus` | Steam module lookup returns `null`; close returns to this plugin's main panel | Pinned HLTB for Deck 2.0.12 entry point and shallow settings view in local host test. Steam library patch and live HLTB search remain unverified. | | Steam library/store patches, CEF tab injection | Unavailable; no matching target in the isolated panel | Frame CEF inspection | -| `fetchNoCors`, Decky event listeners, file picker, root calls | Unavailable | Not supported by Framey | +| `fetchNoCors`, file picker, root calls | Unavailable | Not supported by Framey | Keep routes local to the panel and give every operation a visible result. A plugin that calls `Navigation.Navigate('/decky/store')` will not open the diff --git a/docs/framey-compatibility.md b/docs/framey-compatibility.md index d96a4c7..0b08ac5 100644 --- a/docs/framey-compatibility.md +++ b/docs/framey-compatibility.md @@ -4,9 +4,9 @@ Snapshot: 2026-10-25T23:19:48.854504+00:00; 116 current visible official plugins This table and both store views use [`ui/framey-compatibility.json`](../ui/framey-compatibility.json). Results apply only to the exact store ID, version and SHA-256. A changed digest/version is **unverified**. Regenerate this document with `python3 scripts/framey-compatibility-doc.py`; use `--check` in review. -**Works: 0 · Partial: 43 · Broken: 62 · N/A: 11.** +**Works: 1 · Partial: 42 · Broken: 62 · N/A: 11.** -- **Works** requires verified useful plugin behavior with its real backend. None qualifies from this audit. +- **Works** requires verified useful plugin behavior with its real backend. Read the reason for the verified scope and remaining limits; it does not imply worn-VR verification. - **Partial** means a limited frontend/settings surface initialized or rendered. Backend-dependent functionality remains unverified; it is not an end-to-end success claim. - **Broken** means a concrete archive, frontend or Steam dependency blocks the audited version. An artificial backend/network denial alone is not evidence that the real backend is broken. - **N/A** means the plugin targets hardware or a display/UI surface that Framey does not provide. The store disables installation for these versions. @@ -27,7 +27,7 @@ Open the browser audit URL in local Chromium, run `runAudit()` and inspect/downl The current published digests are pinned in the JSON; if the store changed, retain/download those exact archives for reproduction. Audit downloads permit larger ZIPs for inspection; production installer limits remain 50 MiB ZIP / 150 MiB unpacked / 30 MiB per member. -After the VR job released the shared lock, this audit acquired it and used a private temporary Decky 3.3.0 ARM64 user service. Read-only ProtonDB `get_setting` and Steam Status `get_settings` calls succeeded through the live Steam CEF websocket. The service and private base were removed and the lock released; normal managed services stayed inactive, battery was 100 → 99, and no power setting was changed. [Redacted results](evidence/framey-official-backend-2026-10-25.json) record exactly what ran. All other official backends remain unexecuted. The separate VR job proves Frame Control/sample compositor rendering, not any official plugin. No worn-VR official-plugin result is claimed. Earlier focused checks cover [ProtonDB](evidence/framey-browser-2026-10-25.md), [Steam Status/cache](evidence/framey-cache-browser-2026-10-25.md), [Storage Cleaner](evidence/framey-api-v1-browser-2026-10-25.md), and [legacy MusicControl](evidence/framey-legacy-2026-10-25.md). +After the VR job released the shared lock, this audit acquired it and used a private temporary Decky 3.3.0 ARM64 user service. Read-only ProtonDB `get_setting` and Steam Status `get_settings` calls succeeded through the live Steam CEF websocket. The service and private base were removed and the lock released; normal managed services stayed inactive, battery was 100 → 99, and no power setting was changed. [Redacted results](evidence/framey-official-backend-2026-10-25.json) record exactly what ran. A [2026-10-26 follow-up](evidence/framey-events-backend-2026-10-26.json) verified default Notebook drawing/save/reopen across a real backend restart and Simple Timer start/cancel, countdown/completion events, a visible toast and persisted recents/subtle mode. Only private test settings were written. Simple Timer remains partial because its own expired-timer startup raises a missing `timer_task` error; sound/suspend are unsupported. All other official backends remain unexecuted. The separate VR job proves Frame Control/sample compositor rendering, not any official plugin. No worn-VR official-plugin result is claimed. Earlier focused checks cover [ProtonDB](evidence/framey-browser-2026-10-25.md), [Steam Status/cache](evidence/framey-cache-browser-2026-10-25.md), [Storage Cleaner](evidence/framey-api-v1-browser-2026-10-25.md), and [legacy MusicControl](evidence/framey-legacy-2026-10-25.md). ## Every current plugin @@ -44,7 +44,7 @@ The API column shows manifest / actual bundled frontend API. Many manifests say | 14: ProtonDB Badges 1.3.4 | 2 / 2 | partial | Settings render locally and read-only get_setting RPC returned the default through real Decky 3.3.0 on ARM64 Frame. Steam game-page badges have no patch target; live writes and VR controls unverified. | static archive, local browser, fake-Frame, real Frame SSH | | 16: Audio Loader 1.6.1 | legacy / legacy | broken | This plugin needs root and is not supported by the Frame user service. Frontend audit failed: Cannot read properties of null (reading 'm_GamepadUIAudioStore'). | static archive, local browser, fake-Frame | | 18: Bluetooth 3.1.1 | 1 / 2 | partial | Controls render with fixture replies; actual Bluetooth discovery, labels and connections remain unverified. | static archive, local browser, fake-Frame | -| 19: Notebook 0.1.5 | legacy / legacy | partial | Notebook initializes; real note load/save remains unverified. | static archive, local browser, fake-Frame | +| 19: Notebook 0.1.5 | legacy / legacy | works | Default notebook drawing, save and reopen verified in Chromium with real Decky 3.3.0 ARM64, including backend restart. Per-game auto-selection and background artwork unavailable; worn-VR input unverified. | static archive, local browser, fake-Frame, real Frame SSH | | 21: PowerTools 2.0.3 | legacy / legacy | N/A | Not supported on Frame: Targets AMD Ryzen power controls; Frame uses an ARM64 SoC. | static archive, local browser, fake-Frame | | 22: Memory Deck 0.1.8 | legacy / legacy | broken | Plugin contains a non-ARM64 Linux binary: memory-deck/bin/libscanmem.so.1.0.0. | static archive, local browser, fake-Frame | | 23: Animation Changer 1.3.2 | legacy / legacy | partial | Initializer succeeds; forced backend failure prevents loading animations. Boot-video changes untested. | static archive, local browser, fake-Frame | @@ -102,7 +102,7 @@ The API column shows manifest / actual bundled frontend API. Many manifests say | 93: Deck Settings 1.2.6 | 1 / 2 | broken | Initializer succeeds; game selection fails because SteamClient.InstallFolder.GetInstallFolders is unavailable. | static archive, local browser, fake-Frame | | 94: Decky DoT DNS 0.0.2 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame | | 95: Decky-SpoofDPI 0.12.0 | 1 / 2 | broken | Plugin contains a non-ARM64 Linux binary: decky-spoof-dpi/bin/spoofdpi. | static archive, local browser, fake-Frame | -| 96: Simple Timer 1.0.0 | 1 / 2 | partial | Timer panel initializes; live timer persistence, notifications and audio are unverified. | static archive, local browser, fake-Frame | +| 96: Simple Timer 1.0.0 | 1 / 2 | partial | Live start/cancel, countdown events, completion toast, persisted recents/subtle mode verified in Chromium with real ARM64 Decky. Upstream expired-timer restart raises missing timer_task. Audio/suspend unsupported; alerts require an open panel; worn VR unverified. | static archive, local browser, fake-Frame, real Frame SSH | | 97: Decky-Framegen 0.11.15 | 1 / 2 | broken | Plugin archive size or SHA-256 did not match the store. | static archive, local browser, fake-Frame | | 98: Decky-Lookup 0.1.0 | 1 / 2 | partial | Initializer succeeds; game-page context actions and live lookups remain unverified. | static archive, local browser, fake-Frame | | 99: Decky Zerotier 0.3.1 | 1 / 2 | broken | This plugin needs root and is not supported by the Frame user service. | static archive, local browser, fake-Frame | diff --git a/docs/framey-loader.md b/docs/framey-loader.md index 17ef038..b4e17f5 100644 --- a/docs/framey-loader.md +++ b/docs/framey-loader.md @@ -51,6 +51,25 @@ restores the cache when a panel reopens if the parent's browser storage is available. If storage is denied or cleared, the cache lasts only for the current panel. Use the plugin backend for settings that must survive cache loss. +Decky API v1/v2 `addEventListener` and `removeEventListener` receive real backend +events while the panel is open. The parent resolves the plugin name from its +installed store receipt, polls Steam's existing Decky event connection, and sends +only subscribed events to that panel session. A subscription is established +before a backend call can emit its response. The bridge allows 32 event names, +queues at most 64 events and 48 KiB, and drops any event whose arguments exceed +8 KiB. Polling occurs about once a second, so this is unsuitable for high-rate +input/audio streams. Closing/reopening a panel removes its subscriptions; +abandoned CEF subscriptions expire after 15 seconds. This does not deliver +global notifications while a Framey panel is closed. + +The [26 October live checks](evidence/framey-events-backend-2026-10-26.json) +used the actual official Notebook and Simple Timer Python backends in a private +removable ARM64 Decky service. Notebook's default notebook saved a pointer stroke +and loaded it after a backend restart. Simple Timer's start/cancel controls, +countdown, completion toast and persisted recents/subtle mode worked. Simple +Timer remains partial: its own expired-timer startup raises a missing +`timer_task` error, sound/suspend are unsupported, and worn VR was not checked. + The host currently supplies sections, rows, buttons, toggles, text fields, sliders, grouped dropdowns, scroll containers, a modal, locale information, local panel routes and toasts, diff --git a/scripts/framey-audit-browser.py b/scripts/framey-audit-browser.py index 6f3e215..100a085 100644 --- a/scripts/framey-audit-browser.py +++ b/scripts/framey-audit-browser.py @@ -44,6 +44,7 @@ addEventListener('message',event=>{ if(data.type==='audit:log'&&active.logs.length<8)active.logs.push({kind:data.kind,message:data.message.split('\\n')[0].slice(0,400)}); if(data.type==='framey:error')active.error=String(data.message).slice(0,500); if(data.type==='framey:ready')active.ready=true; + if(data.type==='framey:events')event.source.postMessage({type:'framey:reply',id:data.id,result:{events:[],cursor:0}},'*'); if(data.type==='framey:call'){ if(!active.methods.includes(data.method))active.methods.push(data.method); event.source.postMessage({type:'framey:reply',id:data.id,error:'Read-only compatibility audit: backend method blocked'},'*');} diff --git a/scripts/framey-compatibility-doc.py b/scripts/framey-compatibility-doc.py index b6f3326..4875d54 100644 --- a/scripts/framey-compatibility-doc.py +++ b/scripts/framey-compatibility-doc.py @@ -22,7 +22,7 @@ def render(data): 'Results apply only to the exact store ID, version and SHA-256. A changed digest/version is **unverified**. ' 'Regenerate this document with `python3 scripts/framey-compatibility-doc.py`; use `--check` in review.', '', f"**Works: {counts['works']} · Partial: {counts['partial']} · Broken: {counts['broken']} · N/A: {counts['na']}.**", '', - '- **Works** requires verified useful plugin behavior with its real backend. None qualifies from this audit.', + '- **Works** requires verified useful plugin behavior with its real backend. Read the reason for the verified scope and remaining limits; it does not imply worn-VR verification.', '- **Partial** means a limited frontend/settings surface initialized or rendered. Backend-dependent functionality remains unverified; it is not an end-to-end success claim.', '- **Broken** means a concrete archive, frontend or Steam dependency blocks the audited version. An artificial backend/network denial alone is not evidence that the real backend is broken.', '- **N/A** means the plugin targets hardware or a display/UI surface that Framey does not provide. The store disables installation for these versions.', '', @@ -46,7 +46,12 @@ def render(data): 'Read-only ProtonDB `get_setting` and Steam Status `get_settings` calls succeeded through the live Steam CEF websocket. ' 'The service and private base were removed and the lock released; normal managed services stayed inactive, battery was 100 → 99, ' 'and no power setting was changed. [Redacted results](evidence/framey-official-backend-2026-10-25.json) ' - 'record exactly what ran. All other official backends remain unexecuted. ' + 'record exactly what ran. A [2026-10-26 follow-up](evidence/framey-events-backend-2026-10-26.json) ' + 'verified default Notebook drawing/save/reopen across a real backend restart and Simple Timer start/cancel, ' + 'countdown/completion events, a visible toast and persisted recents/subtle mode. ' + 'Only private test settings were written. Simple Timer remains partial because its own expired-timer startup ' + 'raises a missing `timer_task` error; sound/suspend are unsupported. ' + 'All other official backends remain unexecuted. ' 'The separate VR job proves Frame Control/sample compositor rendering, not any official plugin. ' 'No worn-VR official-plugin result is claimed. ' 'Earlier focused checks cover [ProtonDB](evidence/framey-browser-2026-10-25.md), ' diff --git a/tests/page/framey_actions.mjs b/tests/page/framey_actions.mjs index f419f27..8dd5fe0 100644 --- a/tests/page/framey_actions.mjs +++ b/tests/page/framey_actions.mjs @@ -206,3 +206,35 @@ assert.throws(()=>api.routerHook.addRoute('/plugin/:id?/art',GameRoute)); removeGame(); assert.equal(window.DFL.useParams().appid,undefined); assert.equal(nodes(window.__surface({plugin}),GameRoute).length,0); + +// Event subscription is acknowledged before a method that immediately emits. +const onMessage=listeners.find(x=>x.type==='message').handler; +const eventValues=[]; +const callback=(...args)=>eventValues.push(args); +assert.equal(api.addEventListener('timer:tick',callback),callback); +const subscribe=messages.at(-1); +assert.equal(subscribe.type,'framey:events'); +assert.deepEqual(Array.from(subscribe.args[0]),['timer:tick']); +const before=messages.length; +const timerCall=api.call('load_remaining_seconds'); +assert.equal(messages.length,before); +// Other windows cannot inject backend replies. +onMessage({source:{},data:{type:'framey:reply',id:subscribe.id,result:{events:[],cursor:0}}}); +await Promise.resolve();assert.equal(messages.length,before); +onMessage({source:parent,data:{type:'framey:reply',id:subscribe.id, + result:{events:[{event:'timer:tick',args:[12,'seconds']}],cursor:1}}}); +for(let i=0;i<12;i++)await Promise.resolve(); +assert.deepEqual(eventValues,[[12,'seconds']]); +const timerRequest=messages.at(-1); +assert.equal(timerRequest.type,'framey:call');assert.equal(timerRequest.method,'load_remaining_seconds'); +onMessage({source:parent,data:{type:'framey:reply',id:timerRequest.id,result:null}});await timerCall; +api.removeEventListener('timer:tick',callback); +const unsubscribeRequest=messages.at(-1);assert.equal(unsubscribeRequest.type,'framey:events'); +assert.equal(unsubscribeRequest.args[0].length,0); +onMessage({source:parent,data:{type:'framey:reply',id:unsubscribeRequest.id, + result:{events:[{event:'timer:tick',args:[99]}],cursor:2}}}); +for(let i=0;i<12;i++)await Promise.resolve();assert.equal(eventValues.length,1); +assert.throws(()=>api.addEventListener('bad event',callback),/Invalid Decky event/); +assert.throws(()=>api.addEventListener('tick',null),/Invalid Decky event/); +listeners.find(x=>x.type==='unload').handler(); +assert.equal(messages.at(-1).type,'framey:events:close'); diff --git a/tests/page/framey_event_parent.mjs b/tests/page/framey_event_parent.mjs new file mode 100644 index 0000000..5a86abc --- /dev/null +++ b/tests/page/framey_event_parent.mjs @@ -0,0 +1,49 @@ +import assert from 'node:assert/strict'; +import {readFileSync} from 'node:fs'; +import vm from 'node:vm'; + +const html=readFileSync(new URL('../../ui/headset.html',import.meta.url),'utf8'); +const source=html.match(/