Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(iOS): support react native new architecture. #4661

Merged
merged 51 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
b3529f4
temp: migrate test-app `ios` files to `ios-old`.
asafkorem Dec 16, 2024
1cc1f6b
test(app): create new iOS project from template.
asafkorem Dec 16, 2024
09eebd8
test(ios): remove example native tests.
asafkorem Dec 16, 2024
687ff84
chore(ios): update git to ignore generated files.
asafkorem Dec 16, 2024
b6df6c9
test(ios): update app's bundle-id and build commands.
asafkorem Dec 16, 2024
7cad1c8
test(ios): link Detox framework with project.
asafkorem Dec 16, 2024
35cdb2d
test(ios): update Podfile with dependencies.
asafkorem Dec 16, 2024
b8a4b2f
test(ios): update `AppDelegate.mm` with deps.
asafkorem Dec 16, 2024
336d83d
test(ios): add `NativeModule`.
asafkorem Dec 16, 2024
c8589fa
test(ios): refactor legacy `NativeModule`.
asafkorem Dec 16, 2024
294a164
test(ios): add background modes capabilities.
asafkorem Dec 16, 2024
47625f0
test(ios): enable upside-down rotation.
asafkorem Dec 16, 2024
d1800f1
test(ios): update `Info.plist` with missing keys.
asafkorem Dec 16, 2024
e54f558
test(ios): copy command line arguments from legacy ios project.
asafkorem Dec 16, 2024
23edfc9
feat(iOS): support new `RCTScrollViewComponentView`.
asafkorem Dec 16, 2024
488c203
feat(ios): support new-arch's `RCTParagraphComponentView`.
asafkorem Dec 16, 2024
67e1f60
feat(ios): support React Native new arch app-reloading.
asafkorem Dec 16, 2024
ba95461
test(ios): add example-ci scheme (no Detox linkage).
asafkorem Dec 16, 2024
d7f7f97
test(ios): refactor Podfile.
asafkorem Dec 16, 2024
23c726a
chore(deps): update DetoxSync with new-arch sync support.
asafkorem Dec 17, 2024
5492e6a
chore(ios): add manual test env vars for debugging.
asafkorem Dec 17, 2024
bdcddea
test(ios): fix test-app Podfile.
asafkorem Dec 17, 2024
6c280b8
test(ios): fix test-app Podfile.
asafkorem Dec 17, 2024
bcea8cd
test(ios): update binary path to the non-detox-linked app (example-ci).
asafkorem Dec 17, 2024
abca5b9
fix(ios): temporary remove text fix for predicate.
asafkorem Dec 17, 2024
2fd21b0
fix(ios): support new `RCTParagraphComponentView` component.
asafkorem Dec 18, 2024
824c053
test: enable new arch from the Podfile.
asafkorem Dec 22, 2024
e5f0127
test(RN .76): update view-hierarchy snapshot tests.
asafkorem Dec 22, 2024
fc625fd
fix(iOS): fix implicit conversion of Integer in Podfile.
asafkorem Dec 23, 2024
263a85f
fix(ios): remove `RCT_NEW_ARCH_ENABLED` assignment from Podfile.
asafkorem Dec 23, 2024
b5bd5b4
fix: make keyboard tests pass on legacy arch.
asafkorem Dec 25, 2024
28358fb
refactor(iOS): `AppDelegate` extensions.
asafkorem Dec 26, 2024
4dbe3c4
fix: support background searchable items.
asafkorem Dec 29, 2024
35ba68a
chore: update detox-copilot cache.
asafkorem Dec 29, 2024
594c66c
test: update image snapshot tests.
asafkorem Dec 29, 2024
9c7b542
test(ios): support shake events from test-app.
asafkorem Dec 29, 2024
d4fe2ad
test(app): support stack view for toasts.
asafkorem Dec 31, 2024
c27a003
test: fix test text matcher.
asafkorem Dec 31, 2024
88f22ff
test(app): remove old iOS code.
asafkorem Jan 1, 2025
d8ec159
test(podfile): remove old RN versions check.
asafkorem Jan 1, 2025
865645d
chore: clean old snapshot test artifacts.
asafkorem Jan 1, 2025
c469c8b
revert(test): use old string for native root native module.
asafkorem Jan 1, 2025
58e7757
test: remove old RN workarounds from podfile.
asafkorem Jan 1, 2025
207673c
test: update snapshots.
asafkorem Jan 1, 2025
b1889a6
ci: add new arch pipeline step for RN 76 + new arch + iOS.
asafkorem Jan 2, 2025
24d5198
test: update copilot cache.
asafkorem Jan 2, 2025
abb9ae4
test: print if react native new arch is enabled.
asafkorem Jan 2, 2025
6902b23
test: skip picker tests on new arch.
asafkorem Jan 2, 2025
e017d39
test(custom-describe): change to skip methods.
asafkorem Jan 6, 2025
30b0dac
ci: remove common & redundant steps from pipelines.
asafkorem Jan 6, 2025
0b9b92f
test: change skip method to skip only on iOS.
asafkorem Jan 6, 2025
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
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
- label: ":ios::react: RN .76 + iOS: Demo app"
- label: ":new::ios::react: RN .76 + iOS: Demo app"
command:
- "nvm install"
- "./scripts/demo-projects.ios.sh"
env:
REACT_NATIVE_VERSION: 0.76.3
RCT_NEW_ARCH_ENABLED: 1
artifact_paths:
- "/Users/builder/uibuilder/work/coverage/**/*.lcov"
- "/Users/builder/uibuilder/work/artifacts*.tar.gz"
1 change: 1 addition & 0 deletions .buildkite/jobs/pipeline.ios_rn_76.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
- "./scripts/ci.ios.sh"
env:
REACT_NATIVE_VERSION: 0.76.3
RCT_NEW_ARCH_ENABLED: 0
artifact_paths:
- "/Users/builder/uibuilder/work/coverage/**/*.lcov"
- "/Users/builder/uibuilder/work/**/allure-report-*.html"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
- label: ":ios::detox: RN .75 + iOS: Tests app"
- label: ":new::ios::detox: RN .76 + New Arch + iOS: Tests app"
command:
- "nvm install"
- "./scripts/ci.ios.sh"
env:
REACT_NATIVE_VERSION: 0.75.4
REACT_NATIVE_VERSION: 0.76.3
RCT_NEW_ARCH_ENABLED: 1
artifact_paths:
- "/Users/builder/uibuilder/work/coverage/**/*.lcov"
- "/Users/builder/uibuilder/work/**/allure-report-*.html"
Expand Down
4 changes: 2 additions & 2 deletions .buildkite/pipeline_common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

