Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion packages/shared/src/point-picker/point-picker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@ function mount(options: PointPickerOptions, o: MountOpts = {}) {
const message = (data: Record<string, unknown>, origin = PARENT, source: unknown = parent) => {
const ev = new win.MessageEvent('message', { data: { [field]: options.channel, ...data } });
Object.defineProperty(ev, 'origin', { value: origin });
Object.defineProperty(ev, 'source', { value: o.rn ? null : source });
// A top-level WebView: the host dispatches from the window itself (its own parent); a
// caller passing `win` says so, the default stands for "not the parent".
Object.defineProperty(ev, 'source', { value: o.rn && source === parent ? null : source });
win.dispatchEvent(ev);
};
/** The page's element (must exist). */
Expand Down Expand Up @@ -175,6 +177,20 @@ describe('pointPickerScript — single mode (Remix)', () => {
expect(h.win.__codeamInspector).toBeUndefined();
expect(Object.isFrozen(h.win.__remixPoint)).toBe(true);
});

it("inside a React Native WebView (the phone's Point at it) it answers over the bridge, no API needed", () => {
// Remix's phone app drives the picker with an `enable` dispatched from the window itself (its
// parent) and reads `ReactNativeWebView.postMessage`; before 2026-11-01 `send` dropped every
// reply of a top-level document without `globalName`, so the phone never saw `on` nor `pick`.
const h = mount({ ...REMIX, globalName: undefined }, { rn: true, html: '<button id="go" data-box="10"><span>Start</span></button>' });
h.message({ type: 'enable', accent: '#FF5A1F' }, PARENT, h.win);
expect(h.sent[0]).toEqual({ msg: { channel: 'remix-point', type: 'on' }, origin: 'rn' });
h.tap(h.q('#go span'));
expect(h.types()).toEqual(['on', 'pick']);
expect(h.picks()[0]).toMatchObject({ tag: 'button', id: 'go', text: 'Start' });
expect(h.sent[1].origin).toBe('rn');
expect(h.win.__codeamInspector).toBeUndefined();
});
});

describe('pointPickerScript — the shared look', () => {
Expand Down
14 changes: 10 additions & 4 deletions packages/shared/src/point-picker/script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,10 @@ export function pointPickerScript(opts: PointPickerOptions): string {

// Inside a React Native WebView the document is TOP-LEVEL (window.parent ===
// window), so the only way out is the bridge RN injects. It carries STRINGS.
// Any mode may answer through it: the host app owns the WebView, there is no
// third party to keep out — only the control API (the door below) needs
// \`globalName\`, because a global turns the picker on with nobody's permission.
function rnBridge() {
if (!GLOBAL) return null;
var rn = W.ReactNativeWebView;
return rn && typeof rn.postMessage === 'function' ? rn : null;
}
Expand All @@ -159,13 +161,17 @@ export function pointPickerScript(opts: PointPickerOptions): string {
msg[FIELD] = CH;
msg.type = type;
if (extra) for (var k in extra) msg[k] = extra[k];
// A parent window first (the feed framing the app; a host that installed
// one); only a top-level document falls back to the React Native bridge
// (2026-11-01: Remix's phone app got no \`on\` and no \`pick\` here).
if (W.parent !== W) {
try { W.parent.postMessage(msg, driver); } catch (e) {}
return;
}
var rn = rnBridge();
if (rn) {
try { rn.postMessage(JSON.stringify(msg)); } catch (e) {}
return;
}
if (W.parent === W) return;
try { W.parent.postMessage(msg, driver); } catch (e) {}
}

function clip(s, max) { return s.length > max ? s.slice(0, max - 1) + '\\u2026' : s; }
Expand Down
Loading