also looking at this
fix(android): tie a ready mark to its own navigation, and inject a document once
#239
5 files
+159
-3
| @@ -30,6 +30,18 @@ function pageScript(enabled = true): string { | ||
| 30 | 30 | return script |
| 31 | 31 | } |
| 32 | 32 | |
| 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 | */ | |
| 38 | function 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 | ||
| 33 | 45 | type Listener = (event: { type: string, detail: unknown }) => void |
| 34 | 46 | |
| 35 | 47 | /** |
| @@ -84,7 +96,7 @@ function loadPage( | ||
| 84 | 96 | |
| 85 | 97 | const inject = () => { |
| 86 | 98 | // 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))( | |
| 88 | 100 | page, |
| 89 | 101 | document, |
| 90 | 102 | CraftAndroid, |
| @@ -371,4 +383,40 @@ describe('the injected Android page script', () => { | ||
| 371 | 383 | expect(first).toEqual([{ url: 'app://warm', initial: false }]) |
| 372 | 384 | expect(second).toEqual([]) |
| 373 | 385 | }) |
| 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 | }) | |
| 374 | 422 | }) |