echo "steps:"

cat .buildkite/jobs/pipeline.ios_rn_76_new_arch.yml
cat .buildkite/jobs/pipeline.ios_rn_76.yml
cat .buildkite/jobs/pipeline.ios_rn_75.yml
cat .buildkite/jobs/pipeline.ios_rn_73.yml
cat .buildkite/jobs/pipeline.ios_demo_app_rn_76.yml
cat .buildkite/jobs/pipeline.ios_demo_app_rn_76_new_arch.yml
cat .buildkite/jobs/pipeline.android_rn_76.yml
cat .buildkite/jobs/pipeline.android_rn_75.yml
cat .buildkite/jobs/pipeline.android_rn_73.yml
Expand Down
8 changes: 4 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ fabric.properties
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore

## Build generated
ios/build/
ios/DerivedData/
*/*/ios/build/
*/*/ios/DerivedData/

## Various settings
*.pbxuser
Expand All @@ -93,12 +93,12 @@ ios/DerivedData/
!default.mode2v3
*.perspectivev3
!default.perspectivev3
ios/xcuserdata/
*/*/ios/xcuserdata/

## Other
*.moved-aside
*.xcuserstate
ios/.xcode.env.local
*/*/ios/.xcode.env.local

## Obj-C/Swift specific
*.hmap
Expand Down
35 changes: 22 additions & 13 deletions detox/ios/Detox/Invocation/Element.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,19 +73,28 @@ class Element : NSObject {
return element
}

private func extractScrollView() -> UIScrollView {
if let view = self.view as? UIScrollView {
return view
}
else if let view = self.view as? WKWebView {
return view.scrollView
} else if ReactNativeSupport.isReactNativeApp && NSStringFromClass(type(of: view)) == "RCTScrollView" {
return (view.value(forKey: "scrollView") as! UIScrollView)
}

dtx_fatalError("View “\(self.view.dtx_shortDescription)” is not an instance of “UIScrollView”", viewDescription: debugAttributes)
}

