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/index.test.tsmodified+26-1
Changes to packages/android/src/index.test.ts
@@ -348,7 +348,7 @@ describe('Craft Android builder', () => {
348348 expect(activity).toContain('craftBridge.restoreDeepLinks(savedInstanceState)')
349349
350350 // Flushed in the injection's completion, after the page's replay exists.
351 const injected = bridge.slice(bridge.indexOf('webView.evaluateJavascript(script) {'))
351 const injected = bridge.slice(bridge.indexOf('webView.evaluateJavascript(onlyOnce(script)) {'))
352352 const flush = injected.slice(0, injected.indexOf('fun markBridgeLoading()'))
353353 expect(flush.indexOf('bridgeReady = true')).toBeLessThan(flush.indexOf('links.forEach { (url, initial) -> dispatchDeepLink(url, initial) }'))
354354
@@ -413,6 +413,31 @@ describe('Craft Android builder', () => {
413413 expect(bridge).toContain('sendEvent("craftSpeechEnd", emptyMap())')
414414 })
415415
416 it('marks ready only the page its injection ran in, and injects a document once', async () => {
417 // #228, two halves. The evaluateJavascript completion is posted, so it
418 // can land after the next navigation has started and mark a page that
419 // was never injected as ready — flushing the deep-link and shortcut
420 // queues into a document with no bridge. And onPageFinished arrives
421 // without onPageStarted for a same-document navigation, so the script
422 // could run twice in one document.
423 const output = mkdtempSync(join(tmpdir(), 'craft-android-injection-'))
424 await init({ name: 'WildLoop', packageName: 'org.wildloop.app', output })
425
426 const bridge = readFileSync(join(output, 'app/src/main/java/org/wildloop/app/CraftBridge.kt'), 'utf8')
427 expect(bridge).toContain('private var navigationGeneration = 0')
428 expect(bridge).toContain('navigationGeneration += 1')
429 expect(bridge).toContain('val generation = navigationGeneration')
430 expect(bridge).toContain('if (generation != navigationGeneration) return@evaluateJavascript')
431 // The generation is taken before the script is handed over, and compared
432 // after; either order alone leaves the race open.
433 expect(bridge.indexOf('val generation = navigationGeneration'))
434 .toBeLessThan(bridge.indexOf('if (generation != navigationGeneration)'))
435
436 expect(bridge).toContain('private fun onlyOnce(script: String): String')
437 expect(bridge).toContain('evaluateJavascript(onlyOnce(script))')
438 expect(bridge).toContain('if (!window.__craftBridgeInstalled)')
439 })
440
416441 it('routes external Activity results back to every pending media promise', async () => {
417442 const output = mkdtempSync(join(tmpdir(), 'craft-android-activity-results-'))
418443 await init({ name: 'WildLoop', packageName: 'org.wildloop.app', output })