ReviewOS

also looking at this

craft-native/craft

fix(android): tie a ready mark to its own navigation, and inject a document once

#239
Merged glennmichael123 wants to merge fix/android-injection-generation into main
5 files +159 -3
packages/android/src/page-script.test.tsmodified+49-1
Changes to packages/android/src/page-script.test.ts
@@ -30,6 +30,18 @@ function pageScript(enabled = true): string {
3030 return script
3131}
3232
33/**
34 * The script as Kotlin actually evaluates it: wrapped by `onlyOnce`, which is
35 * what keeps a second injection into the same document from tearing the
36 * bridge down and building it again (#228).
37 */
38function injectedScript(enabled = true): string {
39 const wrapper = template.match(/private fun onlyOnce\(script: String\): String =\n\s*"([^"]*)\$script([^"]*)"/)
40 if (!wrapper) throw new Error('CraftBridge.kt.template no longer guards the script with onlyOnce')
41 const unescape = (part: string) => part.replace(/\\n/g, '\n')
42 return `${unescape(wrapper[1]!)}${pageScript(enabled)}${unescape(wrapper[2]!)}`
43}
44
3345type Listener = (event: { type: string, detail: unknown }) => void
3446
3547/**
@@ -84,7 +96,7 @@ function loadPage(
8496
8597 const inject = () => {
8698 // eslint-disable-next-line no-new-func
87 new Function('window', 'document', 'CraftAndroid', 'CustomEvent', 'console', pageScript(enabled))(
99 new Function('window', 'document', 'CraftAndroid', 'CustomEvent', 'console', injectedScript(enabled))(
88100 page,
89101 document,
90102 CraftAndroid,
@@ -371,4 +383,40 @@ describe('the injected Android page script', () => {
371383 expect(first).toEqual([{ url: 'app://warm', initial: false }])
372384 expect(second).toEqual([])
373385 })
386
387 // #228: Chromium calls onPageFinished without onPageStarted for a
388 // same-document navigation — a fragment change, a history.pushState — and
389 // MainActivity injects on every one. Running the script again used to
390 // replace window.craft, fire craftReady twice, and reject everything in
391 // flight with "Android bridge reinitialized".
392 it('leaves the bridge alone when the same document is injected again', async () => {
393 const page = loadPage()
394 const installed = page.craft
395 let readyAgain = 0
396 page.page.addEventListener('craftReady', () => { readyAgain += 1 })
397
398 page.inject()
399
400 expect(page.craft).toBe(installed)
401 expect(readyAgain).toBe(0)
402 })
403
404 it('does not reject an in-flight promise when the same document is injected again', async () => {
405 const page = loadPage()
406 const pending = page.craft.share('hello')
407 page.inject()
408
409 // Settle it the way Kotlin does. A second injection used to reject this
410 // first, and the page would have seen a share it never cancelled fail.
411 page.page._craftShareResolve(true)
412 expect(await pending).toBe(true)
413 })
414
415 it('installs into a document that has never had it', async () => {
416 // The other half: a real navigation gets a new window, so the flag is
417 // absent and the guard must not stop the bridge appearing at all.
418 const page = loadPage()
419 expect(typeof page.craft).toBe('object')
420 expect(page.page.__craftBridgeInstalled).toBe(true)
421 })
374422})