private func extractScrollView() -> UIScrollView {
if let view = self.view as? UIScrollView {
return view
}

if let webView = self.view as? WKWebView {
return webView.scrollView
}

if ReactNativeSupport.isReactNativeApp {
let className = NSStringFromClass(type(of: view))
switch className {
case "RCTScrollView", "RCTScrollViewComponentView":
return (view.value(forKey: "scrollView") as! UIScrollView)
default:
break
}
}

dtx_fatalError("View “\(self.view.dtx_shortDescription)” is not an instance of “UIScrollView”", viewDescription: debugAttributes)
}

override var description: String {
return String(format: "MATCHER(%@)%@", predicate.description, index != nil ? " AT INDEX(\(index!))" : "")
}
Expand Down
56 changes: 40 additions & 16 deletions detox/ios/Detox/Invocation/Predicate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,20 +65,32 @@ class Predicate : CustomStringConvertible, CustomDebugStringConvertible {
if ReactNativeSupport.isReactNativeApp == false {
return ValuePredicate(kind: kind, modifiers: modifiers, value: label, requiresAccessibilityElement: true, isRegex: isRegex)
} else {
//Will crash if RN app and neither class exists
let RCTTextViewClass : AnyClass = NSClassFromString("RCTText") ?? NSClassFromString("RCTTextView")!

let descendantPredicate = DescendantPredicate(predicate: AndCompoundPredicate(predicates: [
try KindOfPredicate(kind: Kind.type, modifiers: [], className: NSStringFromClass(RCTTextViewClass)),
ValuePredicate(kind: kind, modifiers: modifiers, value: label, requiresAccessibilityElement: true, isRegex: isRegex)
], modifiers: []), modifiers: [Modifier.not])
descendantPredicate.hidden = true

return AndCompoundPredicate(predicates: [
ValuePredicate(kind: kind, modifiers: modifiers, value: label, requiresAccessibilityElement: true, isRegex: isRegex),
descendantPredicate
], modifiers: [])
let possibleRNClasses: [AnyClass] = [
NSClassFromString("RCTParagraphComponentView"),
NSClassFromString("RCTText"),
NSClassFromString("RCTTextView")
].compactMap { $0 }

guard !possibleRNClasses.isEmpty else {
fatalError("No React Native text component classes found")
}

let typePredicates = possibleRNClasses.map { rnClass in
try! KindOfPredicate(kind: Kind.type, modifiers: [], className: NSStringFromClass(rnClass))
}

let descendantPredicate = DescendantPredicate(predicate: AndCompoundPredicate(predicates: [
OrCompoundPredicate(predicates: typePredicates, modifiers: []),
ValuePredicate(kind: kind, modifiers: modifiers, value: label, requiresAccessibilityElement: true, isRegex: isRegex)
], modifiers: []), modifiers: [Modifier.not])
descendantPredicate.hidden = true

return AndCompoundPredicate(predicates: [
ValuePredicate(kind: kind, modifiers: modifiers, value: label, requiresAccessibilityElement: true, isRegex: isRegex),
descendantPredicate
], modifiers: [])
}

case Kind.text:
let text = dictionaryRepresentation[Keys.value] as! String
var orPredicates = [
Expand All @@ -88,9 +100,21 @@ class Predicate : CustomStringConvertible, CustomDebugStringConvertible {
]

if ReactNativeSupport.isReactNativeApp == true {
//Will crash if RN app and neither class exists
let RCTTextViewClass : AnyClass = NSClassFromString("RCTText") ?? NSClassFromString("RCTTextView")!
orPredicates.append(try KindOfPredicate(kind: Kind.type, modifiers: [], className: NSStringFromClass(RCTTextViewClass)))
let possibleRNClasses: [AnyClass] = [
NSClassFromString("RCTParagraphComponentView"),
NSClassFromString("RCTText"),
NSClassFromString("RCTTextView")
].compactMap { $0 }

guard !possibleRNClasses.isEmpty else {
fatalError("No React Native text component classes found")
}

possibleRNClasses.forEach { rnClass in
let predicate = try! KindOfPredicate(kind: Kind.type, modifiers: [], className: NSStringFromClass(rnClass))

orPredicates.append(predicate)
}
}

let orCompoundPredicate = OrCompoundPredicate(predicates: orPredicates, modifiers: [])
Expand Down
60 changes: 38 additions & 22 deletions detox/ios/Detox/Utilities/NSObject+DontCrash.m
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,48 @@ @implementation NSObject (DontCrash)

- (id)_dtx_text
{
if([self respondsToSelector:@selector(text)])
{
return [(UITextView*)self text];
}

static Class RCTTextView;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
RCTTextView = NSClassFromString(@"RCTTextView");
});
if(RCTTextView != nil && [self isKindOfClass:RCTTextView])
{
return [(NSTextStorage*)[self valueForKey:@"textStorage"] string];
}

return nil;
if([self respondsToSelector:@selector(text)])
{
return [(UITextView*)self text];
}

static Class RCTParagraphComponentViewClass;
static Class RCTTextClass;
static Class RCTTextViewClass;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
RCTParagraphComponentViewClass = NSClassFromString(@"RCTParagraphComponentView");
RCTTextClass = NSClassFromString(@"RCTText");
RCTTextViewClass = NSClassFromString(@"RCTTextView");
});

