ReviewOS

also looking at this

craft-native/craft

fix(android): hand the launch link to the page that subscribes for it

#232
Merged glennmichael123 wants to merge fix/android-deep-link-replay into main
17 files +776 -136
packages/ios/src/page-script.test.tsmodified+46-5
Changes to packages/ios/src/page-script.test.ts
@@ -23,17 +23,22 @@ function pageScript(): string {
2323
2424interface Post { action: string, callbackId?: string, [key: string]: unknown }
2525
26function loadPage() {
26type Listener = (event: { type: string, detail: unknown }) => void
27
28/** `beforeInject` runs first, the way a page's own script runs before WebKit's didFinish injects the bridge. */
29function loadPage(beforeInject?: (page: Record<string, any>) => void) {
2730 const posts: Post[] = []
28 const listeners: Record<string, ((event: { type: string, detail: unknown }) => void)[]> = {}
31 const listeners: Record<string, Listener[]> = {}
2932 const page: Record<string, any> = {
3033 webkit: { messageHandlers: { craft: { postMessage: (message: Post) => posts.push(message) } } },
31 addEventListener: (type: string, listener: (event: { type: string, detail: unknown }) => void) => {
34 addEventListener: (type: string, listener: Listener) => {
3235 (listeners[type] ||= []).push(listener)
3336 },
34 removeEventListener: () => {},
37 removeEventListener: (type: string, listener: Listener) => {
38 listeners[type] = (listeners[type] || []).filter(existing => existing !== listener)
39 },
3540 dispatchEvent: (event: { type: string, detail: unknown }) => {
36 for (const listener of listeners[event.type] || []) listener(event)
41 for (const listener of [...(listeners[event.type] || [])]) listener(event)
3742 return true
3843 },
3944 location: { href: 'craft://app/' },
@@ -47,6 +52,7 @@ function loadPage() {
4752 }
4853 }
4954 const quiet = { log() {}, warn() {}, error() {} }
55 beforeInject?.(page)
5056 // eslint-disable-next-line no-new-func
5157 new Function('window', 'document', 'navigator', 'CustomEvent', 'console', pageScript())(
5258 page,
@@ -69,6 +75,9 @@ function loadPage() {
6975 // What Swift's resolveCallback and rejectCallback evaluate.
7076 answer: (action: string, value: unknown) => craft._resolveCallback(last(action).callbackId, value),
7177 refuse: (action: string, message: string, code: string) => craft._rejectCallback(last(action).callbackId, message, code),
78 // What DeepLinkManager.dispatchDeepLink evaluates, once the script has run.
79 link: (url: string, initial: boolean) =>
80 page.dispatchEvent(new CustomEvent('craftDeepLink', { detail: { url, initial } })),
7281 }
7382}
7483
@@ -144,4 +153,36 @@ describe('the injected iOS page script', () => {
144153 await expect(settled).rejects.toMatchObject({ code: 'NATIVE_CALL_FAILED' })
145154 })
146155 }
156
157 // #198 and #215. The Android page script runs the same block; its own test
158 // covers the rest of the contract.
159 const tick = () => new Promise(resolve => setTimeout(resolve, 5))
160
161 it('hands the launch link to a page that subscribes after it arrived', async () => {
162 const page = loadPage()
163 page.link('app://launch', true)
164
165 const seen: unknown[] = []
166 page.craft.deepLinks.onLink((detail: unknown) => seen.push(detail))
167 await tick()
168
169 expect(seen).toEqual([{ url: 'app://launch', initial: true }])
170 })
171
172 it('delivers the launch link once to a craftReady handler that asks for it both ways', async () => {
173 // The handler runs inside the script, before native has dispatched the
174 // link at all, so there is nothing held yet for getInitialURL to claim.
175 const seen: unknown[] = []
176 const page = loadPage((window) => {
177 window.addEventListener('craftReady', () => {
178 void window.craft.deepLinks.getInitialURL()
179 window.craft.deepLinks.onLink((detail: unknown) => seen.push(detail))
180 })
181 })
182 page.link('app://launch', true)
183 await tick()
184
185 expect(seen).toEqual([])
186 expect(page.last('getInitialURL').callbackId).toMatch(/^cb_\d+$/)
187 })
147188})