ReviewOS

also looking at this

craft-native/craft

test(ios): preserve native shortcuts through bundle initialization

#423
Merged glennmichael123 wants to merge fix/native-screen-shortcuts-module into main
3 files +91 -0
packages/ios/fixtures/native-screen/NativeRenderUnitTests.swiftmodified+36-0
Changes to packages/ios/fixtures/native-screen/NativeRenderUnitTests.swift
@@ -121,6 +121,42 @@ final class NativeShortcutsUnitTests: XCTestCase {
121121 }
122122 }
123123
124 func testScreenAndAppScriptRegisterAndClearAfterPartialOrNullNamespaceReplacement() async throws {
125 for appScript in [false, true] {
126 let screen = CraftNativeScreenController(config: CraftConfig(), isAppScript: appScript)
127 screen.loadViewIfNeeded()
128 defer { screen.viewDidDisappear(false) }
129 for replacement in ["{extra:42}", "null", "undefined"] {
130 screen.evaluateScript("""
131 globalThis.__shortcutAnswer = null;
132 craft.shortcuts = \(replacement);
133 craft.shortcuts.set([{type:'partial',title:'Partial'}]).then(function(result) {
134 __shortcutAnswer = {count:result.count, extra:craft.shortcuts.extra};
135 }).catch(function(error) { __shortcutAnswer = {error:String(error)}; });
136 """)
137 for _ in 0..<100 {
138 if screen.evaluateScript("__shortcutAnswer !== null")?.toBool() == true { break }
139 try await Task.sleep(nanoseconds: 20_000_000)
140 }
141 let result = try XCTUnwrap(screen.evaluateScript("__shortcutAnswer")?.toDictionary())
142 XCTAssertNil(result["error"])
143 XCTAssertEqual(result["count"] as? Int, 1)
144 XCTAssertEqual(result["extra"] as? Int, 42)
145 XCTAssertEqual(UIApplication.shared.shortcutItems?.map(\.type), ["partial"])
146 screen.evaluateScript("""
147 globalThis.__shortcutCleared = null;
148 craft.shortcuts.clear().then(function(result) { __shortcutCleared = result; });
149 """)
150 for _ in 0..<100 {
151 if screen.evaluateScript("__shortcutCleared === true")?.toBool() == true { break }
152 try await Task.sleep(nanoseconds: 20_000_000)
153 }
154 XCTAssertTrue(screen.evaluateScript("__shortcutCleared === true")?.toBool() == true)
155 XCTAssertTrue(UIApplication.shared.shortcutItems?.isEmpty ?? true)
156 }
157 }
158 }
159
124160 func testColdAndWarmShortcutTapsReachTheAppHandlerExactlyOnce() {
125161 let events = CraftEventManager()
126162 events.configure(appScript: true, page: false)
packages/ios/src/index.test.tsmodified+52-0
Changes to packages/ios/src/index.test.ts
@@ -3,6 +3,7 @@ import { tmpdir } from 'node:os'
33import { gunzipSync } from 'node:zlib'
44import { chmodSync, existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'
55import { join, resolve } from 'node:path'
6import { createContext, runInContext } from 'node:vm'
67import { afterAll, describe, expect, it } from 'bun:test'
78import { addSchemeBuildTargets, addSchemeTestTargets, insertProjectTargetsBeforeSchemes } from '../scripts/insert-project-targets'
89import {
@@ -646,6 +647,57 @@ describe('Craft iOS builder', () => {
646647 expect(source).toContain('guard width > 0, abs(width - reportedViewportWidth) >= 0.5 else { return }')
647648 })
648649
650 it('preserves native shortcut requests through the full prelude and bundle namespace replacement', async () => {
651 const source = readFileSync(join(import.meta.dir, '..', 'templates', 'CraftNativeScreen.swift'), 'utf8')
652 // Execute the actual two template preludes, including any later keep()
653 // registrations. A source-string assertion cannot catch accessor replacement.
654 function prelude(anchor: string): string {
655 const marker = source.indexOf(anchor)
656 expect(marker).toBeGreaterThan(-1)
657 const opening = 'jsContext.evaluateScript("""'
658 const start = source.lastIndexOf(opening, marker) + opening.length
659 const end = source.indexOf('""")', marker)
660 expect(end).toBeGreaterThan(start)
661 return source.slice(start, end)
662 }
663 const requests: Array<{ module: string, method: string, args: unknown[] }> = []
664 const sandbox: {
665 __stxNativeBridge: { capabilities: string[], postMessage: (raw: string) => void }
666 __stxNativeCallback?: (message: unknown) => void
667 } = {
668 __stxNativeBridge: {
669 capabilities: ['shortcuts'],
670 postMessage(raw) {
671 const message = JSON.parse(raw)
672 requests.push({ module: message.payload.module, method: message.payload.method, args: message.payload.args })
673 sandbox.__stxNativeCallback?.({ type: 'API_RESPONSE', payload: {
674 requestId: message.id, data: message.payload.method === 'set' ? { count: 1 } : true,
675 } })
676 },
677 },
678 }
679 const context = createContext(sandbox)
680 runInContext(prelude('var bridge = globalThis.__stxNativeBridge;'), context)
681 const nativeSetter = runInContext('Object.getOwnPropertyDescriptor(craft, "shortcuts").set', context)
682 runInContext(prelude('function keep(name, extras)'), context)
683 // Exercise bootstrap *before* loadBundle's reinstall can hide the regression,
684 // and normal calls after that reinstall too.
685 for (const reinstall of [false, true]) {
686 if (reinstall) runInContext('__craftInstallNativeDialog()', context)
687 for (const replacement of ['{extra:42}', '{extra:42,set:function(){return false},clear:function(){return false}}', 'null', 'undefined']) {
688 runInContext(`craft.shortcuts = ${replacement}`, context)
689 expect(await runInContext('craft.shortcuts.set([{type:"saved",title:"Saved"}])', context)).toEqual({ count: 1 })
690 expect(await runInContext('craft.shortcuts.clear()', context)).toBe(true)
691 expect(runInContext('craft.shortcuts.extra', context)).toBe(42)
692 }
693 if (!reinstall) expect(runInContext('Object.getOwnPropertyDescriptor(craft, "shortcuts").set', context)).toBe(nativeSetter)
694 }
695 expect(requests).toEqual(Array.from({ length: 8 }, () => [
696 { module: 'Shortcuts', method: 'set', args: [[{ type: 'saved', title: 'Saved' }]] },
697 { module: 'Shortcuts', method: 'clear', args: [] },
698 ]).flat())
699 })
700
649701 it('advertises mounted-view focus and measurement in native screens', () => {
650702 const source = readFileSync(join(import.meta.dir, '..', 'templates', 'CraftNativeScreen.swift'), 'utf8')
651703 const actions = readFileSync(join(import.meta.dir, '..', 'templates', 'CraftNativeActions.swift'), 'utf8')
packages/ios/templates/CraftNativeScreen.swiftmodified+3-0
Changes to packages/ios/templates/CraftNativeScreen.swift
@@ -7017,6 +7017,9 @@ final class CraftNativeScreenController: UIViewController, UIScrollViewDelegate,
70177017 keep('navigation', {
70187018 setOptions: function(options) { craftNativeSetOptions(JSON.stringify(options || {})); }
70197019 });
7020 // shortcuts already has a capability-owned accessor above.
7021 // Do not wrap it in keep(): its adopt setter would let a
7022 // bundle replace native set/clear during bootstrap.
70207023 })();
70217024 """)
70227025 }