if(RCTParagraphComponentViewClass != nil && [self isKindOfClass:RCTParagraphComponentViewClass])
{
NSAttributedString *attributedText = [self valueForKey:@"attributedText"];
return [attributedText string];
}

if(RCTTextClass != nil && [self isKindOfClass:RCTTextClass])
{
return [(NSTextStorage*)[self valueForKey:@"textStorage"] string];
}

if(RCTTextViewClass != nil && [self isKindOfClass:RCTTextViewClass])
{
return [(NSTextStorage*)[self valueForKey:@"textStorage"] string];
}

return nil;
}

- (id)_dtx_placeholder
{
if([self respondsToSelector:@selector(placeholder)])
{
return [(UITextField*)self placeholder];
}
return nil;
if([self respondsToSelector:@selector(placeholder)])
{
return [(UITextField*)self placeholder];
}

return nil;
}

@end
50 changes: 30 additions & 20 deletions detox/ios/Detox/Utilities/ReactNativeSupport/ReactNativeSupport.m
Original file line number Diff line number Diff line change
Expand Up @@ -28,26 +28,36 @@ + (BOOL)isReactNativeApp

+ (void)reloadApp
{
if([DTXReactNativeSupport hasReactNative] == NO)
{
return;
}

id<RN_RCTBridge> bridge = [NSClassFromString(@"RCTBridge") valueForKey:@"currentBridge"];

SEL reqRelSel = NSSelectorFromString(@"requestReload");
if([bridge respondsToSelector:reqRelSel])
{
//Call RN public API to request reload.
[bridge requestReload];
}
else
{
//Legacy call to reload RN.
[[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification
object:nil
userInfo:nil];
}
// Early return if React Native is not present
if (![DTXReactNativeSupport hasReactNative]) {
return;
}

// Try legacy reload approach (without new arch)
id<RN_RCTBridge> bridge = [NSClassFromString(@"RCTBridge") valueForKey:@"currentBridge"];
if ([bridge respondsToSelector:@selector(requestReload)]) {
[bridge requestReload];
return;
}

// Try New Architecture reload approach
NSObject<UIApplicationDelegate> *appDelegate = UIApplication.sharedApplication.delegate;
NSObject *rootViewFactory = [appDelegate valueForKey:@"rootViewFactory"];
NSObject *reactHost = [rootViewFactory valueForKey:@"reactHost"];

SEL reloadCommand = NSSelectorFromString(@"didReceiveReloadCommand");
if (reactHost && [reactHost respondsToSelector:reloadCommand]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[reactHost performSelector:reloadCommand];
#pragma clang diagnostic pop
return;
}

// Fallback to legacy^2 reload approach
[[NSNotificationCenter defaultCenter] postNotificationName:RCTReloadNotification
object:nil
userInfo:nil];
}

+ (void)waitForReactNativeLoadWithCompletionHandler:(void (^)(void))handler
Expand Down
2 changes: 1 addition & 1 deletion detox/ios/DetoxSync
Loading
Loading