Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tidy-unicode-tabs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'react-native-bottom-tabs': patch
---

Fix an iOS Fabric crash when selecting or long-pressing a tab whose key contains non-ASCII characters.
18 changes: 18 additions & 0 deletions apps/example/e2e/unicode-tab-key.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
appId: bottomtabs.example
name: Interact with a Unicode tab key
---
- launchApp
- scrollUntilVisible:
element:
text: ^Native Bottom Tabs$
direction: DOWN
timeout: 15000
- tapOn:
text: ^Native Bottom Tabs$
- assertVisible: Press me
- tapOn:
text: ^메시지$
- assertVisible: 메시지
- longPressOn:
text: ^메시지$
- assertVisible: 메시지
2 changes: 1 addition & 1 deletion apps/example/src/Examples/NativeBottomTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ function NativeBottomTabs() {
}}
/>
<Tab.Screen
name="Albums"
name="메시지"
component={Albums}
options={{
tabBarButtonTestID: 'albumsTestID',
Expand Down
38 changes: 38 additions & 0 deletions artifacts/issue-560/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Issue #560 device evidence

Captured on 2026-08-25 with `agent-device 0.20.10`, React Native 0.81.4,
Fabric enabled, Xcode 27.0 (27A5228h), and the repository example app
(`bottomtabs.example`).

## Before the fix

- Runtime: iOS 27.0, iPhone 17 Pro simulator.
- `before/unicode-tab-selection-crash.mp4` records tapping the `메시지` tab.
- `before/ReactTestApp-selection-crash.ips` records `SIGABRT` in
`std::string(char const *)`, called by
`-[RCTTabViewComponentView onPageSelectedWithKey:reactTag:]` at line 241.
- `before/unicode-tab-long-press-crash.mp4` records a long-press gesture on the
same tab. The selection delegate aborts before the custom long-press callback
can finish.
- `before/ReactTestApp-long-press-crash.ips` records the corresponding abort.

## After the fix

- Runtime: iOS 26.5, iPhone 17 Pro simulator (matching the issue's reported
major runtime), plus an iOS 27.0 selection pass.
- `after/ios26-unicode-tab-selection-and-long-press.mp4` records successful
selection and repeated long-press gestures without termination.
- `after/ios26-unicode-tab-selected.png` shows the selected Unicode tab and
rendered Albums screen.
- `after/unicode-tab-selected.png` shows the corresponding iOS 27.0 selection
pass.
- No new `ReactTestApp` crash report was generated during either post-fix run.
- `e2e-result/junit.xml` records the focused agent-device/Maestro regression
flow passing (one test, zero failures). The corresponding interaction video,
replay, and gesture telemetry are in the nested `attempt-1` directory.

The XCUITest synthesized hold used by `agent-device longpress` selected the tab
but did not trigger this example's custom `UILongPressGestureRecognizer`; the
same limitation reproduced on the existing ASCII `Article` tab. The focused
Maestro flow therefore asserts the original regression boundary: Unicode tap
and long-press gestures leave the app alive and the Unicode tab visible.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"version": 1,
"generatedAt": "2026-08-25T00:19:54.264Z",
"events": [
{
"kind": "tap",
"tMs": 2458.2578125,
"x": 159,
"y": 822,
"referenceWidth": 402,
"referenceHeight": 874
},
{
"kind": "longpress",
"tMs": 14660,
"x": 159,
"y": 822,
"referenceWidth": 402,
"referenceHeight": 874,
"durationMs": 1000
},
{
"kind": "longpress",
"tMs": 21047,
"x": 159,
"y": 822,
"referenceWidth": 402,
"referenceHeight": 874,
"durationMs": 3000
},
{
"kind": "longpress",
"tMs": 43627,
"x": 75,
"y": 822,
"referenceWidth": 402,
"referenceHeight": 874,
"durationMs": 3000
}
]
}
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
988 changes: 988 additions & 0 deletions artifacts/issue-560/before/ReactTestApp-long-press-crash.ips

Large diffs are not rendered by default.

997 changes: 997 additions & 0 deletions artifacts/issue-560/before/ReactTestApp-selection-crash.ips

Large diffs are not rendered by default.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"version": 1,
"generatedAt": "2026-08-25T00:11:41.551Z",
"events": [
{
"kind": "longpress",
"tMs": 2168,
"x": 159,
"y": 822,
"referenceWidth": 402,
"referenceHeight": 874,
"durationMs": 8373
}
]
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"version": 1,
"generatedAt": "2026-08-25T00:10:30.072Z",
"events": [
{
"kind": "tap",
"tMs": 2510.88330078125,
"x": 159,
"y": 822,
"referenceWidth": 402,
"referenceHeight": 874
}
]
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"version": 1,
"generatedAt": "2026-08-25T00:25:26.472Z",
"events": [
{
"kind": "swipe",
"tMs": 6958.691650390625,
"x": 201,
"y": 495,
"x2": 201,
"y2": 191,
"referenceWidth": 402,
"referenceHeight": 874,
"durationMs": 912
},
{
"kind": "swipe",
"tMs": 10499.9970703125,
"x": 201,
"y": 495,
"x2": 201,
"y2": 191,
"referenceWidth": 402,
"referenceHeight": 874,
"durationMs": 917
},
{
"kind": "tap",
"tMs": 14251.433349609375,
"x": 201,
"y": 642,
"referenceWidth": 402,
"referenceHeight": 874
},
{
"kind": "tap",
"tMs": 16403.9990234375,
"x": 159,
"y": 822,
"referenceWidth": 402,
"referenceHeight": 874
},
{
"kind": "longpress",
"tMs": 18017.404541015625,
"x": 159,
"y": 822,
"referenceWidth": 402,
"referenceHeight": 874,
"durationMs": 3000
}
]
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{"type":"replay_test_attempt_start","ts":"2026-08-25T00:24:57.582Z","replayPath":"/Users/thiagobrez/.codex/worktrees/98e3/react-native-bottom-tabs/apps/example/e2e/unicode-tab-key.yaml","session":"issue560-e2e:test:b9e1da5f1f6b7d0c:1-unicode-tab-key:attempt-1","requestId":"b9e1da5f1f6b7d0c:test:1:unicode-tab-key.yaml:attempt:1"}
{"type":"replay_action_start","ts":"2026-08-25T00:24:57.586Z","replayPath":"/Users/thiagobrez/.codex/worktrees/98e3/react-native-bottom-tabs/apps/example/e2e/unicode-tab-key.yaml","line":4,"step":1,"command":"launchApp","positionals":[]}
{"type":"video_recording_start","session":"issue560-e2e:test:b9e1da5f1f6b7d0c:1-unicode-tab-key:attempt-1","videoPath":"/Users/thiagobrez/.codex/worktrees/98e3/react-native-bottom-tabs/artifacts/issue-560/e2e-result/b9e1da5f1f6b7d0c/apps__example__e2e__unicode-tab-key.yaml/attempt-1/recording.mp4","ts":"2026-08-25T00:25:00.165Z"}
{"type":"video_preroll_done","session":"issue560-e2e:test:b9e1da5f1f6b7d0c:1-unicode-tab-key:attempt-1","durationMs":1003,"requestedDurationMs":1000,"ts":"2026-08-25T00:25:02.094Z"}
{"type":"replay_action_stop","ts":"2026-08-25T00:25:03.741Z","replayPath":"/Users/thiagobrez/.codex/worktrees/98e3/react-native-bottom-tabs/apps/example/e2e/unicode-tab-key.yaml","line":4,"step":1,"command":"launchApp","ok":true,"durationMs":6155,"resultTiming":{"hierarchyCaptures":0,"screenshotCaptures":0,"tapRetries":0}}
{"type":"replay_action_start","ts":"2026-08-25T00:25:03.741Z","replayPath":"/Users/thiagobrez/.codex/worktrees/98e3/react-native-bottom-tabs/apps/example/e2e/unicode-tab-key.yaml","line":5,"step":2,"command":"scrollUntilVisible","positionals":["^Native Bottom Tabs$"]}
{"type":"replay_action_stop","ts":"2026-08-25T00:25:14.780Z","replayPath":"/Users/thiagobrez/.codex/worktrees/98e3/react-native-bottom-tabs/apps/example/e2e/unicode-tab-key.yaml","line":5,"step":2,"command":"scrollUntilVisible","ok":true,"durationMs":11039,"resultTiming":{"hierarchyCaptures":11,"screenshotCaptures":0,"tapRetries":0}}
{"type":"replay_action_start","ts":"2026-08-25T00:25:14.781Z","replayPath":"/Users/thiagobrez/.codex/worktrees/98e3/react-native-bottom-tabs/apps/example/e2e/unicode-tab-key.yaml","line":10,"step":3,"command":"tapOn","positionals":["^Native Bottom Tabs$"]}
{"type":"replay_action_stop","ts":"2026-08-25T00:25:15.610Z","replayPath":"/Users/thiagobrez/.codex/worktrees/98e3/react-native-bottom-tabs/apps/example/e2e/unicode-tab-key.yaml","line":10,"step":3,"command":"tapOn","ok":true,"durationMs":829,"resultTiming":{"hierarchyCaptures":1,"screenshotCaptures":0,"tapRetries":0}}
{"type":"replay_action_start","ts":"2026-08-25T00:25:15.610Z","replayPath":"/Users/thiagobrez/.codex/worktrees/98e3/react-native-bottom-tabs/apps/example/e2e/unicode-tab-key.yaml","line":12,"step":4,"command":"assertVisible","positionals":["Press me"]}
{"type":"replay_action_stop","ts":"2026-08-25T00:25:16.115Z","replayPath":"/Users/thiagobrez/.codex/worktrees/98e3/react-native-bottom-tabs/apps/example/e2e/unicode-tab-key.yaml","line":12,"step":4,"command":"assertVisible","ok":true,"durationMs":505,"resultTiming":{"hierarchyCaptures":1,"screenshotCaptures":0,"tapRetries":0}}
{"type":"replay_action_start","ts":"2026-08-25T00:25:16.116Z","replayPath":"/Users/thiagobrez/.codex/worktrees/98e3/react-native-bottom-tabs/apps/example/e2e/unicode-tab-key.yaml","line":13,"step":5,"command":"tapOn","positionals":["^메시지$"]}
{"type":"replay_action_stop","ts":"2026-08-25T00:25:17.763Z","replayPath":"/Users/thiagobrez/.codex/worktrees/98e3/react-native-bottom-tabs/apps/example/e2e/unicode-tab-key.yaml","line":13,"step":5,"command":"tapOn","ok":true,"durationMs":1647,"resultTiming":{"hierarchyCaptures":2,"screenshotCaptures":0,"tapRetries":0}}
{"type":"replay_action_start","ts":"2026-08-25T00:25:17.764Z","replayPath":"/Users/thiagobrez/.codex/worktrees/98e3/react-native-bottom-tabs/apps/example/e2e/unicode-tab-key.yaml","line":15,"step":6,"command":"assertVisible","positionals":["메시지"]}
{"type":"replay_action_stop","ts":"2026-08-25T00:25:18.146Z","replayPath":"/Users/thiagobrez/.codex/worktrees/98e3/react-native-bottom-tabs/apps/example/e2e/unicode-tab-key.yaml","line":15,"step":6,"command":"assertVisible","ok":true,"durationMs":382,"resultTiming":{"hierarchyCaptures":1,"screenshotCaptures":0,"tapRetries":0}}
{"type":"replay_action_start","ts":"2026-08-25T00:25:18.146Z","replayPath":"/Users/thiagobrez/.codex/worktrees/98e3/react-native-bottom-tabs/apps/example/e2e/unicode-tab-key.yaml","line":16,"step":7,"command":"longPressOn","positionals":["^메시지$"]}
{"type":"replay_action_stop","ts":"2026-08-25T00:25:22.505Z","replayPath":"/Users/thiagobrez/.codex/worktrees/98e3/react-native-bottom-tabs/apps/example/e2e/unicode-tab-key.yaml","line":16,"step":7,"command":"longPressOn","ok":true,"durationMs":4359,"resultTiming":{"hierarchyCaptures":1,"screenshotCaptures":0,"tapRetries":0}}
{"type":"replay_action_start","ts":"2026-08-25T00:25:22.506Z","replayPath":"/Users/thiagobrez/.codex/worktrees/98e3/react-native-bottom-tabs/apps/example/e2e/unicode-tab-key.yaml","line":18,"step":8,"command":"assertVisible","positionals":["메시지"]}
{"type":"replay_action_stop","ts":"2026-08-25T00:25:22.903Z","replayPath":"/Users/thiagobrez/.codex/worktrees/98e3/react-native-bottom-tabs/apps/example/e2e/unicode-tab-key.yaml","line":18,"step":8,"command":"assertVisible","ok":true,"durationMs":397,"resultTiming":{"hierarchyCaptures":1,"screenshotCaptures":0,"tapRetries":0}}
{"type":"replay_test_attempt_stop","ts":"2026-08-25T00:25:22.904Z","session":"issue560-e2e:test:b9e1da5f1f6b7d0c:1-unicode-tab-key:attempt-1","ok":true,"timedOut":false,"durationMs":25322}
{"type":"replay_test_finalize_start","ts":"2026-08-25T00:25:22.904Z","session":"issue560-e2e:test:b9e1da5f1f6b7d0c:1-unicode-tab-key:attempt-1"}
{"type":"video_tail_start","session":"issue560-e2e:test:b9e1da5f1f6b7d0c:1-unicode-tab-key:attempt-1","requestedDurationMs":3000,"ts":"2026-08-25T00:25:22.904Z"}
{"type":"video_recording_stop","session":"issue560-e2e:test:b9e1da5f1f6b7d0c:1-unicode-tab-key:attempt-1","ok":true,"durationMs":4557,"tailDurationMs":3002,"ts":"2026-08-25T00:25:30.463Z"}
{"type":"replay_test_finalize_stop","ts":"2026-08-25T00:25:30.463Z","session":"issue560-e2e:test:b9e1da5f1f6b7d0c:1-unicode-tab-key:attempt-1","ok":true,"durationMs":7559}
{"type":"replay_test_cleanup_start","ts":"2026-08-25T00:25:30.463Z","session":"issue560-e2e:test:b9e1da5f1f6b7d0c:1-unicode-tab-key:attempt-1"}
{"type":"replay_test_cleanup_stop","ts":"2026-08-25T00:25:31.476Z","session":"issue560-e2e:test:b9e1da5f1f6b7d0c:1-unicode-tab-key:attempt-1","ok":true,"durationMs":1013}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
appId: bottomtabs.example
name: Interact with a Unicode tab key
---
- launchApp
- scrollUntilVisible:
element:
text: ^Native Bottom Tabs$
direction: DOWN
timeout: 15000
- tapOn:
text: ^Native Bottom Tabs$
- assertVisible: Press me
- tapOn:
text: ^메시지$
- assertVisible: 메시지
- longPressOn:
text: ^메시지$
- assertVisible: 메시지
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
file: /Users/thiagobrez/.codex/worktrees/98e3/react-native-bottom-tabs/apps/example/e2e/unicode-tab-key.yaml
session: issue560-e2e:test:b9e1da5f1f6b7d0c:1-unicode-tab-key:attempt-1
attempt: 1/1
status: passed
replayed: 8
healed: 0
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
appId: bottomtabs.example
name: Interact with a Unicode tab key
---
- launchApp
- scrollUntilVisible:
element:
text: ^Native Bottom Tabs$
direction: DOWN
timeout: 15000
- tapOn:
text: ^Native Bottom Tabs$
- assertVisible: Press me
- tapOn:
text: ^메시지$
- assertVisible: 메시지
- longPressOn:
text: ^메시지$
- assertVisible: 메시지
14 changes: 14 additions & 0 deletions artifacts/issue-560/e2e-result/junit.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite name="agent-device replay suite" tests="1" failures="0" skipped="0" time="33.903">
<testcase classname="/Users/thiagobrez/.codex/worktrees/98e3/react-native-bottom-tabs/apps/example/e2e" name="Interact with a Unicode tab key" file="/Users/thiagobrez/.codex/worktrees/98e3/react-native-bottom-tabs/apps/example/e2e/unicode-tab-key.yaml" time="33.896">
<system-out>status: passed
durationMs: 33896
attempts: 1
session: issue560-e2e:test:b9e1da5f1f6b7d0c:1-unicode-tab-key:attempt-1
replayed: 8
healed: 0
artifactsDir: /Users/thiagobrez/.codex/worktrees/98e3/react-native-bottom-tabs/artifacts/issue-560/e2e-result/b9e1da5f1f6b7d0c/apps__example__e2e__unicode-tab-key.yaml</system-out>
</testcase>
</testsuite>
</testsuites>
3 changes: 3 additions & 0 deletions artifacts/issue-560/logs/after-launch-console.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
2026-08-25 01:14:29.136 ReactTestApp[52683:132402160] You've implemented -[<UIApplicationDelegate> application:didReceiveRemoteNotification:fetchCompletionHandler:], but you still need to add "remote-notification" to the list of your supported UIBackgroundModes in your Info.plist.
objc[52683]: Class UIAccessibilityLoaderWebShared is implemented in both /private/var/run/com.apple.security.cryptexd/mnt/com.apple.iPhoneOS.SimulatorRuntime-v24.1.5390.6.rDKeNs/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 27.0.simruntime/Contents/Resources/RuntimeRoot/System/Library/AccessibilityBundles/WebCore.axbundle/WebCore (0x111940310) and /private/var/run/com.apple.security.cryptexd/mnt/com.apple.iPhoneOS.SimulatorRuntime-v24.1.5390.6.rDKeNs/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 27.0.simruntime/Contents/Resources/RuntimeRoot/System/Library/AccessibilityBundles/WebKit.axbundle/WebKit (0x110e38398). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
2026-08-25 01:14:29.692 ReactTestApp[52683:132402160] Unbalanced calls to begin/end appearance transitions for <UIViewController: 0x114f50f80>.
3 changes: 3 additions & 0 deletions artifacts/issue-560/logs/before-launch-console.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
2026-08-25 01:07:49.595 ReactTestApp[34573:132373937] You've implemented -[<UIApplicationDelegate> application:didReceiveRemoteNotification:fetchCompletionHandler:], but you still need to add "remote-notification" to the list of your supported UIBackgroundModes in your Info.plist.
2026-08-25 01:07:49.933 ReactTestApp[34573:132373937] Unbalanced calls to begin/end appearance transitions for <UIViewController: 0x10bd2b780>.
objc[34573]: Class UIAccessibilityLoaderWebShared is implemented in both /private/var/run/com.apple.security.cryptexd/mnt/com.apple.iPhoneOS.SimulatorRuntime-v24.1.5390.6.rDKeNs/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 27.0.simruntime/Contents/Resources/RuntimeRoot/System/Library/AccessibilityBundles/WebCore.axbundle/WebCore (0x1128c0310) and /private/var/run/com.apple.security.cryptexd/mnt/com.apple.iPhoneOS.SimulatorRuntime-v24.1.5390.6.rDKeNs/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 27.0.simruntime/Contents/Resources/RuntimeRoot/System/Library/AccessibilityBundles/WebKit.axbundle/WebKit (0x112874398). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
2 changes: 2 additions & 0 deletions artifacts/issue-560/logs/ios26-after-launch-console.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
2026-08-25 01:18:12.976 ReactTestApp[62721:132419046] You've implemented -[<UIApplicationDelegate> application:didReceiveRemoteNotification:fetchCompletionHandler:], but you still need to add "remote-notification" to the list of your supported UIBackgroundModes in your Info.plist.
objc[62721]: Class UIAccessibilityLoaderWebShared is implemented in both /Library/Developer/CoreSimulator/Volumes/iOS_23F77/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 26.5.simruntime/Contents/Resources/RuntimeRoot/System/Library/AccessibilityBundles/WebCore.axbundle/WebCore (0x1269c0310) and /Library/Developer/CoreSimulator/Volumes/iOS_23F77/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 26.5.simruntime/Contents/Resources/RuntimeRoot/System/Library/AccessibilityBundles/WebKit.axbundle/WebKit (0x126788398). This may cause spurious casting failures and mysterious crashes. One of the duplicates must be removed or renamed.
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ - (void)onPageSelectedWithKey:(NSString *)key reactTag:(NSNumber *)reactTag {
auto eventEmitter = std::static_pointer_cast<const RNCTabViewEventEmitter>(_eventEmitter);
if (eventEmitter) {
eventEmitter->onPageSelected(RNCTabViewEventEmitter::OnPageSelected{
.key = [key cStringUsingEncoding:kCFStringEncodingUTF8]
.key = std::string([key UTF8String] ?: "")
});
}
}
Expand All @@ -247,7 +247,7 @@ - (void)onLongPressWithKey:(NSString *)key reactTag:(NSNumber *)reactTag {
auto eventEmitter = std::static_pointer_cast<const RNCTabViewEventEmitter>(_eventEmitter);
if (eventEmitter) {
eventEmitter->onTabLongPress(RNCTabViewEventEmitter::OnTabLongPress {
.key = [key cStringUsingEncoding:kCFStringEncodingUTF8]
.key = std::string([key UTF8String] ?: "")
});
}
}
Expand Down
Loading