@ledgerhq/device-mockserver-react-core 0.0.0-develop-20261104000847
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.MD +202 -0
- package/README.md +29 -0
- package/lib/cjs/index.js +2 -0
- package/lib/cjs/index.js.map +7 -0
- package/lib/cjs/package.json +61 -0
- package/lib/cjs/src/deviceModels.js +2 -0
- package/lib/cjs/src/deviceModels.js.map +7 -0
- package/lib/cjs/src/deviceModels.test.js +2 -0
- package/lib/cjs/src/deviceModels.test.js.map +7 -0
- package/lib/cjs/src/index.js +2 -0
- package/lib/cjs/src/index.js.map +7 -0
- package/lib/cjs/src/mockServerScreenApi.js +2 -0
- package/lib/cjs/src/mockServerScreenApi.js.map +7 -0
- package/lib/cjs/src/mockServerScreenApi.test.js +2 -0
- package/lib/cjs/src/mockServerScreenApi.test.js.map +7 -0
- package/lib/cjs/src/types.js +2 -0
- package/lib/cjs/src/types.js.map +7 -0
- package/lib/cjs/src/useButtonHold.js +2 -0
- package/lib/cjs/src/useButtonHold.js.map +7 -0
- package/lib/cjs/src/useButtonHold.test.js +2 -0
- package/lib/cjs/src/useButtonHold.test.js.map +7 -0
- package/lib/cjs/src/useHold.js +2 -0
- package/lib/cjs/src/useHold.js.map +7 -0
- package/lib/cjs/src/useMockServerDevice.js +2 -0
- package/lib/cjs/src/useMockServerDevice.js.map +7 -0
- package/lib/cjs/src/useMockServerDevice.test.js +2 -0
- package/lib/cjs/src/useMockServerDevice.test.js.map +7 -0
- package/lib/cjs/src/useOpenApp.js +2 -0
- package/lib/cjs/src/useOpenApp.js.map +7 -0
- package/lib/cjs/src/useOpenApp.test.js +2 -0
- package/lib/cjs/src/useOpenApp.test.js.map +7 -0
- package/lib/cjs/src/useScreenPolling.js +2 -0
- package/lib/cjs/src/useScreenPolling.js.map +7 -0
- package/lib/cjs/src/useScreenPolling.test.js +2 -0
- package/lib/cjs/src/useScreenPolling.test.js.map +7 -0
- package/lib/cjs/src/useTouchHold.js +2 -0
- package/lib/cjs/src/useTouchHold.js.map +7 -0
- package/lib/cjs/src/useTouchHold.test.js +2 -0
- package/lib/cjs/src/useTouchHold.test.js.map +7 -0
- package/lib/esm/index.js +2 -0
- package/lib/esm/index.js.map +7 -0
- package/lib/esm/package.json +61 -0
- package/lib/esm/src/deviceModels.js +2 -0
- package/lib/esm/src/deviceModels.js.map +7 -0
- package/lib/esm/src/deviceModels.test.js +2 -0
- package/lib/esm/src/deviceModels.test.js.map +7 -0
- package/lib/esm/src/index.js +2 -0
- package/lib/esm/src/index.js.map +7 -0
- package/lib/esm/src/mockServerScreenApi.js +2 -0
- package/lib/esm/src/mockServerScreenApi.js.map +7 -0
- package/lib/esm/src/mockServerScreenApi.test.js +2 -0
- package/lib/esm/src/mockServerScreenApi.test.js.map +7 -0
- package/lib/esm/src/types.js +1 -0
- package/lib/esm/src/types.js.map +7 -0
- package/lib/esm/src/useButtonHold.js +2 -0
- package/lib/esm/src/useButtonHold.js.map +7 -0
- package/lib/esm/src/useButtonHold.test.js +2 -0
- package/lib/esm/src/useButtonHold.test.js.map +7 -0
- package/lib/esm/src/useHold.js +2 -0
- package/lib/esm/src/useHold.js.map +7 -0
- package/lib/esm/src/useMockServerDevice.js +2 -0
- package/lib/esm/src/useMockServerDevice.js.map +7 -0
- package/lib/esm/src/useMockServerDevice.test.js +2 -0
- package/lib/esm/src/useMockServerDevice.test.js.map +7 -0
- package/lib/esm/src/useOpenApp.js +2 -0
- package/lib/esm/src/useOpenApp.js.map +7 -0
- package/lib/esm/src/useOpenApp.test.js +2 -0
- package/lib/esm/src/useOpenApp.test.js.map +7 -0
- package/lib/esm/src/useScreenPolling.js +2 -0
- package/lib/esm/src/useScreenPolling.js.map +7 -0
- package/lib/esm/src/useScreenPolling.test.js +2 -0
- package/lib/esm/src/useScreenPolling.test.js.map +7 -0
- package/lib/esm/src/useTouchHold.js +2 -0
- package/lib/esm/src/useTouchHold.js.map +7 -0
- package/lib/esm/src/useTouchHold.test.js +2 -0
- package/lib/esm/src/useTouchHold.test.js.map +7 -0
- package/lib/types/index.d.ts +2 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/src/deviceModels.d.ts +12 -0
- package/lib/types/src/deviceModels.d.ts.map +1 -0
- package/lib/types/src/deviceModels.test.d.ts +2 -0
- package/lib/types/src/deviceModels.test.d.ts.map +1 -0
- package/lib/types/src/index.d.ts +10 -0
- package/lib/types/src/index.d.ts.map +1 -0
- package/lib/types/src/mockServerScreenApi.d.ts +3 -0
- package/lib/types/src/mockServerScreenApi.d.ts.map +1 -0
- package/lib/types/src/mockServerScreenApi.test.d.ts +2 -0
- package/lib/types/src/mockServerScreenApi.test.d.ts.map +1 -0
- package/lib/types/src/types.d.ts +60 -0
- package/lib/types/src/types.d.ts.map +1 -0
- package/lib/types/src/useButtonHold.d.ts +6 -0
- package/lib/types/src/useButtonHold.d.ts.map +1 -0
- package/lib/types/src/useButtonHold.test.d.ts +2 -0
- package/lib/types/src/useButtonHold.test.d.ts.map +1 -0
- package/lib/types/src/useHold.d.ts +23 -0
- package/lib/types/src/useHold.d.ts.map +1 -0
- package/lib/types/src/useMockServerDevice.d.ts +9 -0
- package/lib/types/src/useMockServerDevice.d.ts.map +1 -0
- package/lib/types/src/useMockServerDevice.test.d.ts +2 -0
- package/lib/types/src/useMockServerDevice.test.d.ts.map +1 -0
- package/lib/types/src/useOpenApp.d.ts +19 -0
- package/lib/types/src/useOpenApp.d.ts.map +1 -0
- package/lib/types/src/useOpenApp.test.d.ts +2 -0
- package/lib/types/src/useOpenApp.test.d.ts.map +1 -0
- package/lib/types/src/useScreenPolling.d.ts +3 -0
- package/lib/types/src/useScreenPolling.d.ts.map +1 -0
- package/lib/types/src/useScreenPolling.test.d.ts +2 -0
- package/lib/types/src/useScreenPolling.test.d.ts.map +1 -0
- package/lib/types/src/useTouchHold.d.ts +23 -0
- package/lib/types/src/useTouchHold.d.ts.map +1 -0
- package/lib/types/src/useTouchHold.test.d.ts +2 -0
- package/lib/types/src/useTouchHold.test.d.ts.map +1 -0
- package/lib/types/tsconfig.prod.tsbuildinfo +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var h=Object.defineProperty;var L=Object.getOwnPropertyDescriptor;var D=Object.getOwnPropertyNames;var p=Object.prototype.hasOwnProperty;var w=(e,r)=>{for(var c in r)h(e,c,{get:r[c],enumerable:!0})},P=(e,r,c,l)=>{if(r&&typeof r=="object"||typeof r=="function")for(let i of D(r))!p.call(e,i)&&i!==c&&h(e,i,{get:()=>r[i],enumerable:!(l=L(r,i))||l.enumerable});return e};var R=e=>P(h({},"__esModule",{value:!0}),e);var M={};w(M,{useScreenPolling:()=>F});module.exports=R(M);var t=require("react");const T=500,_=2e3,b=3,A=e=>e instanceof Error?e.message:String(e);function F(e,r,c){const[l,i]=(0,t.useState)({kind:"loading"}),f=(0,t.useRef)(null),g=(0,t.useRef)(!1),m=(0,t.useRef)(!1);m.current=l.kind==="image";const E=(0,t.useRef)(0),v=(0,t.useRef)(0),S=(0,t.useCallback)(o=>{m.current&&++E.current<=b||i({kind:"error",message:A(o)})},[]),a=(0,t.useCallback)(()=>{f.current&&(c.releaseImageSource?.(f.current),f.current=null)},[c]),I=(0,t.useRef)(()=>{}),k=(0,t.useMemo)(()=>{const o=n=>void n.then(()=>I.current()).catch(S);return{pressButton:(n,s)=>o(e.pressButton(n,s)),touch:(n,s,u)=>o(e.touch(n,s,u))}},[e,S]),y=(0,t.useCallback)(async()=>{if(g.current)return;g.current=!0;const o=v.current,n=()=>o!==v.current;try{const s=await e.screenshot();if(n())return;if(E.current=0,s){const u=await c.toImageSource(s);if(n()){c.releaseImageSource?.(u);return}u!==f.current&&(a(),f.current=u),i(d=>d.kind==="image"&&d.src===u&&d.input===k?d:{kind:"image",src:u,input:k})}else{const u=await e.idle?.();if(n())return;a(),i(u??{kind:"error",message:"No screen to capture"})}}catch(s){n()||S(s)}finally{g.current=!1}},[e,c,k,S,a]);return I.current=()=>void y(),(0,t.useEffect)(()=>{if(!r){a(),i({kind:"loading"});return}let o,n=!1;const s=async()=>{c.isHidden()||await y(),!n&&(o=setTimeout(()=>void s(),m.current?T:_))};return s(),()=>{n=!0,clearTimeout(o),v.current++}},[r,c,y,a]),(0,t.useEffect)(()=>a,[a]),l}0&&(module.exports={useScreenPolling});
|
|
2
|
+
//# sourceMappingURL=useScreenPolling.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/useScreenPolling.ts"],
|
|
4
|
+
"sourcesContent": ["import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\n\nimport {\n type DeviceScreenInput,\n type DeviceScreenState,\n type ScreenApi,\n type ScreenPlatform,\n} from \"./types\";\n\nconst SCREEN_POLL_MS = 500;\nconst IDLE_POLL_MS = 2000;\n/**\n * An app quitting fails a few calls while Speculos shuts down; the last frame\n * stays up through them rather than flashing an error before the dashboard.\n */\nconst LIVE_FAILURES_TOLERATED = 3;\n\nconst describe = (error: unknown): string =>\n error instanceof Error ? error.message : String(error);\n\nexport function useScreenPolling(\n api: ScreenApi,\n polling: boolean,\n platform: ScreenPlatform,\n): DeviceScreenState {\n const [state, setState] = useState<DeviceScreenState>({ kind: \"loading\" });\n\n const imageSource = useRef<string | null>(null);\n const inFlight = useRef(false);\n const isLive = useRef(false);\n isLive.current = state.kind === \"image\";\n const liveFailures = useRef(0);\n /** Bumped when polling stops, so a response still in flight is dropped. */\n const generation = useRef(0);\n\n const fail = useCallback((error: unknown) => {\n if (isLive.current && ++liveFailures.current <= LIVE_FAILURES_TOLERATED) {\n return;\n }\n setState({ kind: \"error\", message: describe(error) });\n }, []);\n\n const releaseImageSource = useCallback(() => {\n if (imageSource.current) {\n platform.releaseImageSource?.(imageSource.current);\n imageSource.current = null;\n }\n }, [platform]);\n\n /**\n * Input handlers refresh straight after acting. Going through a ref keeps\n * them stable, so the <img> is not remounted on every poll.\n */\n const refreshRef = useRef<() => void>(() => {});\n\n const input = useMemo<DeviceScreenInput>(() => {\n const send = (call: Promise<void>) =>\n void call.then(() => refreshRef.current()).catch(fail);\n\n return {\n pressButton: (button, action) => send(api.pressButton(button, action)),\n touch: (x, y, action) => send(api.touch(x, y, action)),\n };\n }, [api, fail]);\n\n const refresh = useCallback(async () => {\n if (inFlight.current) return;\n inFlight.current = true;\n const started = generation.current;\n const stale = () => started !== generation.current;\n try {\n const blob = await api.screenshot();\n if (stale()) return;\n liveFailures.current = 0;\n if (blob) {\n const source = await platform.toImageSource(blob);\n if (stale()) {\n platform.releaseImageSource?.(source);\n return;\n }\n if (source !== imageSource.current) {\n releaseImageSource();\n imageSource.current = source;\n }\n // A source made from the bytes, like a data URI, repeats while the\n // screen holds still; keeping the state then skips a re-render.\n setState((current) =>\n current.kind === \"image\" &&\n current.src === source &&\n current.input === input\n ? current\n : { kind: \"image\", src: source, input },\n );\n } else {\n const idle = await api.idle?.();\n if (stale()) return;\n releaseImageSource();\n setState(idle ?? { kind: \"error\", message: \"No screen to capture\" });\n }\n } catch (error) {\n if (!stale()) fail(error);\n } finally {\n inFlight.current = false;\n }\n }, [api, platform, input, fail, releaseImageSource]);\n refreshRef.current = () => void refresh();\n\n useEffect(() => {\n if (!polling) {\n // Drop the frame rather than keep a released source around, which\n // would flash a broken image when the panel reopens.\n releaseImageSource();\n setState({ kind: \"loading\" });\n return;\n }\n\n let timer: ReturnType<typeof setTimeout>;\n let cancelled = false;\n\n const tick = async () => {\n if (!platform.isHidden()) await refresh();\n if (cancelled) return;\n timer = setTimeout(\n () => void tick(),\n isLive.current ? SCREEN_POLL_MS : IDLE_POLL_MS,\n );\n };\n void tick();\n\n return () => {\n cancelled = true;\n clearTimeout(timer);\n generation.current++;\n };\n }, [polling, platform, refresh, releaseImageSource]);\n\n useEffect(() => releaseImageSource, [releaseImageSource]);\n\n return state;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["useScreenPolling_exports", "__export", "useScreenPolling", "__toCommonJS", "import_react", "SCREEN_POLL_MS", "IDLE_POLL_MS", "LIVE_FAILURES_TOLERATED", "describe", "error", "api", "polling", "platform", "state", "setState", "imageSource", "inFlight", "isLive", "liveFailures", "generation", "fail", "releaseImageSource", "refreshRef", "input", "send", "call", "button", "action", "x", "y", "refresh", "started", "stale", "blob", "source", "current", "idle", "timer", "cancelled", "tick"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var y=Object.create;var w=Object.defineProperty;var k=Object.getOwnPropertyDescriptor;var R=Object.getOwnPropertyNames;var B=Object.getPrototypeOf,C=Object.prototype.hasOwnProperty;var E=(t,a,s,c)=>{if(a&&typeof a=="object"||typeof a=="function")for(let o of R(a))!C.call(t,o)&&o!==s&&w(t,o,{get:()=>a[o],enumerable:!(c=k(a,o))||c.enumerable});return t};var P=(t,a,s)=>(s=t!=null?y(B(t)):{},E(a||!t||!t.__esModule?w(s,"default",{value:t,enumerable:!0}):s,t));var p=P(require("react")),f=require("react-dom/client"),g=require("./useScreenPolling");Object.assign(globalThis,{IS_REACT_ACT_ENVIRONMENT:!0});const v=new Blob(["frame"]),m=(t={})=>({screenshot:vi.fn(()=>Promise.resolve(v)),pressButton:vi.fn(()=>Promise.resolve()),touch:vi.fn(()=>Promise.resolve()),...t});describe("useScreenPolling",()=>{let t,a,s,c;const o=vi.fn(),l=vi.fn(),S={toImageSource:o,releaseImageSource:l,isHidden:()=>c},x=({api:e,polling:i})=>(s.push((0,g.useScreenPolling)(e,i,S)),null),r=(e,i=!0)=>(0,p.act)(()=>a.render(p.default.createElement(x,{api:e,polling:i}))),n=(e=0)=>(0,p.act)(()=>vi.advanceTimersByTimeAsync(e)),h=()=>n(2e3),u=()=>s[s.length-1];beforeEach(()=>{vi.useFakeTimers(),s=[],c=!1;let e=0;o.mockReset(),o.mockImplementation(()=>Promise.resolve(`source-${++e}`)),l.mockReset(),t=document.createElement("div"),a=(0,f.createRoot)(t)}),afterEach(()=>{(0,p.act)(()=>a.unmount()),vi.useRealTimers()}),it("shows a frame through the platform's image source",async()=>{r(m()),await n(),expect(o).toHaveBeenCalledWith(v),expect(u()).toMatchObject({kind:"image",src:"source-1"})}),it("releases a frame's source once the next one replaces it",async()=>{r(m()),await n(),await h(),expect(u()).toMatchObject({kind:"image",src:"source-2"}),expect(l).toHaveBeenCalledExactlyOnceWith("source-1")}),it("keeps its state while the frame stays the same",async()=>{o.mockResolvedValue("data:same"),r(m()),await n();const e=u();await h(),await n(500),expect(o).toHaveBeenCalledTimes(3);const i=s.filter(({kind:d})=>d==="image");expect(new Set(i)).toEqual(new Set([e])),expect(l).not.toHaveBeenCalled()}),it("releases the source when the app quits",async()=>{const e=vi.fn().mockResolvedValueOnce(v).mockResolvedValue(null);r(m({screenshot:e})),await n(),await h(),expect(u()).toEqual({kind:"error",message:"No screen to capture"}),expect(l).toHaveBeenCalledWith("source-1")}),it("releases the source when unmounted",async()=>{r(m()),await n(),(0,p.act)(()=>a.unmount()),a=(0,f.createRoot)(t),expect(l).toHaveBeenCalledWith("source-1")}),it("releases a frame made after polling stopped, without showing it",async()=>{let e=()=>{};o.mockReturnValue(new Promise(d=>e=d));const i=m();r(i),await n(),r(i,!1),e("late"),await n(),expect(u()).toEqual({kind:"loading"}),expect(l).toHaveBeenCalledWith("late")}),it("skips its turns while the platform is hidden",async()=>{c=!0;const e=m();r(e),await n(1e4),expect(e.screenshot).not.toHaveBeenCalled(),c=!1,await n(2e3),expect(e.screenshot).toHaveBeenCalledOnce()})});
|
|
2
|
+
//# sourceMappingURL=useScreenPolling.test.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/useScreenPolling.test.tsx"],
|
|
4
|
+
"sourcesContent": ["import React, { act } from \"react\";\nimport { createRoot, type Root } from \"react-dom/client\";\n\nimport {\n type DeviceScreenState,\n type ScreenApi,\n type ScreenPlatform,\n} from \"./types\";\nimport { useScreenPolling } from \"./useScreenPolling\";\n\nObject.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });\n\nconst FRAME = new Blob([\"frame\"]);\n\nconst anApi = (overrides: Partial<ScreenApi> = {}): ScreenApi => ({\n screenshot: vi.fn(() => Promise.resolve(FRAME)),\n pressButton: vi.fn(() => Promise.resolve()),\n touch: vi.fn(() => Promise.resolve()),\n ...overrides,\n});\n\ndescribe(\"useScreenPolling\", () => {\n let container: HTMLDivElement;\n let root: Root;\n let states: DeviceScreenState[];\n let hidden: boolean;\n const toImageSource = vi.fn<ScreenPlatform[\"toImageSource\"]>();\n const releaseImageSource = vi.fn<(source: string) => void>();\n const platform: ScreenPlatform = {\n toImageSource,\n releaseImageSource,\n isHidden: () => hidden,\n };\n\n const Probe: React.FC<{ api: ScreenApi; polling: boolean }> = ({\n api,\n polling,\n }) => {\n states.push(useScreenPolling(api, polling, platform));\n return null;\n };\n const render = (api: ScreenApi, polling = true) =>\n act(() => root.render(<Probe api={api} polling={polling} />));\n const wait = (ms = 0) => act(() => vi.advanceTimersByTimeAsync(ms));\n /**\n * The turn after the first frame comes at the idle pace: the delay is picked\n * before the frame renders. Live turns follow every 500ms.\n */\n const nextTurn = () => wait(2000);\n const latest = () => states[states.length - 1];\n\n beforeEach(() => {\n vi.useFakeTimers();\n states = [];\n hidden = false;\n let frames = 0;\n toImageSource.mockReset();\n toImageSource.mockImplementation(() =>\n Promise.resolve(`source-${++frames}`),\n );\n releaseImageSource.mockReset();\n container = document.createElement(\"div\");\n root = createRoot(container);\n });\n\n afterEach(() => {\n act(() => root.unmount());\n vi.useRealTimers();\n });\n\n it(\"shows a frame through the platform's image source\", async () => {\n render(anApi());\n await wait();\n\n expect(toImageSource).toHaveBeenCalledWith(FRAME);\n expect(latest()).toMatchObject({ kind: \"image\", src: \"source-1\" });\n });\n\n it(\"releases a frame's source once the next one replaces it\", async () => {\n render(anApi());\n await wait();\n await nextTurn();\n\n expect(latest()).toMatchObject({ kind: \"image\", src: \"source-2\" });\n expect(releaseImageSource).toHaveBeenCalledExactlyOnceWith(\"source-1\");\n });\n\n it(\"keeps its state while the frame stays the same\", async () => {\n toImageSource.mockResolvedValue(\"data:same\");\n render(anApi());\n await wait();\n const frame = latest();\n\n await nextTurn();\n await wait(500);\n\n expect(toImageSource).toHaveBeenCalledTimes(3);\n // React may render once more before bailing out, but with the same state:\n // the image never gets a new source to draw.\n const frames = states.filter(({ kind }) => kind === \"image\");\n expect(new Set(frames)).toEqual(new Set([frame]));\n expect(releaseImageSource).not.toHaveBeenCalled();\n });\n\n it(\"releases the source when the app quits\", async () => {\n const screenshot = vi\n .fn<ScreenApi[\"screenshot\"]>()\n .mockResolvedValueOnce(FRAME)\n .mockResolvedValue(null);\n render(anApi({ screenshot }));\n await wait();\n await nextTurn();\n\n expect(latest()).toEqual({\n kind: \"error\",\n message: \"No screen to capture\",\n });\n expect(releaseImageSource).toHaveBeenCalledWith(\"source-1\");\n });\n\n it(\"releases the source when unmounted\", async () => {\n render(anApi());\n await wait();\n\n act(() => root.unmount());\n root = createRoot(container);\n\n expect(releaseImageSource).toHaveBeenCalledWith(\"source-1\");\n });\n\n it(\"releases a frame made after polling stopped, without showing it\", async () => {\n let finish: (source: string) => void = () => {};\n toImageSource.mockReturnValue(new Promise((resolve) => (finish = resolve)));\n const api = anApi();\n render(api);\n await wait();\n\n render(api, false);\n finish(\"late\");\n await wait();\n\n expect(latest()).toEqual({ kind: \"loading\" });\n expect(releaseImageSource).toHaveBeenCalledWith(\"late\");\n });\n\n it(\"skips its turns while the platform is hidden\", async () => {\n hidden = true;\n const api = anApi();\n render(api);\n await wait(10_000);\n expect(api.screenshot).not.toHaveBeenCalled();\n\n hidden = false;\n await wait(2000);\n\n expect(api.screenshot).toHaveBeenCalledOnce();\n });\n});\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["import_react", "import_client", "import_useScreenPolling", "FRAME", "anApi", "overrides", "container", "root", "states", "hidden", "toImageSource", "releaseImageSource", "platform", "Probe", "api", "polling", "render", "React", "wait", "ms", "nextTurn", "latest", "frames", "frame", "kind", "screenshot", "finish", "resolve"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var r=Object.defineProperty;var d=Object.getOwnPropertyDescriptor;var c=Object.getOwnPropertyNames;var p=Object.prototype.hasOwnProperty;var y=(e,t)=>{for(var o in t)r(e,o,{get:t[o],enumerable:!0})},S=(e,t,o,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of c(t))!p.call(e,i)&&i!==o&&r(e,i,{get:()=>t[i],enumerable:!(n=d(t,i))||n.enumerable});return e};var b=e=>S(r({},"__esModule",{value:!0}),e);var f={};y(f,{toScreenPoint:()=>x,useTouchHold:()=>a});module.exports=b(f);var u=require("react"),h=require("./useHold");const x=(e,t,o,n)=>{if(!o.width||!o.height||!n.width||!n.height)return null;const i=(m,l)=>Math.min(Math.max(Math.round(m),0),l-1);return{x:i(e/o.width*n.width,n.width),y:i(t/o.height*n.height,n.height)}};function a(e){const t=(0,u.useMemo)(()=>e&&((o,n)=>e(o.x,o.y,n)),[e]);return(0,h.useHold)(t)}0&&(module.exports={toScreenPoint,useTouchHold});
|
|
2
|
+
//# sourceMappingURL=useTouchHold.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/useTouchHold.ts"],
|
|
4
|
+
"sourcesContent": ["import { useMemo } from \"react\";\nimport { type SpeculosAction } from \"@ledgerhq/device-mockserver-client\";\n\nimport { type Hold, useHold } from \"./useHold\";\n\nexport interface Size {\n readonly width: number;\n readonly height: number;\n}\n\nexport interface ScreenPoint {\n readonly x: number;\n readonly y: number;\n}\n\n/**\n * Where an offset into the screen as drawn lands in device pixels, or null\n * while either size is unknown.\n */\nexport const toScreenPoint = (\n offsetX: number,\n offsetY: number,\n drawn: Size,\n screen: Size,\n): ScreenPoint | null => {\n if (!drawn.width || !drawn.height || !screen.width || !screen.height) {\n return null;\n }\n // The far edge maps to the pixel count itself, one past the last pixel.\n const clamp = (value: number, size: number) =>\n Math.min(Math.max(Math.round(value), 0), size - 1);\n return {\n x: clamp((offsetX / drawn.width) * screen.width, screen.width),\n y: clamp((offsetY / drawn.height) * screen.height, screen.height),\n };\n};\n\nexport type TouchHold = Hold<ScreenPoint>;\n\n/**\n * A finger's down and up on the screen, as the actions the device gets: a\n * quick tap as one press-and-release, a hold as a press then a release on the\n * spot where it went down.\n */\nexport function useTouchHold(\n onTouch: ((x: number, y: number, action: SpeculosAction) => void) | undefined,\n): TouchHold {\n const onAction = useMemo(\n () =>\n onTouch &&\n ((point: ScreenPoint, action: SpeculosAction) =>\n onTouch(point.x, point.y, action)),\n [onTouch],\n );\n return useHold(onAction);\n}\n"],
|
|
5
|
+
"mappings": "yaAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,mBAAAE,EAAA,iBAAAC,IAAA,eAAAC,EAAAJ,GAAA,IAAAK,EAAwB,iBAGxBC,EAAmC,qBAgB5B,MAAMJ,EAAgB,CAC3BK,EACAC,EACAC,EACAC,IACuB,CACvB,GAAI,CAACD,EAAM,OAAS,CAACA,EAAM,QAAU,CAACC,EAAO,OAAS,CAACA,EAAO,OAC5D,OAAO,KAGT,MAAMC,EAAQ,CAACC,EAAeC,IAC5B,KAAK,IAAI,KAAK,IAAI,KAAK,MAAMD,CAAK,EAAG,CAAC,EAAGC,EAAO,CAAC,EACnD,MAAO,CACL,EAAGF,EAAOJ,EAAUE,EAAM,MAASC,EAAO,MAAOA,EAAO,KAAK,EAC7D,EAAGC,EAAOH,EAAUC,EAAM,OAAUC,EAAO,OAAQA,EAAO,MAAM,CAClE,CACF,EASO,SAASP,EACdW,EACW,CACX,MAAMC,KAAW,WACf,IACED,IACC,CAACE,EAAoBC,IACpBH,EAAQE,EAAM,EAAGA,EAAM,EAAGC,CAAM,GACpC,CAACH,CAAO,CACV,EACA,SAAO,WAAQC,CAAQ,CACzB",
|
|
6
|
+
"names": ["useTouchHold_exports", "__export", "toScreenPoint", "useTouchHold", "__toCommonJS", "import_react", "import_useHold", "offsetX", "offsetY", "drawn", "screen", "clamp", "value", "size", "onTouch", "onAction", "point", "action"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var d=Object.create;var h=Object.defineProperty;var u=Object.getOwnPropertyDescriptor;var p=Object.getOwnPropertyNames;var x=Object.getPrototypeOf,m=Object.prototype.hasOwnProperty;var T=(t,e,o,l)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of p(e))!m.call(t,a)&&a!==o&&h(t,a,{get:()=>e[a],enumerable:!(l=u(e,a))||l.enumerable});return t};var y=(t,e,o)=>(o=t!=null?d(x(t)):{},T(e||!t||!t.__esModule?h(o,"default",{value:t,enumerable:!0}):o,t));var s=y(require("react")),i=require("react-dom/client"),c=require("./useHold"),n=require("./useTouchHold");Object.assign(globalThis,{IS_REACT_ACT_ENVIRONMENT:!0});describe("toScreenPoint",()=>{const t={width:200,height:300},e={width:400,height:600};it("scales a point on the drawn screen to device pixels",()=>{expect((0,n.toScreenPoint)(50,150,t,e)).toEqual({x:100,y:300})}),it("keeps the far edges on the last pixel",()=>{expect((0,n.toScreenPoint)(200,300,t,e)).toEqual({x:399,y:599}),expect((0,n.toScreenPoint)(-5,-5,t,e)).toEqual({x:0,y:0})}),it("has no point while a size is unknown",()=>{expect((0,n.toScreenPoint)(50,50,{width:0,height:0},e)).toBeNull(),expect((0,n.toScreenPoint)(50,50,t,{width:0,height:0})).toBeNull()})});describe("useTouchHold",()=>{let t,e;const o=vi.fn(),l=({handler:r})=>(e=(0,n.useTouchHold)(r),null),a=r=>(0,s.act)(()=>t.render(s.default.createElement(l,{handler:r})));beforeEach(()=>{vi.useFakeTimers(),o.mockReset(),t=(0,i.createRoot)(document.createElement("div"))}),afterEach(()=>{(0,s.act)(()=>t.unmount()),vi.useRealTimers()}),it("sends a quick tap as one press-and-release",()=>{a(o),expect(e.hold({x:10,y:20})).toBe(!0),expect(e.release()).toBe(!0),expect(o).toHaveBeenCalledExactlyOnceWith(10,20,"press-and-release")}),it("sends a hold as a press, then a release where the finger went down",()=>{a(o),e.hold({x:10,y:20}),vi.advanceTimersByTime(c.HOLD_THRESHOLD_MS),e.release(),expect(o.mock.calls).toEqual([[10,20,"press"],[10,20,"release"]])}),it("ignores a second finger while one is down",()=>{a(o),e.hold({x:10,y:20}),expect(e.hold({x:30,y:40})).toBe(!1),e.release(),expect(o).toHaveBeenCalledExactlyOnceWith(10,20,"press-and-release")}),it("ignores touches without a handler",()=>{a(),expect(e.hold({x:10,y:20})).toBe(!1),expect(e.release()).toBe(!1)}),it("releases a held finger when unmounted",()=>{a(o),e.hold({x:10,y:20}),vi.advanceTimersByTime(c.HOLD_THRESHOLD_MS),(0,s.act)(()=>t.unmount()),t=(0,i.createRoot)(document.createElement("div")),expect(o).toHaveBeenLastCalledWith(10,20,"release")})});
|
|
2
|
+
//# sourceMappingURL=useTouchHold.test.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/useTouchHold.test.tsx"],
|
|
4
|
+
"sourcesContent": ["import React, { act } from \"react\";\nimport { createRoot, type Root } from \"react-dom/client\";\n\nimport { HOLD_THRESHOLD_MS } from \"./useHold\";\nimport { toScreenPoint, type TouchHold, useTouchHold } from \"./useTouchHold\";\n\nObject.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });\n\ndescribe(\"toScreenPoint\", () => {\n const drawn = { width: 200, height: 300 };\n const screen = { width: 400, height: 600 };\n\n it(\"scales a point on the drawn screen to device pixels\", () => {\n expect(toScreenPoint(50, 150, drawn, screen)).toEqual({ x: 100, y: 300 });\n });\n\n it(\"keeps the far edges on the last pixel\", () => {\n expect(toScreenPoint(200, 300, drawn, screen)).toEqual({ x: 399, y: 599 });\n expect(toScreenPoint(-5, -5, drawn, screen)).toEqual({ x: 0, y: 0 });\n });\n\n it(\"has no point while a size is unknown\", () => {\n expect(toScreenPoint(50, 50, { width: 0, height: 0 }, screen)).toBeNull();\n expect(toScreenPoint(50, 50, drawn, { width: 0, height: 0 })).toBeNull();\n });\n});\n\ndescribe(\"useTouchHold\", () => {\n let root: Root;\n let touch: TouchHold;\n const onTouch = vi.fn();\n\n const Probe: React.FC<{ handler?: typeof onTouch }> = ({ handler }) => {\n touch = useTouchHold(handler);\n return null;\n };\n const render = (handler?: typeof onTouch) =>\n act(() => root.render(<Probe handler={handler} />));\n\n beforeEach(() => {\n vi.useFakeTimers();\n onTouch.mockReset();\n root = createRoot(document.createElement(\"div\"));\n });\n\n afterEach(() => {\n act(() => root.unmount());\n vi.useRealTimers();\n });\n\n it(\"sends a quick tap as one press-and-release\", () => {\n render(onTouch);\n\n expect(touch.hold({ x: 10, y: 20 })).toBe(true);\n expect(touch.release()).toBe(true);\n\n expect(onTouch).toHaveBeenCalledExactlyOnceWith(\n 10,\n 20,\n \"press-and-release\",\n );\n });\n\n it(\"sends a hold as a press, then a release where the finger went down\", () => {\n render(onTouch);\n\n touch.hold({ x: 10, y: 20 });\n vi.advanceTimersByTime(HOLD_THRESHOLD_MS);\n touch.release();\n\n expect(onTouch.mock.calls).toEqual([\n [10, 20, \"press\"],\n [10, 20, \"release\"],\n ]);\n });\n\n it(\"ignores a second finger while one is down\", () => {\n render(onTouch);\n touch.hold({ x: 10, y: 20 });\n\n expect(touch.hold({ x: 30, y: 40 })).toBe(false);\n touch.release();\n expect(onTouch).toHaveBeenCalledExactlyOnceWith(\n 10,\n 20,\n \"press-and-release\",\n );\n });\n\n it(\"ignores touches without a handler\", () => {\n render();\n\n expect(touch.hold({ x: 10, y: 20 })).toBe(false);\n expect(touch.release()).toBe(false);\n });\n\n it(\"releases a held finger when unmounted\", () => {\n render(onTouch);\n touch.hold({ x: 10, y: 20 });\n vi.advanceTimersByTime(HOLD_THRESHOLD_MS);\n\n act(() => root.unmount());\n root = createRoot(document.createElement(\"div\"));\n\n expect(onTouch).toHaveBeenLastCalledWith(10, 20, \"release\");\n });\n});\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["import_react", "import_client", "import_useHold", "import_useTouchHold", "drawn", "screen", "root", "touch", "onTouch", "Probe", "handler", "render", "React"]
|
|
7
|
+
}
|
package/lib/esm/index.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"dependencies": {
|
|
3
|
+
"@ledgerhq/device-mockserver-client": "workspace:*"
|
|
4
|
+
},
|
|
5
|
+
"devDependencies": {
|
|
6
|
+
"@ledgerhq/device-management-kit": "workspace:*",
|
|
7
|
+
"@ledgerhq/eslint-config-dsdk": "workspace:*",
|
|
8
|
+
"@ledgerhq/ldmk-tool": "workspace:*",
|
|
9
|
+
"@ledgerhq/prettier-config-dsdk": "workspace:*",
|
|
10
|
+
"@ledgerhq/tsconfig-dsdk": "workspace:*",
|
|
11
|
+
"@ledgerhq/vitest-config-dmk": "workspace:*",
|
|
12
|
+
"@types/react": "catalog:",
|
|
13
|
+
"@types/react-dom": "catalog:",
|
|
14
|
+
"react": "catalog:",
|
|
15
|
+
"react-dom": "catalog:"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"import": "./lib/esm/index.js",
|
|
20
|
+
"require": "./lib/cjs/index.js",
|
|
21
|
+
"types": "./lib/types/index.d.ts"
|
|
22
|
+
},
|
|
23
|
+
"./*": {
|
|
24
|
+
"import": "./lib/esm/*",
|
|
25
|
+
"require": "./lib/cjs/*",
|
|
26
|
+
"types": "./lib/types/*"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"./lib"
|
|
31
|
+
],
|
|
32
|
+
"license": "Apache-2.0",
|
|
33
|
+
"main": "lib/cjs/index.js",
|
|
34
|
+
"name": "@ledgerhq/device-mockserver-react-core",
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@ledgerhq/device-management-kit": "workspace:*",
|
|
37
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
38
|
+
},
|
|
39
|
+
"private": false,
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "https://github.com/LedgerHQ/device-sdk-ts.git"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "pnpm ldmk-tool build --entryPoints index.ts,src/*.tsx,src/*.ts --tsconfig tsconfig.prod.json",
|
|
46
|
+
"dev": "concurrently \"pnpm watch:builds\" \"pnpm watch:types\"",
|
|
47
|
+
"lint": "eslint",
|
|
48
|
+
"lint:fix": "pnpm lint --fix",
|
|
49
|
+
"prebuild": "rimraf lib",
|
|
50
|
+
"prettier": "prettier . --check",
|
|
51
|
+
"prettier:fix": "prettier . --write",
|
|
52
|
+
"test": "vitest run",
|
|
53
|
+
"test:coverage": "vitest run --coverage",
|
|
54
|
+
"test:watch": "vitest",
|
|
55
|
+
"typecheck": "tsc --noEmit",
|
|
56
|
+
"watch:builds": "pnpm ldmk-tool watch --entryPoints index.ts,src/*.tsx,src/*.ts --tsconfig tsconfig.prod.json",
|
|
57
|
+
"watch:types": "concurrently \"tsc --watch -p tsconfig.prod.json\" \"tsc-alias --watch -p tsconfig.prod.json\""
|
|
58
|
+
},
|
|
59
|
+
"types": "lib/types/index.d.ts",
|
|
60
|
+
"version": "0.0.0-develop-20261104000847"
|
|
61
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=(n,r)=>({label:n,touch:!1,speculos:!0,screenWidth:128,screenHeight:r,lightScreen:!1}),c={nanoS:{...e("Nano S",32),speculos:!1},nanoSP:e("Nano S Plus",64),nanoX:e("Nano X",64),stax:{label:"Stax",touch:!0,speculos:!0,screenWidth:400,screenHeight:672,lightScreen:!0},flex:{label:"Flex",touch:!0,speculos:!0,screenWidth:480,screenHeight:600,lightScreen:!0},apexp:{label:"Apex",touch:!0,speculos:!0,screenWidth:300,screenHeight:400,lightScreen:!0}},o=e("Device",64),t=n=>c[n]??o;export{t as findDeviceScreenModel};
|
|
2
|
+
//# sourceMappingURL=deviceModels.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/deviceModels.ts"],
|
|
4
|
+
"sourcesContent": ["export interface DeviceScreenModel {\n readonly label: string;\n readonly touch: boolean;\n /** Whether Speculos has an emulator for it, so it can have a screen. */\n readonly speculos: boolean;\n /** In device pixels. */\n readonly screenWidth: number;\n readonly screenHeight: number;\n readonly lightScreen: boolean;\n}\n\nconst nano = (label: string, screenHeight: number): DeviceScreenModel => ({\n label,\n touch: false,\n speculos: true,\n screenWidth: 128,\n screenHeight,\n lightScreen: false,\n});\n\n/** Keyed by the mock server's `device_type`, a DMK `DeviceModelId`. */\nconst MODELS: Record<string, DeviceScreenModel> = {\n nanoS: { ...nano(\"Nano S\", 32), speculos: false },\n nanoSP: nano(\"Nano S Plus\", 64),\n nanoX: nano(\"Nano X\", 64),\n stax: {\n label: \"Stax\",\n touch: true,\n speculos: true,\n screenWidth: 400,\n screenHeight: 672,\n lightScreen: true,\n },\n flex: {\n label: \"Flex\",\n touch: true,\n speculos: true,\n screenWidth: 480,\n screenHeight: 600,\n lightScreen: true,\n },\n apexp: {\n label: \"Apex\",\n touch: true,\n speculos: true,\n screenWidth: 300,\n screenHeight: 400,\n lightScreen: true,\n },\n};\n\nconst UNKNOWN: DeviceScreenModel = nano(\"Device\", 64);\n\nexport const findDeviceScreenModel = (deviceType: string): DeviceScreenModel =>\n MODELS[deviceType] ?? UNKNOWN;\n"],
|
|
5
|
+
"mappings": "AAWA,MAAMA,EAAO,CAACC,EAAeC,KAA6C,CACxE,MAAAD,EACA,MAAO,GACP,SAAU,GACV,YAAa,IACb,aAAAC,EACA,YAAa,EACf,GAGMC,EAA4C,CAChD,MAAO,CAAE,GAAGH,EAAK,SAAU,EAAE,EAAG,SAAU,EAAM,EAChD,OAAQA,EAAK,cAAe,EAAE,EAC9B,MAAOA,EAAK,SAAU,EAAE,EACxB,KAAM,CACJ,MAAO,OACP,MAAO,GACP,SAAU,GACV,YAAa,IACb,aAAc,IACd,YAAa,EACf,EACA,KAAM,CACJ,MAAO,OACP,MAAO,GACP,SAAU,GACV,YAAa,IACb,aAAc,IACd,YAAa,EACf,EACA,MAAO,CACL,MAAO,OACP,MAAO,GACP,SAAU,GACV,YAAa,IACb,aAAc,IACd,YAAa,EACf,CACF,EAEMI,EAA6BJ,EAAK,SAAU,EAAE,EAEvCK,EAAyBC,GACpCH,EAAOG,CAAU,GAAKF",
|
|
6
|
+
"names": ["nano", "label", "screenHeight", "MODELS", "UNKNOWN", "findDeviceScreenModel", "deviceType"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{findDeviceScreenModel as e}from"./deviceModels";describe("findDeviceScreenModel",()=>{it("finds a model by the mock server's device type",()=>{expect(e("stax")).toMatchObject({label:"Stax",touch:!0,speculos:!0})}),it("marks Nano S as having no emulator",()=>{expect(e("nanoS").speculos).toBe(!1)}),it("falls back to a button-driven model for an unknown type",()=>{expect(e("unknown")).toMatchObject({label:"Device",touch:!1})})});
|
|
2
|
+
//# sourceMappingURL=deviceModels.test.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/deviceModels.test.ts"],
|
|
4
|
+
"sourcesContent": ["import { findDeviceScreenModel } from \"./deviceModels\";\n\ndescribe(\"findDeviceScreenModel\", () => {\n it(\"finds a model by the mock server's device type\", () => {\n expect(findDeviceScreenModel(\"stax\")).toMatchObject({\n label: \"Stax\",\n touch: true,\n speculos: true,\n });\n });\n\n it(\"marks Nano S as having no emulator\", () => {\n expect(findDeviceScreenModel(\"nanoS\").speculos).toBe(false);\n });\n\n it(\"falls back to a button-driven model for an unknown type\", () => {\n expect(findDeviceScreenModel(\"unknown\")).toMatchObject({\n label: \"Device\",\n touch: false,\n });\n });\n});\n"],
|
|
5
|
+
"mappings": "AAAA,OAAS,yBAAAA,MAA6B,iBAEtC,SAAS,wBAAyB,IAAM,CACtC,GAAG,iDAAkD,IAAM,CACzD,OAAOA,EAAsB,MAAM,CAAC,EAAE,cAAc,CAClD,MAAO,OACP,MAAO,GACP,SAAU,EACZ,CAAC,CACH,CAAC,EAED,GAAG,qCAAsC,IAAM,CAC7C,OAAOA,EAAsB,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAK,CAC5D,CAAC,EAED,GAAG,0DAA2D,IAAM,CAClE,OAAOA,EAAsB,SAAS,CAAC,EAAE,cAAc,CACrD,MAAO,SACP,MAAO,EACT,CAAC,CACH,CAAC,CACH,CAAC",
|
|
6
|
+
"names": ["findDeviceScreenModel"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{findDeviceScreenModel as r}from"./deviceModels";import{mockServerScreenApi as t}from"./mockServerScreenApi";import{useButtonHold as c}from"./useButtonHold";import{HOLD_THRESHOLD_MS as l,useHold as i}from"./useHold";import{useMockServerDevice as m}from"./useMockServerDevice";import{appIconUrl as d,OPEN_APP_TIMEOUT_MS as x,openableApps as y,useOpenApp as H}from"./useOpenApp";import{useScreenPolling as v}from"./useScreenPolling";import{toScreenPoint as M,useTouchHold as P}from"./useTouchHold";export{l as HOLD_THRESHOLD_MS,x as OPEN_APP_TIMEOUT_MS,d as appIconUrl,r as findDeviceScreenModel,t as mockServerScreenApi,y as openableApps,M as toScreenPoint,c as useButtonHold,i as useHold,m as useMockServerDevice,H as useOpenApp,v as useScreenPolling,P as useTouchHold};
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/index.ts"],
|
|
4
|
+
"sourcesContent": ["export { type DeviceScreenModel, findDeviceScreenModel } from \"./deviceModels\";\nexport { mockServerScreenApi } from \"./mockServerScreenApi\";\nexport type {\n DeviceScreenInput,\n DeviceScreenState,\n MockServerClient,\n ScreenApi,\n ScreenPlatform,\n} from \"./types\";\nexport { type ButtonHold, useButtonHold } from \"./useButtonHold\";\nexport { type Hold, HOLD_THRESHOLD_MS, useHold } from \"./useHold\";\nexport { useMockServerDevice } from \"./useMockServerDevice\";\nexport {\n appIconUrl,\n OPEN_APP_TIMEOUT_MS,\n openableApps,\n type OpenApp,\n useOpenApp,\n} from \"./useOpenApp\";\nexport { useScreenPolling } from \"./useScreenPolling\";\nexport {\n type ScreenPoint,\n type Size,\n toScreenPoint,\n type TouchHold,\n useTouchHold,\n} from \"./useTouchHold\";\n"],
|
|
5
|
+
"mappings": "AAAA,OAAiC,yBAAAA,MAA6B,iBAC9D,OAAS,uBAAAC,MAA2B,wBAQpC,OAA0B,iBAAAC,MAAqB,kBAC/C,OAAoB,qBAAAC,EAAmB,WAAAC,MAAe,YACtD,OAAS,uBAAAC,MAA2B,wBACpC,OACE,cAAAC,EACA,uBAAAC,EACA,gBAAAC,EAEA,cAAAC,MACK,eACP,OAAS,oBAAAC,MAAwB,qBACjC,OAGE,iBAAAC,EAEA,gBAAAC,MACK",
|
|
6
|
+
"names": ["findDeviceScreenModel", "mockServerScreenApi", "useButtonHold", "HOLD_THRESHOLD_MS", "useHold", "useMockServerDevice", "appIconUrl", "OPEN_APP_TIMEOUT_MS", "openableApps", "useOpenApp", "useScreenPolling", "toScreenPoint", "useTouchHold"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const c=o=>o?.status===409,a=(o,t)=>({screenshot:async()=>{try{return await o.getScreenshot(t)}catch(n){if(c(n))return null;throw n}},idle:async()=>({kind:"os-info",device:await o.getDevice(t)}),pressButton:(n,e)=>o.pressButton(t,n,e),touch:(n,e,r)=>o.touchScreen(t,n,e,r),openApp:async n=>{const e=new TextEncoder().encode(n);if(e.length>255)throw new Error(`App name too long to open: ${n}`);const{response:r}=await o.sendApdu(t,Uint8Array.from([224,216,0,0,e.length,...e])),s=r.slice(-4);if(s!=="9000")throw new Error(`Could not open ${n} (${s})`)}});export{a as mockServerScreenApi};
|
|
2
|
+
//# sourceMappingURL=mockServerScreenApi.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/mockServerScreenApi.ts"],
|
|
4
|
+
"sourcesContent": ["import { type MockServerClient, type ScreenApi } from \"./types\";\n\n/** The proxy answers 409 when the device has no Speculos instance. */\nconst isNoInstance = (error: unknown): boolean =>\n (error as { status?: unknown } | null)?.status === 409;\n\nexport const mockServerScreenApi = (\n client: MockServerClient,\n deviceId: string,\n): ScreenApi => ({\n screenshot: async () => {\n try {\n return await client.getScreenshot(deviceId);\n } catch (error) {\n if (isNoInstance(error)) return null;\n throw error;\n }\n },\n idle: async () => ({\n kind: \"os-info\",\n device: await client.getDevice(deviceId),\n }),\n pressButton: (button, action) => client.pressButton(deviceId, button, action),\n touch: (x, y, action) => client.touchScreen(deviceId, x, y, action),\n openApp: async (appName) => {\n const name = new TextEncoder().encode(appName);\n // Lc is a single byte.\n if (name.length > 0xff) {\n throw new Error(`App name too long to open: ${appName}`);\n }\n const { response } = await client.sendApdu(\n deviceId,\n Uint8Array.from([0xe0, 0xd8, 0x00, 0x00, name.length, ...name]),\n );\n const statusWord = response.slice(-4);\n if (statusWord !== \"9000\") {\n throw new Error(`Could not open ${appName} (${statusWord})`);\n }\n },\n});\n"],
|
|
5
|
+
"mappings": "AAGA,MAAMA,EAAgBC,GACnBA,GAAuC,SAAW,IAExCC,EAAsB,CACjCC,EACAC,KACe,CACf,WAAY,SAAY,CACtB,GAAI,CACF,OAAO,MAAMD,EAAO,cAAcC,CAAQ,CAC5C,OAASH,EAAO,CACd,GAAID,EAAaC,CAAK,EAAG,OAAO,KAChC,MAAMA,CACR,CACF,EACA,KAAM,UAAa,CACjB,KAAM,UACN,OAAQ,MAAME,EAAO,UAAUC,CAAQ,CACzC,GACA,YAAa,CAACC,EAAQC,IAAWH,EAAO,YAAYC,EAAUC,EAAQC,CAAM,EAC5E,MAAO,CAACC,EAAGC,EAAGF,IAAWH,EAAO,YAAYC,EAAUG,EAAGC,EAAGF,CAAM,EAClE,QAAS,MAAOG,GAAY,CAC1B,MAAMC,EAAO,IAAI,YAAY,EAAE,OAAOD,CAAO,EAE7C,GAAIC,EAAK,OAAS,IAChB,MAAM,IAAI,MAAM,8BAA8BD,CAAO,EAAE,EAEzD,KAAM,CAAE,SAAAE,CAAS,EAAI,MAAMR,EAAO,SAChCC,EACA,WAAW,KAAK,CAAC,IAAM,IAAM,EAAM,EAAMM,EAAK,OAAQ,GAAGA,CAAI,CAAC,CAChE,EACME,EAAaD,EAAS,MAAM,EAAE,EACpC,GAAIC,IAAe,OACjB,MAAM,IAAI,MAAM,kBAAkBH,CAAO,KAAKG,CAAU,GAAG,CAE/D,CACF",
|
|
6
|
+
"names": ["isNoInstance", "error", "mockServerScreenApi", "client", "deviceId", "button", "action", "x", "y", "appName", "name", "response", "statusWord"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{mockServerScreenApi as o}from"./mockServerScreenApi";const n=e=>({getScreenshot:vi.fn(),getDevice:vi.fn(),pressButton:vi.fn(()=>Promise.resolve()),touchScreen:vi.fn(()=>Promise.resolve()),...e});describe("mockServerScreenApi",()=>{it("resolves the screenshot of the device",async()=>{const e=new Blob(["png"]),t=n({getScreenshot:vi.fn(()=>Promise.resolve(e))});await expect(o(t,"device-1").screenshot()).resolves.toBe(e),expect(t.getScreenshot).toHaveBeenCalledWith("device-1")}),it("resolves no screenshot when the device has no Speculos instance",async()=>{const e=n({getScreenshot:vi.fn(()=>Promise.reject({status:409}))});await expect(o(e,"device-1").screenshot()).resolves.toBeNull()}),it("rethrows any other screenshot failure",async()=>{const e=Object.assign(new Error("boom"),{status:500}),t=n({getScreenshot:vi.fn(()=>Promise.reject(e))});await expect(o(t,"device-1").screenshot()).rejects.toBe(e)}),it("shows the device record while idle",async()=>{const e={id:"device-1",device_type:"nanoX"},t=n({getDevice:vi.fn(()=>Promise.resolve(e))});await expect(o(t,"device-1").idle?.()).resolves.toEqual({kind:"os-info",device:e})}),it("forwards buttons and touches to the device",async()=>{const e=n({}),t=o(e,"device-1");await t.pressButton("left","press"),await t.touch(10,20,"release"),expect(e.pressButton).toHaveBeenCalledWith("device-1","left","press"),expect(e.touchScreen).toHaveBeenCalledWith("device-1",10,20,"release")}),it("opens an app by name",async()=>{const e=vi.fn(()=>Promise.resolve({response:"9000"}));await o(n({sendApdu:e}),"device-1").openApp?.("Bitcoin"),expect(e).toHaveBeenCalledWith("device-1",Uint8Array.from([224,216,0,0,7,...new TextEncoder().encode("Bitcoin")]))}),it("fails to open an app the device refuses",async()=>{const e=vi.fn(()=>Promise.resolve({response:"6807"})),t=o(n({sendApdu:e}),"device-1");await expect(t.openApp?.("Bitcoin")).rejects.toThrow("6807")}),it("refuses an app name too long for one APDU",async()=>{const e=vi.fn(()=>Promise.resolve({response:"9000"})),t=o(n({sendApdu:e}),"device-1");await expect(t.openApp?.("a".repeat(256))).rejects.toThrow("too long"),expect(e).not.toHaveBeenCalled()})});
|
|
2
|
+
//# sourceMappingURL=mockServerScreenApi.test.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/mockServerScreenApi.test.ts"],
|
|
4
|
+
"sourcesContent": ["import { mockServerScreenApi } from \"./mockServerScreenApi\";\nimport { type MockServerClient } from \"./types\";\n\nconst aClient = (overrides: Partial<Record<keyof MockServerClient, unknown>>) =>\n ({\n getScreenshot: vi.fn(),\n getDevice: vi.fn(),\n pressButton: vi.fn(() => Promise.resolve()),\n touchScreen: vi.fn(() => Promise.resolve()),\n ...overrides,\n }) as unknown as MockServerClient;\n\ndescribe(\"mockServerScreenApi\", () => {\n it(\"resolves the screenshot of the device\", async () => {\n const blob = new Blob([\"png\"]);\n const client = aClient({\n getScreenshot: vi.fn(() => Promise.resolve(blob)),\n });\n\n await expect(\n mockServerScreenApi(client, \"device-1\").screenshot(),\n ).resolves.toBe(blob);\n expect(client.getScreenshot).toHaveBeenCalledWith(\"device-1\");\n });\n\n it(\"resolves no screenshot when the device has no Speculos instance\", async () => {\n const client = aClient({\n getScreenshot: vi.fn(() => Promise.reject({ status: 409 })),\n });\n\n await expect(\n mockServerScreenApi(client, \"device-1\").screenshot(),\n ).resolves.toBeNull();\n });\n\n it(\"rethrows any other screenshot failure\", async () => {\n const error = Object.assign(new Error(\"boom\"), { status: 500 });\n const client = aClient({\n getScreenshot: vi.fn(() => Promise.reject(error)),\n });\n\n await expect(\n mockServerScreenApi(client, \"device-1\").screenshot(),\n ).rejects.toBe(error);\n });\n\n it(\"shows the device record while idle\", async () => {\n const device = { id: \"device-1\", device_type: \"nanoX\" };\n const client = aClient({ getDevice: vi.fn(() => Promise.resolve(device)) });\n\n await expect(\n mockServerScreenApi(client, \"device-1\").idle?.(),\n ).resolves.toEqual({\n kind: \"os-info\",\n device,\n });\n });\n\n it(\"forwards buttons and touches to the device\", async () => {\n const client = aClient({});\n const api = mockServerScreenApi(client, \"device-1\");\n\n await api.pressButton(\"left\", \"press\");\n await api.touch(10, 20, \"release\");\n\n expect(client.pressButton).toHaveBeenCalledWith(\n \"device-1\",\n \"left\",\n \"press\",\n );\n expect(client.touchScreen).toHaveBeenCalledWith(\n \"device-1\",\n 10,\n 20,\n \"release\",\n );\n });\n\n it(\"opens an app by name\", async () => {\n const sendApdu = vi.fn(() => Promise.resolve({ response: \"9000\" }));\n const api = mockServerScreenApi(aClient({ sendApdu }), \"device-1\");\n\n await api.openApp?.(\"Bitcoin\");\n\n expect(sendApdu).toHaveBeenCalledWith(\n \"device-1\",\n Uint8Array.from([\n 0xe0,\n 0xd8,\n 0x00,\n 0x00,\n 0x07,\n ...new TextEncoder().encode(\"Bitcoin\"),\n ]),\n );\n });\n\n it(\"fails to open an app the device refuses\", async () => {\n const sendApdu = vi.fn(() => Promise.resolve({ response: \"6807\" }));\n const api = mockServerScreenApi(aClient({ sendApdu }), \"device-1\");\n\n await expect(api.openApp?.(\"Bitcoin\")).rejects.toThrow(\"6807\");\n });\n\n it(\"refuses an app name too long for one APDU\", async () => {\n const sendApdu = vi.fn(() => Promise.resolve({ response: \"9000\" }));\n const api = mockServerScreenApi(aClient({ sendApdu }), \"device-1\");\n\n await expect(api.openApp?.(\"a\".repeat(256))).rejects.toThrow(\"too long\");\n expect(sendApdu).not.toHaveBeenCalled();\n });\n});\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["mockServerScreenApi", "aClient", "overrides", "blob", "client", "error", "device", "api", "sendApdu"]
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/useButtonHold.ts"],
|
|
4
|
+
"sourcesContent": ["import {\n type SpeculosAction,\n type SpeculosButton,\n} from \"@ledgerhq/device-mockserver-client\";\n\nimport { type Hold, useHold } from \"./useHold\";\n\nexport type ButtonHold = Hold<SpeculosButton>;\n\n/** A physical button's down and up, as the actions the device gets. */\nexport const useButtonHold = (\n onPress: (button: SpeculosButton, action: SpeculosAction) => void,\n): ButtonHold => useHold(onPress);\n"],
|
|
5
|
+
"mappings": "AAKA,OAAoB,WAAAA,MAAe,YAK5B,MAAMC,EACXC,GACeF,EAAQE,CAAO",
|
|
6
|
+
"names": ["useHold", "useButtonHold", "onPress"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import r,{act as o}from"react";import{createRoot as l}from"react-dom/client";import{useButtonHold as i}from"./useButtonHold";import{HOLD_THRESHOLD_MS as n}from"./useHold";Object.assign(globalThis,{IS_REACT_ACT_ENVIRONMENT:!0});describe("useButtonHold",()=>{let a,e;const t=vi.fn(),s=()=>(e=i(t),null);beforeEach(()=>{vi.useFakeTimers(),t.mockReset(),a=l(document.createElement("div")),o(()=>a.render(r.createElement(s,null)))}),afterEach(()=>{o(()=>a.unmount()),vi.useRealTimers()}),it("sends a quick click as one press-and-release",()=>{e.hold("left"),vi.advanceTimersByTime(n-1),e.release(),expect(t).toHaveBeenCalledExactlyOnceWith("left","press-and-release")}),it("sends a hold as a press then a release",()=>{e.hold("both"),vi.advanceTimersByTime(n),expect(t).toHaveBeenCalledExactlyOnceWith("both","press"),e.release(),expect(t).toHaveBeenLastCalledWith("both","release"),expect(t).toHaveBeenCalledTimes(2)}),it("handles one button at a time",()=>{e.hold("left"),e.hold("right"),e.release(),expect(t).toHaveBeenCalledExactlyOnceWith("left","press-and-release")}),it("ignores a release with nothing held",()=>{e.release(),expect(t).not.toHaveBeenCalled()}),it("releases a held button when unmounted",()=>{e.hold("right"),vi.advanceTimersByTime(n),o(()=>a.unmount()),a=l(document.createElement("div")),expect(t).toHaveBeenLastCalledWith("right","release")}),it("sends nothing for a click cut short by an unmount",()=>{e.hold("right"),o(()=>a.unmount()),a=l(document.createElement("div")),vi.advanceTimersByTime(n),expect(t).not.toHaveBeenCalled()})});
|
|
2
|
+
//# sourceMappingURL=useButtonHold.test.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/useButtonHold.test.tsx"],
|
|
4
|
+
"sourcesContent": ["import React, { act } from \"react\";\nimport { createRoot, type Root } from \"react-dom/client\";\n\nimport { type ButtonHold, useButtonHold } from \"./useButtonHold\";\nimport { HOLD_THRESHOLD_MS } from \"./useHold\";\n\nObject.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });\n\ndescribe(\"useButtonHold\", () => {\n let root: Root;\n let buttons: ButtonHold;\n const onPress = vi.fn();\n\n const Probe: React.FC = () => {\n buttons = useButtonHold(onPress);\n return null;\n };\n\n beforeEach(() => {\n vi.useFakeTimers();\n onPress.mockReset();\n root = createRoot(document.createElement(\"div\"));\n act(() => root.render(<Probe />));\n });\n\n afterEach(() => {\n act(() => root.unmount());\n vi.useRealTimers();\n });\n\n it(\"sends a quick click as one press-and-release\", () => {\n buttons.hold(\"left\");\n vi.advanceTimersByTime(HOLD_THRESHOLD_MS - 1);\n buttons.release();\n\n expect(onPress).toHaveBeenCalledExactlyOnceWith(\n \"left\",\n \"press-and-release\",\n );\n });\n\n it(\"sends a hold as a press then a release\", () => {\n buttons.hold(\"both\");\n vi.advanceTimersByTime(HOLD_THRESHOLD_MS);\n expect(onPress).toHaveBeenCalledExactlyOnceWith(\"both\", \"press\");\n\n buttons.release();\n\n expect(onPress).toHaveBeenLastCalledWith(\"both\", \"release\");\n expect(onPress).toHaveBeenCalledTimes(2);\n });\n\n it(\"handles one button at a time\", () => {\n buttons.hold(\"left\");\n buttons.hold(\"right\");\n buttons.release();\n\n expect(onPress).toHaveBeenCalledExactlyOnceWith(\n \"left\",\n \"press-and-release\",\n );\n });\n\n it(\"ignores a release with nothing held\", () => {\n buttons.release();\n\n expect(onPress).not.toHaveBeenCalled();\n });\n\n it(\"releases a held button when unmounted\", () => {\n buttons.hold(\"right\");\n vi.advanceTimersByTime(HOLD_THRESHOLD_MS);\n\n act(() => root.unmount());\n root = createRoot(document.createElement(\"div\"));\n\n expect(onPress).toHaveBeenLastCalledWith(\"right\", \"release\");\n });\n\n it(\"sends nothing for a click cut short by an unmount\", () => {\n buttons.hold(\"right\");\n\n act(() => root.unmount());\n root = createRoot(document.createElement(\"div\"));\n vi.advanceTimersByTime(HOLD_THRESHOLD_MS);\n\n expect(onPress).not.toHaveBeenCalled();\n });\n});\n"],
|
|
5
|
+
"mappings": "AAAA,OAAOA,GAAS,OAAAC,MAAW,QAC3B,OAAS,cAAAC,MAA6B,mBAEtC,OAA0B,iBAAAC,MAAqB,kBAC/C,OAAS,qBAAAC,MAAyB,YAElC,OAAO,OAAO,WAAY,CAAE,yBAA0B,EAAK,CAAC,EAE5D,SAAS,gBAAiB,IAAM,CAC9B,IAAIC,EACAC,EACJ,MAAMC,EAAU,GAAG,GAAG,EAEhBC,EAAkB,KACtBF,EAAUH,EAAcI,CAAO,EACxB,MAGT,WAAW,IAAM,CACf,GAAG,cAAc,EACjBA,EAAQ,UAAU,EAClBF,EAAOH,EAAW,SAAS,cAAc,KAAK,CAAC,EAC/CD,EAAI,IAAMI,EAAK,OAAOL,EAAA,cAACQ,EAAA,IAAM,CAAE,CAAC,CAClC,CAAC,EAED,UAAU,IAAM,CACdP,EAAI,IAAMI,EAAK,QAAQ,CAAC,EACxB,GAAG,cAAc,CACnB,CAAC,EAED,GAAG,+CAAgD,IAAM,CACvDC,EAAQ,KAAK,MAAM,EACnB,GAAG,oBAAoBF,EAAoB,CAAC,EAC5CE,EAAQ,QAAQ,EAEhB,OAAOC,CAAO,EAAE,gCACd,OACA,mBACF,CACF,CAAC,EAED,GAAG,yCAA0C,IAAM,CACjDD,EAAQ,KAAK,MAAM,EACnB,GAAG,oBAAoBF,CAAiB,EACxC,OAAOG,CAAO,EAAE,gCAAgC,OAAQ,OAAO,EAE/DD,EAAQ,QAAQ,EAEhB,OAAOC,CAAO,EAAE,yBAAyB,OAAQ,SAAS,EAC1D,OAAOA,CAAO,EAAE,sBAAsB,CAAC,CACzC,CAAC,EAED,GAAG,+BAAgC,IAAM,CACvCD,EAAQ,KAAK,MAAM,EACnBA,EAAQ,KAAK,OAAO,EACpBA,EAAQ,QAAQ,EAEhB,OAAOC,CAAO,EAAE,gCACd,OACA,mBACF,CACF,CAAC,EAED,GAAG,sCAAuC,IAAM,CAC9CD,EAAQ,QAAQ,EAEhB,OAAOC,CAAO,EAAE,IAAI,iBAAiB,CACvC,CAAC,EAED,GAAG,wCAAyC,IAAM,CAChDD,EAAQ,KAAK,OAAO,EACpB,GAAG,oBAAoBF,CAAiB,EAExCH,EAAI,IAAMI,EAAK,QAAQ,CAAC,EACxBA,EAAOH,EAAW,SAAS,cAAc,KAAK,CAAC,EAE/C,OAAOK,CAAO,EAAE,yBAAyB,QAAS,SAAS,CAC7D,CAAC,EAED,GAAG,oDAAqD,IAAM,CAC5DD,EAAQ,KAAK,OAAO,EAEpBL,EAAI,IAAMI,EAAK,QAAQ,CAAC,EACxBA,EAAOH,EAAW,SAAS,cAAc,KAAK,CAAC,EAC/C,GAAG,oBAAoBE,CAAiB,EAExC,OAAOG,CAAO,EAAE,IAAI,iBAAiB,CACvC,CAAC,CACH,CAAC",
|
|
6
|
+
"names": ["React", "act", "createRoot", "useButtonHold", "HOLD_THRESHOLD_MS", "root", "buttons", "onPress", "Probe"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{useCallback as l,useEffect as i,useMemo as f,useRef as o}from"react";const p=150;function m(s){const r=o(null),n=o(null),t=o(s);t.current=s;const u=l(()=>{n.current&&(clearTimeout(n.current),n.current=null)},[]),c=l(e=>{if(!t.current||r.current)return!1;const d={target:e,pressed:!1};return r.current=d,n.current=setTimeout(()=>{d.pressed=!0,t.current?.(e,"press")},p),!0},[]),a=l(()=>{const e=r.current;return e?(r.current=null,u(),t.current?.(e.target,e.pressed?"release":"press-and-release"),!0):!1},[u]);return i(()=>()=>{const e=r.current;r.current=null,u(),e?.pressed&&t.current?.(e.target,"release")},[u]),f(()=>({hold:c,release:a}),[c,a])}export{p as HOLD_THRESHOLD_MS,m as useHold};
|
|
2
|
+
//# sourceMappingURL=useHold.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/useHold.ts"],
|
|
4
|
+
"sourcesContent": ["import { useCallback, useEffect, useMemo, useRef } from \"react\";\nimport { type SpeculosAction } from \"@ledgerhq/device-mockserver-client\";\n\n/**\n * How long a button or finger stays down before the interaction counts as a\n * hold. A click sent as a separate press and release is two requests the\n * device can receive in either order, and the emulator only sees what is down\n * when its event loop samples it, so a click that beats the loop is dropped:\n * a quick tap, or a chord of both buttons. One press-and-release leaves the\n * timing to the emulator.\n */\nexport const HOLD_THRESHOLD_MS = 150;\n\ninterface Held<T> {\n target: T;\n pressed: boolean;\n}\n\nexport interface Hold<T> {\n /** Went down. False when ignored: no handler, or something is held. */\n hold(target: T): boolean;\n /** Went up. False when nothing was held. */\n release(): boolean;\n}\n\n/**\n * Turns a button's or a finger's down and up into the actions the device\n * gets: a quick click as one press-and-release, a longer hold as a press then\n * a release. Whatever is still pressed on unmount is released.\n */\nexport function useHold<T>(\n onAction: ((target: T, action: SpeculosAction) => void) | undefined,\n): Hold<T> {\n const held = useRef<Held<T> | null>(null);\n const holdTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\n const actionRef = useRef(onAction);\n actionRef.current = onAction;\n\n const clearHoldTimer = useCallback(() => {\n if (!holdTimer.current) return;\n clearTimeout(holdTimer.current);\n holdTimer.current = null;\n }, []);\n\n const hold = useCallback((target: T) => {\n if (!actionRef.current || held.current) return false;\n const entry: Held<T> = { target, pressed: false };\n held.current = entry;\n holdTimer.current = setTimeout(() => {\n entry.pressed = true;\n actionRef.current?.(target, \"press\");\n }, HOLD_THRESHOLD_MS);\n return true;\n }, []);\n\n const release = useCallback(() => {\n const entry = held.current;\n if (!entry) return false;\n held.current = null;\n clearHoldTimer();\n actionRef.current?.(\n entry.target,\n entry.pressed ? \"release\" : \"press-and-release\",\n );\n return true;\n }, [clearHoldTimer]);\n\n useEffect(\n () => () => {\n const entry = held.current;\n held.current = null;\n clearHoldTimer();\n if (entry?.pressed) actionRef.current?.(entry.target, \"release\");\n },\n [clearHoldTimer],\n );\n\n return useMemo(() => ({ hold, release }), [hold, release]);\n}\n"],
|
|
5
|
+
"mappings": "AAAA,OAAS,eAAAA,EAAa,aAAAC,EAAW,WAAAC,EAAS,UAAAC,MAAc,QAWjD,MAAMC,EAAoB,IAmB1B,SAASC,EACdC,EACS,CACT,MAAMC,EAAOJ,EAAuB,IAAI,EAClCK,EAAYL,EAA6C,IAAI,EAC7DM,EAAYN,EAAOG,CAAQ,EACjCG,EAAU,QAAUH,EAEpB,MAAMI,EAAiBV,EAAY,IAAM,CAClCQ,EAAU,UACf,aAAaA,EAAU,OAAO,EAC9BA,EAAU,QAAU,KACtB,EAAG,CAAC,CAAC,EAECG,EAAOX,EAAaY,GAAc,CACtC,GAAI,CAACH,EAAU,SAAWF,EAAK,QAAS,MAAO,GAC/C,MAAMM,EAAiB,CAAE,OAAAD,EAAQ,QAAS,EAAM,EAChD,OAAAL,EAAK,QAAUM,EACfL,EAAU,QAAU,WAAW,IAAM,CACnCK,EAAM,QAAU,GAChBJ,EAAU,UAAUG,EAAQ,OAAO,CACrC,EAAGR,CAAiB,EACb,EACT,EAAG,CAAC,CAAC,EAECU,EAAUd,EAAY,IAAM,CAChC,MAAMa,EAAQN,EAAK,QACnB,OAAKM,GACLN,EAAK,QAAU,KACfG,EAAe,EACfD,EAAU,UACRI,EAAM,OACNA,EAAM,QAAU,UAAY,mBAC9B,EACO,IAPY,EAQrB,EAAG,CAACH,CAAc,CAAC,EAEnB,OAAAT,EACE,IAAM,IAAM,CACV,MAAMY,EAAQN,EAAK,QACnBA,EAAK,QAAU,KACfG,EAAe,EACXG,GAAO,SAASJ,EAAU,UAAUI,EAAM,OAAQ,SAAS,CACjE,EACA,CAACH,CAAc,CACjB,EAEOR,EAAQ,KAAO,CAAE,KAAAS,EAAM,QAAAG,CAAQ,GAAI,CAACH,EAAMG,CAAO,CAAC,CAC3D",
|
|
6
|
+
"names": ["useCallback", "useEffect", "useMemo", "useRef", "HOLD_THRESHOLD_MS", "useHold", "onAction", "held", "holdTimer", "actionRef", "clearHoldTimer", "hold", "target", "entry", "release"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{useEffect as f,useState as l}from"react";const s=2e3;function p(n,e){const[u,t]=l();return f(()=>{let r=!1,i;if(t(void 0),!e)return;const o=()=>{n.getDevice(e).then(c=>{r||t(c)},()=>{r||(t(null),i=setTimeout(o,s))})};return o(),()=>{r=!0,clearTimeout(i)}},[n,e]),u}export{p as useMockServerDevice};
|
|
2
|
+
//# sourceMappingURL=useMockServerDevice.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/useMockServerDevice.ts"],
|
|
4
|
+
"sourcesContent": ["import { useEffect, useState } from \"react\";\nimport { type Device } from \"@ledgerhq/device-mockserver-client\";\n\nimport { type MockServerClient } from \"./types\";\n\nconst RETRY_MS = 2000;\n\n/**\n * The mock server's record of a device: undefined while it loads, or while no\n * device id is given, and null while it cannot be read. A failed read is\n * retried, so a transient error does not leave the device without its model.\n */\nexport function useMockServerDevice(\n client: MockServerClient,\n deviceId: string,\n): Device | null | undefined {\n const [device, setDevice] = useState<Device | null>();\n\n useEffect(() => {\n let cancelled = false;\n let retry: ReturnType<typeof setTimeout> | undefined;\n setDevice(undefined);\n if (!deviceId) return;\n\n const load = () => {\n client.getDevice(deviceId).then(\n (record) => {\n if (!cancelled) setDevice(record);\n },\n () => {\n if (cancelled) return;\n setDevice(null);\n retry = setTimeout(load, RETRY_MS);\n },\n );\n };\n load();\n\n return () => {\n cancelled = true;\n clearTimeout(retry);\n };\n }, [client, deviceId]);\n\n return device;\n}\n"],
|
|
5
|
+
"mappings": "AAAA,OAAS,aAAAA,EAAW,YAAAC,MAAgB,QAKpC,MAAMC,EAAW,IAOV,SAASC,EACdC,EACAC,EAC2B,CAC3B,KAAM,CAACC,EAAQC,CAAS,EAAIN,EAAwB,EAEpD,OAAAD,EAAU,IAAM,CACd,IAAIQ,EAAY,GACZC,EAEJ,GADAF,EAAU,MAAS,EACf,CAACF,EAAU,OAEf,MAAMK,EAAO,IAAM,CACjBN,EAAO,UAAUC,CAAQ,EAAE,KACxBM,GAAW,CACLH,GAAWD,EAAUI,CAAM,CAClC,EACA,IAAM,CACAH,IACJD,EAAU,IAAI,EACdE,EAAQ,WAAWC,EAAMR,CAAQ,EACnC,CACF,CACF,EACA,OAAAQ,EAAK,EAEE,IAAM,CACXF,EAAY,GACZ,aAAaC,CAAK,CACpB,CACF,EAAG,CAACL,EAAQC,CAAQ,CAAC,EAEdC,CACT",
|
|
6
|
+
"names": ["useEffect", "useState", "RETRY_MS", "useMockServerDevice", "client", "deviceId", "device", "setDevice", "cancelled", "retry", "load", "record"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import v,{act as c}from"react";import{createRoot as m}from"react-dom/client";import{useMockServerDevice as p}from"./useMockServerDevice";Object.assign(globalThis,{IS_REACT_ACT_ENVIRONMENT:!0});const n={id:"device-1",device_type:"stax"};describe("useMockServerDevice",()=>{let l,s,t;const d=({client:e,deviceId:i})=>(t=p(e,i),null),a=e=>({getDevice:vi.fn(e)}),r=(e,i="device-1")=>c(()=>s.render(v.createElement(d,{client:e,deviceId:i}))),o=()=>c(()=>vi.advanceTimersByTimeAsync(0));beforeEach(()=>{vi.useFakeTimers(),t=void 0,l=document.createElement("div"),s=m(l)}),afterEach(()=>{c(()=>s.unmount()),vi.useRealTimers()}),it("reads the device record",async()=>{const e=a(()=>Promise.resolve(n));r(e),expect(t).toBeUndefined(),await o(),expect(t).toBe(n),expect(e.getDevice).toHaveBeenCalledWith("device-1")}),it("accepts a client from another copy of the package",async()=>{class e{record=n;getDevice=vi.fn(()=>Promise.resolve(this.record));getScreenshot=vi.fn();pressButton=vi.fn();touchScreen=vi.fn();sendApdu=vi.fn()}const i=new e;r(i),await o(),expect(t).toBe(n)}),it("reads nothing without a device id",async()=>{const e=a(()=>Promise.resolve(n));r(e,""),await o(),expect(t).toBeUndefined(),expect(e.getDevice).not.toHaveBeenCalled()}),it("retries a failed read until it succeeds",async()=>{const e=vi.fn().mockRejectedValueOnce(new Error("offline")).mockResolvedValue(n),i=a(e);r(i),await o(),expect(t).toBeNull(),await c(()=>vi.advanceTimersByTimeAsync(2e3)),expect(t).toBe(n),expect(e).toHaveBeenCalledTimes(2)}),it("stops retrying once unmounted",async()=>{const e=a(()=>Promise.reject(new Error("offline")));r(e),await o(),c(()=>s.render(v.createElement(v.Fragment,null))),await c(()=>vi.advanceTimersByTimeAsync(1e4)),expect(e.getDevice).toHaveBeenCalledOnce()})});
|
|
2
|
+
//# sourceMappingURL=useMockServerDevice.test.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/useMockServerDevice.test.tsx"],
|
|
4
|
+
"sourcesContent": ["import React, { act } from \"react\";\nimport { createRoot, type Root } from \"react-dom/client\";\nimport { type Device } from \"@ledgerhq/device-mockserver-client\";\n\nimport { type MockServerClient } from \"./types\";\nimport { useMockServerDevice } from \"./useMockServerDevice\";\n\nObject.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });\n\nconst DEVICE = { id: \"device-1\", device_type: \"stax\" } as Device;\n\ndescribe(\"useMockServerDevice\", () => {\n let container: HTMLDivElement;\n let root: Root;\n let result: Device | null | undefined;\n\n const Probe: React.FC<{ client: MockServerClient; deviceId: string }> = ({\n client,\n deviceId,\n }) => {\n result = useMockServerDevice(client, deviceId);\n return null;\n };\n const aClient = (getDevice: () => Promise<Device>) =>\n ({ getDevice: vi.fn(getDevice) }) as unknown as MockServerClient;\n const render = (client: MockServerClient, deviceId = \"device-1\") =>\n act(() => root.render(<Probe client={client} deviceId={deviceId} />));\n const flush = () => act(() => vi.advanceTimersByTimeAsync(0));\n\n beforeEach(() => {\n vi.useFakeTimers();\n result = undefined;\n container = document.createElement(\"div\");\n root = createRoot(container);\n });\n\n afterEach(() => {\n act(() => root.unmount());\n vi.useRealTimers();\n });\n\n it(\"reads the device record\", async () => {\n const client = aClient(() => Promise.resolve(DEVICE));\n\n render(client);\n expect(result).toBeUndefined();\n await flush();\n\n expect(result).toBe(DEVICE);\n expect(client.getDevice).toHaveBeenCalledWith(\"device-1\");\n });\n\n it(\"accepts a client from another copy of the package\", async () => {\n // Its own private field, as a MockClient from a second install would have:\n // only the public calls must match.\n class OtherCopyClient implements MockServerClient {\n private readonly record = DEVICE;\n getDevice = vi.fn(() => Promise.resolve(this.record));\n getScreenshot = vi.fn<MockServerClient[\"getScreenshot\"]>();\n pressButton = vi.fn<MockServerClient[\"pressButton\"]>();\n touchScreen = vi.fn<MockServerClient[\"touchScreen\"]>();\n sendApdu = vi.fn<MockServerClient[\"sendApdu\"]>();\n }\n const client = new OtherCopyClient();\n\n render(client);\n await flush();\n\n expect(result).toBe(DEVICE);\n });\n\n it(\"reads nothing without a device id\", async () => {\n const client = aClient(() => Promise.resolve(DEVICE));\n\n render(client, \"\");\n await flush();\n\n expect(result).toBeUndefined();\n expect(client.getDevice).not.toHaveBeenCalled();\n });\n\n it(\"retries a failed read until it succeeds\", async () => {\n const getDevice = vi\n .fn<() => Promise<Device>>()\n .mockRejectedValueOnce(new Error(\"offline\"))\n .mockResolvedValue(DEVICE);\n const client = aClient(getDevice);\n\n render(client);\n await flush();\n expect(result).toBeNull();\n\n await act(() => vi.advanceTimersByTimeAsync(2000));\n\n expect(result).toBe(DEVICE);\n expect(getDevice).toHaveBeenCalledTimes(2);\n });\n\n it(\"stops retrying once unmounted\", async () => {\n const client = aClient(() => Promise.reject(new Error(\"offline\")));\n\n render(client);\n await flush();\n act(() => root.render(<></>));\n await act(() => vi.advanceTimersByTimeAsync(10_000));\n\n expect(client.getDevice).toHaveBeenCalledOnce();\n });\n});\n"],
|
|
5
|
+
"mappings": "AAAA,OAAOA,GAAS,OAAAC,MAAW,QAC3B,OAAS,cAAAC,MAA6B,mBAItC,OAAS,uBAAAC,MAA2B,wBAEpC,OAAO,OAAO,WAAY,CAAE,yBAA0B,EAAK,CAAC,EAE5D,MAAMC,EAAS,CAAE,GAAI,WAAY,YAAa,MAAO,EAErD,SAAS,sBAAuB,IAAM,CACpC,IAAIC,EACAC,EACAC,EAEJ,MAAMC,EAAkE,CAAC,CACvE,OAAAC,EACA,SAAAC,CACF,KACEH,EAASJ,EAAoBM,EAAQC,CAAQ,EACtC,MAEHC,EAAWC,IACd,CAAE,UAAW,GAAG,GAAGA,CAAS,CAAE,GAC3BC,EAAS,CAACJ,EAA0BC,EAAW,aACnDT,EAAI,IAAMK,EAAK,OAAON,EAAA,cAACQ,EAAA,CAAM,OAAQC,EAAQ,SAAUC,EAAU,CAAE,CAAC,EAChEI,EAAQ,IAAMb,EAAI,IAAM,GAAG,yBAAyB,CAAC,CAAC,EAE5D,WAAW,IAAM,CACf,GAAG,cAAc,EACjBM,EAAS,OACTF,EAAY,SAAS,cAAc,KAAK,EACxCC,EAAOJ,EAAWG,CAAS,CAC7B,CAAC,EAED,UAAU,IAAM,CACdJ,EAAI,IAAMK,EAAK,QAAQ,CAAC,EACxB,GAAG,cAAc,CACnB,CAAC,EAED,GAAG,0BAA2B,SAAY,CACxC,MAAMG,EAASE,EAAQ,IAAM,QAAQ,QAAQP,CAAM,CAAC,EAEpDS,EAAOJ,CAAM,EACb,OAAOF,CAAM,EAAE,cAAc,EAC7B,MAAMO,EAAM,EAEZ,OAAOP,CAAM,EAAE,KAAKH,CAAM,EAC1B,OAAOK,EAAO,SAAS,EAAE,qBAAqB,UAAU,CAC1D,CAAC,EAED,GAAG,oDAAqD,SAAY,CAGlE,MAAMM,CAA4C,CAC/B,OAASX,EAC1B,UAAY,GAAG,GAAG,IAAM,QAAQ,QAAQ,KAAK,MAAM,CAAC,EACpD,cAAgB,GAAG,GAAsC,EACzD,YAAc,GAAG,GAAoC,EACrD,YAAc,GAAG,GAAoC,EACrD,SAAW,GAAG,GAAiC,CACjD,CACA,MAAMK,EAAS,IAAIM,EAEnBF,EAAOJ,CAAM,EACb,MAAMK,EAAM,EAEZ,OAAOP,CAAM,EAAE,KAAKH,CAAM,CAC5B,CAAC,EAED,GAAG,oCAAqC,SAAY,CAClD,MAAMK,EAASE,EAAQ,IAAM,QAAQ,QAAQP,CAAM,CAAC,EAEpDS,EAAOJ,EAAQ,EAAE,EACjB,MAAMK,EAAM,EAEZ,OAAOP,CAAM,EAAE,cAAc,EAC7B,OAAOE,EAAO,SAAS,EAAE,IAAI,iBAAiB,CAChD,CAAC,EAED,GAAG,0CAA2C,SAAY,CACxD,MAAMG,EAAY,GACf,GAA0B,EAC1B,sBAAsB,IAAI,MAAM,SAAS,CAAC,EAC1C,kBAAkBR,CAAM,EACrBK,EAASE,EAAQC,CAAS,EAEhCC,EAAOJ,CAAM,EACb,MAAMK,EAAM,EACZ,OAAOP,CAAM,EAAE,SAAS,EAExB,MAAMN,EAAI,IAAM,GAAG,yBAAyB,GAAI,CAAC,EAEjD,OAAOM,CAAM,EAAE,KAAKH,CAAM,EAC1B,OAAOQ,CAAS,EAAE,sBAAsB,CAAC,CAC3C,CAAC,EAED,GAAG,gCAAiC,SAAY,CAC9C,MAAMH,EAASE,EAAQ,IAAM,QAAQ,OAAO,IAAI,MAAM,SAAS,CAAC,CAAC,EAEjEE,EAAOJ,CAAM,EACb,MAAMK,EAAM,EACZb,EAAI,IAAMK,EAAK,OAAON,EAAA,cAAAA,EAAA,aAAE,CAAG,CAAC,EAC5B,MAAMC,EAAI,IAAM,GAAG,yBAAyB,GAAM,CAAC,EAEnD,OAAOQ,EAAO,SAAS,EAAE,qBAAqB,CAChD,CAAC,CACH,CAAC",
|
|
6
|
+
"names": ["React", "act", "createRoot", "useMockServerDevice", "DEVICE", "container", "root", "result", "Probe", "client", "deviceId", "aClient", "getDevice", "render", "flush", "OtherCopyClient"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{useCallback as d,useEffect as f,useRef as g,useState as p}from"react";const l="BOLOS",a=1e4,v=e=>(e.apps??[]).filter(({name:n})=>n!==l),O=e=>`https://cdn.live.ledger.com/icons/${e.toLowerCase().replaceAll(" ","_")}.png`;function T(e){const[n,t]=p(),[u,r]=p(),s=g(void 0);f(()=>()=>clearTimeout(s.current),[]);const c=d(o=>{!e||n||(t(o),r(void 0),e(o).then(()=>{s.current=setTimeout(()=>{t(void 0),r(`${o} opened but its screen never showed up`)},a)},i=>{t(void 0),r(i instanceof Error?i.message:String(i))}))},[e,n]);return{opening:n,error:u,open:c}}export{a as OPEN_APP_TIMEOUT_MS,O as appIconUrl,v as openableApps,T as useOpenApp};
|
|
2
|
+
//# sourceMappingURL=useOpenApp.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/useOpenApp.ts"],
|
|
4
|
+
"sourcesContent": ["import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { type Device } from \"@ledgerhq/device-mockserver-client\";\n\n/** The OS reports itself as an app; it is not one the user can open. */\nconst OS_APP_NAME = \"BOLOS\";\n\nexport const OPEN_APP_TIMEOUT_MS = 10_000;\n\n/** The apps a device lists that the user can open. */\nexport const openableApps = (device: Device): NonNullable<Device[\"apps\"]> =>\n (device.apps ?? []).filter(({ name }) => name !== OS_APP_NAME);\n\n/** Ledger Live's app icons, keyed by the app's Manager API icon name. */\nexport const appIconUrl = (appName: string): string =>\n `https://cdn.live.ledger.com/icons/${appName.toLowerCase().replaceAll(\" \", \"_\")}.png`;\n\nexport interface OpenApp {\n /**\n * The app being opened. Kept on success, until the live screen replaces the\n * app list or the wait times out.\n */\n opening: string | undefined;\n error: string | undefined;\n /** Ignored without a handler, or while another app is opening. */\n open(appName: string): void;\n}\n\n/** Omitted, the apps are listed but cannot be opened. */\nexport function useOpenApp(\n onOpenApp: ((appName: string) => Promise<void>) | undefined,\n): OpenApp {\n const [opening, setOpening] = useState<string>();\n const [error, setError] = useState<string>();\n\n const openTimer = useRef<ReturnType<typeof setTimeout> | undefined>(\n undefined,\n );\n useEffect(() => () => clearTimeout(openTimer.current), []);\n\n const open = useCallback(\n (appName: string) => {\n if (!onOpenApp || opening) return;\n setOpening(appName);\n setError(undefined);\n onOpenApp(appName).then(\n () => {\n openTimer.current = setTimeout(() => {\n setOpening(undefined);\n setError(`${appName} opened but its screen never showed up`);\n }, OPEN_APP_TIMEOUT_MS);\n },\n (cause: unknown) => {\n setOpening(undefined);\n setError(cause instanceof Error ? cause.message : String(cause));\n },\n );\n },\n [onOpenApp, opening],\n );\n\n return { opening, error, open };\n}\n"],
|
|
5
|
+
"mappings": "AAAA,OAAS,eAAAA,EAAa,aAAAC,EAAW,UAAAC,EAAQ,YAAAC,MAAgB,QAIzD,MAAMC,EAAc,QAEPC,EAAsB,IAGtBC,EAAgBC,IAC1BA,EAAO,MAAQ,CAAC,GAAG,OAAO,CAAC,CAAE,KAAAC,CAAK,IAAMA,IAASJ,CAAW,EAGlDK,EAAcC,GACzB,qCAAqCA,EAAQ,YAAY,EAAE,WAAW,IAAK,GAAG,CAAC,OAc1E,SAASC,EACdC,EACS,CACT,KAAM,CAACC,EAASC,CAAU,EAAIX,EAAiB,EACzC,CAACY,EAAOC,CAAQ,EAAIb,EAAiB,EAErCc,EAAYf,EAChB,MACF,EACAD,EAAU,IAAM,IAAM,aAAagB,EAAU,OAAO,EAAG,CAAC,CAAC,EAEzD,MAAMC,EAAOlB,EACVU,GAAoB,CACf,CAACE,GAAaC,IAClBC,EAAWJ,CAAO,EAClBM,EAAS,MAAS,EAClBJ,EAAUF,CAAO,EAAE,KACjB,IAAM,CACJO,EAAU,QAAU,WAAW,IAAM,CACnCH,EAAW,MAAS,EACpBE,EAAS,GAAGN,CAAO,wCAAwC,CAC7D,EAAGL,CAAmB,CACxB,EACCc,GAAmB,CAClBL,EAAW,MAAS,EACpBE,EAASG,aAAiB,MAAQA,EAAM,QAAU,OAAOA,CAAK,CAAC,CACjE,CACF,EACF,EACA,CAACP,EAAWC,CAAO,CACrB,EAEA,MAAO,CAAE,QAAAA,EAAS,MAAAE,EAAO,KAAAG,CAAK,CAChC",
|
|
6
|
+
"names": ["useCallback", "useEffect", "useRef", "useState", "OS_APP_NAME", "OPEN_APP_TIMEOUT_MS", "openableApps", "device", "name", "appIconUrl", "appName", "useOpenApp", "onOpenApp", "opening", "setOpening", "error", "setError", "openTimer", "open", "cause"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import a,{act as s}from"react";import{createRoot as d}from"react-dom/client";import{appIconUrl as l,OPEN_APP_TIMEOUT_MS as m,openableApps as c,useOpenApp as v}from"./useOpenApp";Object.assign(globalThis,{IS_REACT_ACT_ENVIRONMENT:!0});describe("openableApps",()=>{it("lists the installed apps, without the OS",()=>{const o={apps:[{name:"BOLOS",version:"1.0.0"},{name:"Bitcoin",version:"2.5.0"}]};expect(c(o)).toEqual([{name:"Bitcoin",version:"2.5.0"}])}),it("lists nothing for a device without apps",()=>{expect(c({})).toEqual([])})});describe("appIconUrl",()=>{it("points at Ledger Live's icon for the app",()=>{expect(l("Ethereum Classic")).toBe("https://cdn.live.ledger.com/icons/ethereum_classic.png")})});describe("useOpenApp",()=>{let o,t;const r=({onOpenApp:e})=>(t=v(e),null),i=e=>s(()=>o.render(a.createElement(r,{onOpenApp:e}))),n=e=>s(()=>t.open(e)),p=(e=0)=>s(()=>vi.advanceTimersByTimeAsync(e));beforeEach(()=>{vi.useFakeTimers(),o=d(document.createElement("div"))}),afterEach(()=>{s(()=>o.unmount()),vi.useRealTimers()}),it("keeps the app as opening once the device accepted it",async()=>{const e=vi.fn(()=>Promise.resolve());i(e),n("Bitcoin"),await p(),expect(e).toHaveBeenCalledWith("Bitcoin"),expect(t.opening).toBe("Bitcoin"),expect(t.error).toBeUndefined()}),it("opens one app at a time",async()=>{const e=vi.fn(()=>Promise.resolve());i(e),n("Bitcoin"),n("Ethereum"),await p(),expect(e).toHaveBeenCalledOnce()}),it("says why an app could not be opened",async()=>{i(()=>Promise.reject(new Error("Could not open Bitcoin (6807)"))),n("Bitcoin"),await p(),expect(t.opening).toBeUndefined(),expect(t.error).toBe("Could not open Bitcoin (6807)")}),it("gives up when an opened app never shows its screen",async()=>{i(()=>Promise.resolve()),n("Bitcoin"),await p(m),expect(t.opening).toBeUndefined(),expect(t.error).toBe("Bitcoin opened but its screen never showed up")}),it("opens nothing without a handler",()=>{i(),n("Bitcoin"),expect(t.opening).toBeUndefined()})});
|
|
2
|
+
//# sourceMappingURL=useOpenApp.test.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/useOpenApp.test.tsx"],
|
|
4
|
+
"sourcesContent": ["import React, { act } from \"react\";\nimport { createRoot, type Root } from \"react-dom/client\";\nimport { type Device } from \"@ledgerhq/device-mockserver-client\";\n\nimport {\n appIconUrl,\n OPEN_APP_TIMEOUT_MS,\n openableApps,\n type OpenApp,\n useOpenApp,\n} from \"./useOpenApp\";\n\nObject.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });\n\ndescribe(\"openableApps\", () => {\n it(\"lists the installed apps, without the OS\", () => {\n const device = {\n apps: [\n { name: \"BOLOS\", version: \"1.0.0\" },\n { name: \"Bitcoin\", version: \"2.5.0\" },\n ],\n } as Device;\n\n expect(openableApps(device)).toEqual([\n { name: \"Bitcoin\", version: \"2.5.0\" },\n ]);\n });\n\n it(\"lists nothing for a device without apps\", () => {\n expect(openableApps({} as Device)).toEqual([]);\n });\n});\n\ndescribe(\"appIconUrl\", () => {\n it(\"points at Ledger Live's icon for the app\", () => {\n expect(appIconUrl(\"Ethereum Classic\")).toBe(\n \"https://cdn.live.ledger.com/icons/ethereum_classic.png\",\n );\n });\n});\n\ndescribe(\"useOpenApp\", () => {\n let root: Root;\n let result: OpenApp;\n\n const Probe: React.FC<{ onOpenApp?: (name: string) => Promise<void> }> = ({\n onOpenApp,\n }) => {\n result = useOpenApp(onOpenApp);\n return null;\n };\n const render = (onOpenApp?: (name: string) => Promise<void>) =>\n act(() => root.render(<Probe onOpenApp={onOpenApp} />));\n const open = (name: string) => act(() => result.open(name));\n const wait = (ms = 0) => act(() => vi.advanceTimersByTimeAsync(ms));\n\n beforeEach(() => {\n vi.useFakeTimers();\n root = createRoot(document.createElement(\"div\"));\n });\n\n afterEach(() => {\n act(() => root.unmount());\n vi.useRealTimers();\n });\n\n it(\"keeps the app as opening once the device accepted it\", async () => {\n const onOpenApp = vi.fn(() => Promise.resolve());\n render(onOpenApp);\n\n open(\"Bitcoin\");\n await wait();\n\n expect(onOpenApp).toHaveBeenCalledWith(\"Bitcoin\");\n expect(result.opening).toBe(\"Bitcoin\");\n expect(result.error).toBeUndefined();\n });\n\n it(\"opens one app at a time\", async () => {\n const onOpenApp = vi.fn(() => Promise.resolve());\n render(onOpenApp);\n\n open(\"Bitcoin\");\n open(\"Ethereum\");\n await wait();\n\n expect(onOpenApp).toHaveBeenCalledOnce();\n });\n\n it(\"says why an app could not be opened\", async () => {\n render(() => Promise.reject(new Error(\"Could not open Bitcoin (6807)\")));\n\n open(\"Bitcoin\");\n await wait();\n\n expect(result.opening).toBeUndefined();\n expect(result.error).toBe(\"Could not open Bitcoin (6807)\");\n });\n\n it(\"gives up when an opened app never shows its screen\", async () => {\n render(() => Promise.resolve());\n\n open(\"Bitcoin\");\n await wait(OPEN_APP_TIMEOUT_MS);\n\n expect(result.opening).toBeUndefined();\n expect(result.error).toBe(\"Bitcoin opened but its screen never showed up\");\n });\n\n it(\"opens nothing without a handler\", () => {\n render();\n\n open(\"Bitcoin\");\n\n expect(result.opening).toBeUndefined();\n });\n});\n"],
|
|
5
|
+
"mappings": "AAAA,OAAOA,GAAS,OAAAC,MAAW,QAC3B,OAAS,cAAAC,MAA6B,mBAGtC,OACE,cAAAC,EACA,uBAAAC,EACA,gBAAAC,EAEA,cAAAC,MACK,eAEP,OAAO,OAAO,WAAY,CAAE,yBAA0B,EAAK,CAAC,EAE5D,SAAS,eAAgB,IAAM,CAC7B,GAAG,2CAA4C,IAAM,CACnD,MAAMC,EAAS,CACb,KAAM,CACJ,CAAE,KAAM,QAAS,QAAS,OAAQ,EAClC,CAAE,KAAM,UAAW,QAAS,OAAQ,CACtC,CACF,EAEA,OAAOF,EAAaE,CAAM,CAAC,EAAE,QAAQ,CACnC,CAAE,KAAM,UAAW,QAAS,OAAQ,CACtC,CAAC,CACH,CAAC,EAED,GAAG,0CAA2C,IAAM,CAClD,OAAOF,EAAa,CAAC,CAAW,CAAC,EAAE,QAAQ,CAAC,CAAC,CAC/C,CAAC,CACH,CAAC,EAED,SAAS,aAAc,IAAM,CAC3B,GAAG,2CAA4C,IAAM,CACnD,OAAOF,EAAW,kBAAkB,CAAC,EAAE,KACrC,wDACF,CACF,CAAC,CACH,CAAC,EAED,SAAS,aAAc,IAAM,CAC3B,IAAIK,EACAC,EAEJ,MAAMC,EAAmE,CAAC,CACxE,UAAAC,CACF,KACEF,EAASH,EAAWK,CAAS,EACtB,MAEHC,EAAUD,GACdV,EAAI,IAAMO,EAAK,OAAOR,EAAA,cAACU,EAAA,CAAM,UAAWC,EAAW,CAAE,CAAC,EAClDE,EAAQC,GAAiBb,EAAI,IAAMQ,EAAO,KAAKK,CAAI,CAAC,EACpDC,EAAO,CAACC,EAAK,IAAMf,EAAI,IAAM,GAAG,yBAAyBe,CAAE,CAAC,EAElE,WAAW,IAAM,CACf,GAAG,cAAc,EACjBR,EAAON,EAAW,SAAS,cAAc,KAAK,CAAC,CACjD,CAAC,EAED,UAAU,IAAM,CACdD,EAAI,IAAMO,EAAK,QAAQ,CAAC,EACxB,GAAG,cAAc,CACnB,CAAC,EAED,GAAG,uDAAwD,SAAY,CACrE,MAAMG,EAAY,GAAG,GAAG,IAAM,QAAQ,QAAQ,CAAC,EAC/CC,EAAOD,CAAS,EAEhBE,EAAK,SAAS,EACd,MAAME,EAAK,EAEX,OAAOJ,CAAS,EAAE,qBAAqB,SAAS,EAChD,OAAOF,EAAO,OAAO,EAAE,KAAK,SAAS,EACrC,OAAOA,EAAO,KAAK,EAAE,cAAc,CACrC,CAAC,EAED,GAAG,0BAA2B,SAAY,CACxC,MAAME,EAAY,GAAG,GAAG,IAAM,QAAQ,QAAQ,CAAC,EAC/CC,EAAOD,CAAS,EAEhBE,EAAK,SAAS,EACdA,EAAK,UAAU,EACf,MAAME,EAAK,EAEX,OAAOJ,CAAS,EAAE,qBAAqB,CACzC,CAAC,EAED,GAAG,sCAAuC,SAAY,CACpDC,EAAO,IAAM,QAAQ,OAAO,IAAI,MAAM,+BAA+B,CAAC,CAAC,EAEvEC,EAAK,SAAS,EACd,MAAME,EAAK,EAEX,OAAON,EAAO,OAAO,EAAE,cAAc,EACrC,OAAOA,EAAO,KAAK,EAAE,KAAK,+BAA+B,CAC3D,CAAC,EAED,GAAG,qDAAsD,SAAY,CACnEG,EAAO,IAAM,QAAQ,QAAQ,CAAC,EAE9BC,EAAK,SAAS,EACd,MAAME,EAAKX,CAAmB,EAE9B,OAAOK,EAAO,OAAO,EAAE,cAAc,EACrC,OAAOA,EAAO,KAAK,EAAE,KAAK,+CAA+C,CAC3E,CAAC,EAED,GAAG,kCAAmC,IAAM,CAC1CG,EAAO,EAEPC,EAAK,SAAS,EAEd,OAAOJ,EAAO,OAAO,EAAE,cAAc,CACvC,CAAC,CACH,CAAC",
|
|
6
|
+
"names": ["React", "act", "createRoot", "appIconUrl", "OPEN_APP_TIMEOUT_MS", "openableApps", "useOpenApp", "device", "root", "result", "Probe", "onOpenApp", "render", "open", "name", "wait", "ms"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{useCallback as k,useEffect as L,useMemo as D,useRef as o,useState as p}from"react";const w=500,P=2e3,R=3,T=n=>n instanceof Error?n.message:String(n);function b(n,y,s){const[h,a]=p({kind:"loading"}),u=o(null),S=o(!1),d=o(!1);d.current=h.kind==="image";const E=o(0),g=o(0),l=k(r=>{d.current&&++E.current<=R||a({kind:"error",message:T(r)})},[]),i=k(()=>{u.current&&(s.releaseImageSource?.(u.current),u.current=null)},[s]),I=o(()=>{}),m=D(()=>{const r=e=>void e.then(()=>I.current()).catch(l);return{pressButton:(e,t)=>r(n.pressButton(e,t)),touch:(e,t,c)=>r(n.touch(e,t,c))}},[n,l]),v=k(async()=>{if(S.current)return;S.current=!0;const r=g.current,e=()=>r!==g.current;try{const t=await n.screenshot();if(e())return;if(E.current=0,t){const c=await s.toImageSource(t);if(e()){s.releaseImageSource?.(c);return}c!==u.current&&(i(),u.current=c),a(f=>f.kind==="image"&&f.src===c&&f.input===m?f:{kind:"image",src:c,input:m})}else{const c=await n.idle?.();if(e())return;i(),a(c??{kind:"error",message:"No screen to capture"})}}catch(t){e()||l(t)}finally{S.current=!1}},[n,s,m,l,i]);return I.current=()=>void v(),L(()=>{if(!y){i(),a({kind:"loading"});return}let r,e=!1;const t=async()=>{s.isHidden()||await v(),!e&&(r=setTimeout(()=>void t(),d.current?w:P))};return t(),()=>{e=!0,clearTimeout(r),g.current++}},[y,s,v,i]),L(()=>i,[i]),h}export{b as useScreenPolling};
|
|
2
|
+
//# sourceMappingURL=useScreenPolling.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/useScreenPolling.ts"],
|
|
4
|
+
"sourcesContent": ["import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\n\nimport {\n type DeviceScreenInput,\n type DeviceScreenState,\n type ScreenApi,\n type ScreenPlatform,\n} from \"./types\";\n\nconst SCREEN_POLL_MS = 500;\nconst IDLE_POLL_MS = 2000;\n/**\n * An app quitting fails a few calls while Speculos shuts down; the last frame\n * stays up through them rather than flashing an error before the dashboard.\n */\nconst LIVE_FAILURES_TOLERATED = 3;\n\nconst describe = (error: unknown): string =>\n error instanceof Error ? error.message : String(error);\n\nexport function useScreenPolling(\n api: ScreenApi,\n polling: boolean,\n platform: ScreenPlatform,\n): DeviceScreenState {\n const [state, setState] = useState<DeviceScreenState>({ kind: \"loading\" });\n\n const imageSource = useRef<string | null>(null);\n const inFlight = useRef(false);\n const isLive = useRef(false);\n isLive.current = state.kind === \"image\";\n const liveFailures = useRef(0);\n /** Bumped when polling stops, so a response still in flight is dropped. */\n const generation = useRef(0);\n\n const fail = useCallback((error: unknown) => {\n if (isLive.current && ++liveFailures.current <= LIVE_FAILURES_TOLERATED) {\n return;\n }\n setState({ kind: \"error\", message: describe(error) });\n }, []);\n\n const releaseImageSource = useCallback(() => {\n if (imageSource.current) {\n platform.releaseImageSource?.(imageSource.current);\n imageSource.current = null;\n }\n }, [platform]);\n\n /**\n * Input handlers refresh straight after acting. Going through a ref keeps\n * them stable, so the <img> is not remounted on every poll.\n */\n const refreshRef = useRef<() => void>(() => {});\n\n const input = useMemo<DeviceScreenInput>(() => {\n const send = (call: Promise<void>) =>\n void call.then(() => refreshRef.current()).catch(fail);\n\n return {\n pressButton: (button, action) => send(api.pressButton(button, action)),\n touch: (x, y, action) => send(api.touch(x, y, action)),\n };\n }, [api, fail]);\n\n const refresh = useCallback(async () => {\n if (inFlight.current) return;\n inFlight.current = true;\n const started = generation.current;\n const stale = () => started !== generation.current;\n try {\n const blob = await api.screenshot();\n if (stale()) return;\n liveFailures.current = 0;\n if (blob) {\n const source = await platform.toImageSource(blob);\n if (stale()) {\n platform.releaseImageSource?.(source);\n return;\n }\n if (source !== imageSource.current) {\n releaseImageSource();\n imageSource.current = source;\n }\n // A source made from the bytes, like a data URI, repeats while the\n // screen holds still; keeping the state then skips a re-render.\n setState((current) =>\n current.kind === \"image\" &&\n current.src === source &&\n current.input === input\n ? current\n : { kind: \"image\", src: source, input },\n );\n } else {\n const idle = await api.idle?.();\n if (stale()) return;\n releaseImageSource();\n setState(idle ?? { kind: \"error\", message: \"No screen to capture\" });\n }\n } catch (error) {\n if (!stale()) fail(error);\n } finally {\n inFlight.current = false;\n }\n }, [api, platform, input, fail, releaseImageSource]);\n refreshRef.current = () => void refresh();\n\n useEffect(() => {\n if (!polling) {\n // Drop the frame rather than keep a released source around, which\n // would flash a broken image when the panel reopens.\n releaseImageSource();\n setState({ kind: \"loading\" });\n return;\n }\n\n let timer: ReturnType<typeof setTimeout>;\n let cancelled = false;\n\n const tick = async () => {\n if (!platform.isHidden()) await refresh();\n if (cancelled) return;\n timer = setTimeout(\n () => void tick(),\n isLive.current ? SCREEN_POLL_MS : IDLE_POLL_MS,\n );\n };\n void tick();\n\n return () => {\n cancelled = true;\n clearTimeout(timer);\n generation.current++;\n };\n }, [polling, platform, refresh, releaseImageSource]);\n\n useEffect(() => releaseImageSource, [releaseImageSource]);\n\n return state;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["useCallback", "useEffect", "useMemo", "useRef", "useState", "SCREEN_POLL_MS", "IDLE_POLL_MS", "LIVE_FAILURES_TOLERATED", "describe", "error", "useScreenPolling", "api", "polling", "platform", "state", "setState", "imageSource", "inFlight", "isLive", "liveFailures", "generation", "fail", "releaseImageSource", "refreshRef", "input", "send", "call", "button", "action", "x", "y", "refresh", "started", "stale", "blob", "source", "current", "idle", "timer", "cancelled", "tick"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import S,{act as u}from"react";import{createRoot as v}from"react-dom/client";import{useScreenPolling as x}from"./useScreenPolling";Object.assign(globalThis,{IS_REACT_ACT_ENVIRONMENT:!0});const f=new Blob(["frame"]),r=(i={})=>({screenshot:vi.fn(()=>Promise.resolve(f)),pressButton:vi.fn(()=>Promise.resolve()),touch:vi.fn(()=>Promise.resolve()),...i});describe("useScreenPolling",()=>{let i,c,l,p;const s=vi.fn(),n=vi.fn(),w={toImageSource:s,releaseImageSource:n,isHidden:()=>p},g=({api:e,polling:o})=>(l.push(x(e,o,w)),null),a=(e,o=!0)=>u(()=>c.render(S.createElement(g,{api:e,polling:o}))),t=(e=0)=>u(()=>vi.advanceTimersByTimeAsync(e)),h=()=>t(2e3),m=()=>l[l.length-1];beforeEach(()=>{vi.useFakeTimers(),l=[],p=!1;let e=0;s.mockReset(),s.mockImplementation(()=>Promise.resolve(`source-${++e}`)),n.mockReset(),i=document.createElement("div"),c=v(i)}),afterEach(()=>{u(()=>c.unmount()),vi.useRealTimers()}),it("shows a frame through the platform's image source",async()=>{a(r()),await t(),expect(s).toHaveBeenCalledWith(f),expect(m()).toMatchObject({kind:"image",src:"source-1"})}),it("releases a frame's source once the next one replaces it",async()=>{a(r()),await t(),await h(),expect(m()).toMatchObject({kind:"image",src:"source-2"}),expect(n).toHaveBeenCalledExactlyOnceWith("source-1")}),it("keeps its state while the frame stays the same",async()=>{s.mockResolvedValue("data:same"),a(r()),await t();const e=m();await h(),await t(500),expect(s).toHaveBeenCalledTimes(3);const o=l.filter(({kind:d})=>d==="image");expect(new Set(o)).toEqual(new Set([e])),expect(n).not.toHaveBeenCalled()}),it("releases the source when the app quits",async()=>{const e=vi.fn().mockResolvedValueOnce(f).mockResolvedValue(null);a(r({screenshot:e})),await t(),await h(),expect(m()).toEqual({kind:"error",message:"No screen to capture"}),expect(n).toHaveBeenCalledWith("source-1")}),it("releases the source when unmounted",async()=>{a(r()),await t(),u(()=>c.unmount()),c=v(i),expect(n).toHaveBeenCalledWith("source-1")}),it("releases a frame made after polling stopped, without showing it",async()=>{let e=()=>{};s.mockReturnValue(new Promise(d=>e=d));const o=r();a(o),await t(),a(o,!1),e("late"),await t(),expect(m()).toEqual({kind:"loading"}),expect(n).toHaveBeenCalledWith("late")}),it("skips its turns while the platform is hidden",async()=>{p=!0;const e=r();a(e),await t(1e4),expect(e.screenshot).not.toHaveBeenCalled(),p=!1,await t(2e3),expect(e.screenshot).toHaveBeenCalledOnce()})});
|
|
2
|
+
//# sourceMappingURL=useScreenPolling.test.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/useScreenPolling.test.tsx"],
|
|
4
|
+
"sourcesContent": ["import React, { act } from \"react\";\nimport { createRoot, type Root } from \"react-dom/client\";\n\nimport {\n type DeviceScreenState,\n type ScreenApi,\n type ScreenPlatform,\n} from \"./types\";\nimport { useScreenPolling } from \"./useScreenPolling\";\n\nObject.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });\n\nconst FRAME = new Blob([\"frame\"]);\n\nconst anApi = (overrides: Partial<ScreenApi> = {}): ScreenApi => ({\n screenshot: vi.fn(() => Promise.resolve(FRAME)),\n pressButton: vi.fn(() => Promise.resolve()),\n touch: vi.fn(() => Promise.resolve()),\n ...overrides,\n});\n\ndescribe(\"useScreenPolling\", () => {\n let container: HTMLDivElement;\n let root: Root;\n let states: DeviceScreenState[];\n let hidden: boolean;\n const toImageSource = vi.fn<ScreenPlatform[\"toImageSource\"]>();\n const releaseImageSource = vi.fn<(source: string) => void>();\n const platform: ScreenPlatform = {\n toImageSource,\n releaseImageSource,\n isHidden: () => hidden,\n };\n\n const Probe: React.FC<{ api: ScreenApi; polling: boolean }> = ({\n api,\n polling,\n }) => {\n states.push(useScreenPolling(api, polling, platform));\n return null;\n };\n const render = (api: ScreenApi, polling = true) =>\n act(() => root.render(<Probe api={api} polling={polling} />));\n const wait = (ms = 0) => act(() => vi.advanceTimersByTimeAsync(ms));\n /**\n * The turn after the first frame comes at the idle pace: the delay is picked\n * before the frame renders. Live turns follow every 500ms.\n */\n const nextTurn = () => wait(2000);\n const latest = () => states[states.length - 1];\n\n beforeEach(() => {\n vi.useFakeTimers();\n states = [];\n hidden = false;\n let frames = 0;\n toImageSource.mockReset();\n toImageSource.mockImplementation(() =>\n Promise.resolve(`source-${++frames}`),\n );\n releaseImageSource.mockReset();\n container = document.createElement(\"div\");\n root = createRoot(container);\n });\n\n afterEach(() => {\n act(() => root.unmount());\n vi.useRealTimers();\n });\n\n it(\"shows a frame through the platform's image source\", async () => {\n render(anApi());\n await wait();\n\n expect(toImageSource).toHaveBeenCalledWith(FRAME);\n expect(latest()).toMatchObject({ kind: \"image\", src: \"source-1\" });\n });\n\n it(\"releases a frame's source once the next one replaces it\", async () => {\n render(anApi());\n await wait();\n await nextTurn();\n\n expect(latest()).toMatchObject({ kind: \"image\", src: \"source-2\" });\n expect(releaseImageSource).toHaveBeenCalledExactlyOnceWith(\"source-1\");\n });\n\n it(\"keeps its state while the frame stays the same\", async () => {\n toImageSource.mockResolvedValue(\"data:same\");\n render(anApi());\n await wait();\n const frame = latest();\n\n await nextTurn();\n await wait(500);\n\n expect(toImageSource).toHaveBeenCalledTimes(3);\n // React may render once more before bailing out, but with the same state:\n // the image never gets a new source to draw.\n const frames = states.filter(({ kind }) => kind === \"image\");\n expect(new Set(frames)).toEqual(new Set([frame]));\n expect(releaseImageSource).not.toHaveBeenCalled();\n });\n\n it(\"releases the source when the app quits\", async () => {\n const screenshot = vi\n .fn<ScreenApi[\"screenshot\"]>()\n .mockResolvedValueOnce(FRAME)\n .mockResolvedValue(null);\n render(anApi({ screenshot }));\n await wait();\n await nextTurn();\n\n expect(latest()).toEqual({\n kind: \"error\",\n message: \"No screen to capture\",\n });\n expect(releaseImageSource).toHaveBeenCalledWith(\"source-1\");\n });\n\n it(\"releases the source when unmounted\", async () => {\n render(anApi());\n await wait();\n\n act(() => root.unmount());\n root = createRoot(container);\n\n expect(releaseImageSource).toHaveBeenCalledWith(\"source-1\");\n });\n\n it(\"releases a frame made after polling stopped, without showing it\", async () => {\n let finish: (source: string) => void = () => {};\n toImageSource.mockReturnValue(new Promise((resolve) => (finish = resolve)));\n const api = anApi();\n render(api);\n await wait();\n\n render(api, false);\n finish(\"late\");\n await wait();\n\n expect(latest()).toEqual({ kind: \"loading\" });\n expect(releaseImageSource).toHaveBeenCalledWith(\"late\");\n });\n\n it(\"skips its turns while the platform is hidden\", async () => {\n hidden = true;\n const api = anApi();\n render(api);\n await wait(10_000);\n expect(api.screenshot).not.toHaveBeenCalled();\n\n hidden = false;\n await wait(2000);\n\n expect(api.screenshot).toHaveBeenCalledOnce();\n });\n});\n"],
|
|
5
|
+
"mappings": "AAAA,OAAOA,GAAS,OAAAC,MAAW,QAC3B,OAAS,cAAAC,MAA6B,mBAOtC,OAAS,oBAAAC,MAAwB,qBAEjC,OAAO,OAAO,WAAY,CAAE,yBAA0B,EAAK,CAAC,EAE5D,MAAMC,EAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,EAE1BC,EAAQ,CAACC,EAAgC,CAAC,KAAkB,CAChE,WAAY,GAAG,GAAG,IAAM,QAAQ,QAAQF,CAAK,CAAC,EAC9C,YAAa,GAAG,GAAG,IAAM,QAAQ,QAAQ,CAAC,EAC1C,MAAO,GAAG,GAAG,IAAM,QAAQ,QAAQ,CAAC,EACpC,GAAGE,CACL,GAEA,SAAS,mBAAoB,IAAM,CACjC,IAAIC,EACAC,EACAC,EACAC,EACJ,MAAMC,EAAgB,GAAG,GAAoC,EACvDC,EAAqB,GAAG,GAA6B,EACrDC,EAA2B,CAC/B,cAAAF,EACA,mBAAAC,EACA,SAAU,IAAMF,CAClB,EAEMI,EAAwD,CAAC,CAC7D,IAAAC,EACA,QAAAC,CACF,KACEP,EAAO,KAAKN,EAAiBY,EAAKC,EAASH,CAAQ,CAAC,EAC7C,MAEHI,EAAS,CAACF,EAAgBC,EAAU,KACxCf,EAAI,IAAMO,EAAK,OAAOR,EAAA,cAACc,EAAA,CAAM,IAAKC,EAAK,QAASC,EAAS,CAAE,CAAC,EACxDE,EAAO,CAACC,EAAK,IAAMlB,EAAI,IAAM,GAAG,yBAAyBkB,CAAE,CAAC,EAK5DC,EAAW,IAAMF,EAAK,GAAI,EAC1BG,EAAS,IAAMZ,EAAOA,EAAO,OAAS,CAAC,EAE7C,WAAW,IAAM,CACf,GAAG,cAAc,EACjBA,EAAS,CAAC,EACVC,EAAS,GACT,IAAIY,EAAS,EACbX,EAAc,UAAU,EACxBA,EAAc,mBAAmB,IAC/B,QAAQ,QAAQ,UAAU,EAAEW,CAAM,EAAE,CACtC,EACAV,EAAmB,UAAU,EAC7BL,EAAY,SAAS,cAAc,KAAK,EACxCC,EAAON,EAAWK,CAAS,CAC7B,CAAC,EAED,UAAU,IAAM,CACdN,EAAI,IAAMO,EAAK,QAAQ,CAAC,EACxB,GAAG,cAAc,CACnB,CAAC,EAED,GAAG,oDAAqD,SAAY,CAClES,EAAOZ,EAAM,CAAC,EACd,MAAMa,EAAK,EAEX,OAAOP,CAAa,EAAE,qBAAqBP,CAAK,EAChD,OAAOiB,EAAO,CAAC,EAAE,cAAc,CAAE,KAAM,QAAS,IAAK,UAAW,CAAC,CACnE,CAAC,EAED,GAAG,0DAA2D,SAAY,CACxEJ,EAAOZ,EAAM,CAAC,EACd,MAAMa,EAAK,EACX,MAAME,EAAS,EAEf,OAAOC,EAAO,CAAC,EAAE,cAAc,CAAE,KAAM,QAAS,IAAK,UAAW,CAAC,EACjE,OAAOT,CAAkB,EAAE,gCAAgC,UAAU,CACvE,CAAC,EAED,GAAG,iDAAkD,SAAY,CAC/DD,EAAc,kBAAkB,WAAW,EAC3CM,EAAOZ,EAAM,CAAC,EACd,MAAMa,EAAK,EACX,MAAMK,EAAQF,EAAO,EAErB,MAAMD,EAAS,EACf,MAAMF,EAAK,GAAG,EAEd,OAAOP,CAAa,EAAE,sBAAsB,CAAC,EAG7C,MAAMW,EAASb,EAAO,OAAO,CAAC,CAAE,KAAAe,CAAK,IAAMA,IAAS,OAAO,EAC3D,OAAO,IAAI,IAAIF,CAAM,CAAC,EAAE,QAAQ,IAAI,IAAI,CAACC,CAAK,CAAC,CAAC,EAChD,OAAOX,CAAkB,EAAE,IAAI,iBAAiB,CAClD,CAAC,EAED,GAAG,yCAA0C,SAAY,CACvD,MAAMa,EAAa,GAChB,GAA4B,EAC5B,sBAAsBrB,CAAK,EAC3B,kBAAkB,IAAI,EACzBa,EAAOZ,EAAM,CAAE,WAAAoB,CAAW,CAAC,CAAC,EAC5B,MAAMP,EAAK,EACX,MAAME,EAAS,EAEf,OAAOC,EAAO,CAAC,EAAE,QAAQ,CACvB,KAAM,QACN,QAAS,sBACX,CAAC,EACD,OAAOT,CAAkB,EAAE,qBAAqB,UAAU,CAC5D,CAAC,EAED,GAAG,qCAAsC,SAAY,CACnDK,EAAOZ,EAAM,CAAC,EACd,MAAMa,EAAK,EAEXjB,EAAI,IAAMO,EAAK,QAAQ,CAAC,EACxBA,EAAON,EAAWK,CAAS,EAE3B,OAAOK,CAAkB,EAAE,qBAAqB,UAAU,CAC5D,CAAC,EAED,GAAG,kEAAmE,SAAY,CAChF,IAAIc,EAAmC,IAAM,CAAC,EAC9Cf,EAAc,gBAAgB,IAAI,QAASgB,GAAaD,EAASC,CAAQ,CAAC,EAC1E,MAAMZ,EAAMV,EAAM,EAClBY,EAAOF,CAAG,EACV,MAAMG,EAAK,EAEXD,EAAOF,EAAK,EAAK,EACjBW,EAAO,MAAM,EACb,MAAMR,EAAK,EAEX,OAAOG,EAAO,CAAC,EAAE,QAAQ,CAAE,KAAM,SAAU,CAAC,EAC5C,OAAOT,CAAkB,EAAE,qBAAqB,MAAM,CACxD,CAAC,EAED,GAAG,+CAAgD,SAAY,CAC7DF,EAAS,GACT,MAAMK,EAAMV,EAAM,EAClBY,EAAOF,CAAG,EACV,MAAMG,EAAK,GAAM,EACjB,OAAOH,EAAI,UAAU,EAAE,IAAI,iBAAiB,EAE5CL,EAAS,GACT,MAAMQ,EAAK,GAAI,EAEf,OAAOH,EAAI,UAAU,EAAE,qBAAqB,CAC9C,CAAC,CACH,CAAC",
|
|
6
|
+
"names": ["React", "act", "createRoot", "useScreenPolling", "FRAME", "anApi", "overrides", "container", "root", "states", "hidden", "toImageSource", "releaseImageSource", "platform", "Probe", "api", "polling", "render", "wait", "ms", "nextTurn", "latest", "frames", "frame", "kind", "screenshot", "finish", "resolve"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{useMemo as h}from"react";import{useHold as m}from"./useHold";const c=(o,n,t,e)=>{if(!t.width||!t.height||!e.width||!e.height)return null;const i=(r,u)=>Math.min(Math.max(Math.round(r),0),u-1);return{x:i(o/t.width*e.width,e.width),y:i(n/t.height*e.height,e.height)}};function p(o){const n=h(()=>o&&((t,e)=>o(t.x,t.y,e)),[o]);return m(n)}export{c as toScreenPoint,p as useTouchHold};
|
|
2
|
+
//# sourceMappingURL=useTouchHold.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/useTouchHold.ts"],
|
|
4
|
+
"sourcesContent": ["import { useMemo } from \"react\";\nimport { type SpeculosAction } from \"@ledgerhq/device-mockserver-client\";\n\nimport { type Hold, useHold } from \"./useHold\";\n\nexport interface Size {\n readonly width: number;\n readonly height: number;\n}\n\nexport interface ScreenPoint {\n readonly x: number;\n readonly y: number;\n}\n\n/**\n * Where an offset into the screen as drawn lands in device pixels, or null\n * while either size is unknown.\n */\nexport const toScreenPoint = (\n offsetX: number,\n offsetY: number,\n drawn: Size,\n screen: Size,\n): ScreenPoint | null => {\n if (!drawn.width || !drawn.height || !screen.width || !screen.height) {\n return null;\n }\n // The far edge maps to the pixel count itself, one past the last pixel.\n const clamp = (value: number, size: number) =>\n Math.min(Math.max(Math.round(value), 0), size - 1);\n return {\n x: clamp((offsetX / drawn.width) * screen.width, screen.width),\n y: clamp((offsetY / drawn.height) * screen.height, screen.height),\n };\n};\n\nexport type TouchHold = Hold<ScreenPoint>;\n\n/**\n * A finger's down and up on the screen, as the actions the device gets: a\n * quick tap as one press-and-release, a hold as a press then a release on the\n * spot where it went down.\n */\nexport function useTouchHold(\n onTouch: ((x: number, y: number, action: SpeculosAction) => void) | undefined,\n): TouchHold {\n const onAction = useMemo(\n () =>\n onTouch &&\n ((point: ScreenPoint, action: SpeculosAction) =>\n onTouch(point.x, point.y, action)),\n [onTouch],\n );\n return useHold(onAction);\n}\n"],
|
|
5
|
+
"mappings": "AAAA,OAAS,WAAAA,MAAe,QAGxB,OAAoB,WAAAC,MAAe,YAgB5B,MAAMC,EAAgB,CAC3BC,EACAC,EACAC,EACAC,IACuB,CACvB,GAAI,CAACD,EAAM,OAAS,CAACA,EAAM,QAAU,CAACC,EAAO,OAAS,CAACA,EAAO,OAC5D,OAAO,KAGT,MAAMC,EAAQ,CAACC,EAAeC,IAC5B,KAAK,IAAI,KAAK,IAAI,KAAK,MAAMD,CAAK,EAAG,CAAC,EAAGC,EAAO,CAAC,EACnD,MAAO,CACL,EAAGF,EAAOJ,EAAUE,EAAM,MAASC,EAAO,MAAOA,EAAO,KAAK,EAC7D,EAAGC,EAAOH,EAAUC,EAAM,OAAUC,EAAO,OAAQA,EAAO,MAAM,CAClE,CACF,EASO,SAASI,EACdC,EACW,CACX,MAAMC,EAAWZ,EACf,IACEW,IACC,CAACE,EAAoBC,IACpBH,EAAQE,EAAM,EAAGA,EAAM,EAAGC,CAAM,GACpC,CAACH,CAAO,CACV,EACA,OAAOV,EAAQW,CAAQ,CACzB",
|
|
6
|
+
"names": ["useMemo", "useHold", "toScreenPoint", "offsetX", "offsetY", "drawn", "screen", "clamp", "value", "size", "useTouchHold", "onTouch", "onAction", "point", "action"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import h,{act as l}from"react";import{createRoot as r}from"react-dom/client";import{HOLD_THRESHOLD_MS as i}from"./useHold";import{toScreenPoint as n,useTouchHold as d}from"./useTouchHold";Object.assign(globalThis,{IS_REACT_ACT_ENVIRONMENT:!0});describe("toScreenPoint",()=>{const t={width:200,height:300},e={width:400,height:600};it("scales a point on the drawn screen to device pixels",()=>{expect(n(50,150,t,e)).toEqual({x:100,y:300})}),it("keeps the far edges on the last pixel",()=>{expect(n(200,300,t,e)).toEqual({x:399,y:599}),expect(n(-5,-5,t,e)).toEqual({x:0,y:0})}),it("has no point while a size is unknown",()=>{expect(n(50,50,{width:0,height:0},e)).toBeNull(),expect(n(50,50,t,{width:0,height:0})).toBeNull()})});describe("useTouchHold",()=>{let t,e;const o=vi.fn(),c=({handler:s})=>(e=d(s),null),a=s=>l(()=>t.render(h.createElement(c,{handler:s})));beforeEach(()=>{vi.useFakeTimers(),o.mockReset(),t=r(document.createElement("div"))}),afterEach(()=>{l(()=>t.unmount()),vi.useRealTimers()}),it("sends a quick tap as one press-and-release",()=>{a(o),expect(e.hold({x:10,y:20})).toBe(!0),expect(e.release()).toBe(!0),expect(o).toHaveBeenCalledExactlyOnceWith(10,20,"press-and-release")}),it("sends a hold as a press, then a release where the finger went down",()=>{a(o),e.hold({x:10,y:20}),vi.advanceTimersByTime(i),e.release(),expect(o.mock.calls).toEqual([[10,20,"press"],[10,20,"release"]])}),it("ignores a second finger while one is down",()=>{a(o),e.hold({x:10,y:20}),expect(e.hold({x:30,y:40})).toBe(!1),e.release(),expect(o).toHaveBeenCalledExactlyOnceWith(10,20,"press-and-release")}),it("ignores touches without a handler",()=>{a(),expect(e.hold({x:10,y:20})).toBe(!1),expect(e.release()).toBe(!1)}),it("releases a held finger when unmounted",()=>{a(o),e.hold({x:10,y:20}),vi.advanceTimersByTime(i),l(()=>t.unmount()),t=r(document.createElement("div")),expect(o).toHaveBeenLastCalledWith(10,20,"release")})});
|
|
2
|
+
//# sourceMappingURL=useTouchHold.test.js.map
|