[IOS] Add open extension EG test
Create an EGTest to check that the open extension works as
intended.
The real extension requires app_groups and uses Chrome scheme
so create a smaller one that requires no entitlement and
uses a customizable scheme.
This smaller extension uses the same method to open the
application, so it should be enough to test the application
openining.
This CL:
- Extracts the open url logic in a ios/chrome/common function
- Creates a small extension that use this function
- Creates an EG test that triggers this extension
- Refactor existing extension to use the function
- Cleans the share extension to use weakself in blocks as a cleanup.
Bug: 1478760
Change-Id: I8593d4ae87675877e7415ee8f4f9545f77b804d3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4916304
Commit-Queue: Olivier Robin <olivierrobin@chromium.org>
Reviewed-by: Sylvain Defresne <sdefresne@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1210134}
diff --git a/ios/chrome/browser/ui/sharing/activity_services/BUILD.gn b/ios/chrome/browser/ui/sharing/activity_services/BUILD.gn
index fff5bc5..ae19b5d1 100644
--- a/ios/chrome/browser/ui/sharing/activity_services/BUILD.gn
+++ b/ios/chrome/browser/ui/sharing/activity_services/BUILD.gn
@@ -102,6 +102,7 @@
sources = [ "activity_service_controller_egtest.mm" ]
deps = [
+ "//base/test:test_support",
"//components/strings",
"//ios/chrome/app/strings",
"//ios/chrome/browser/ui/popup_menu:constants",
diff --git a/ios/chrome/browser/ui/sharing/activity_services/activity_service_controller_egtest.mm b/ios/chrome/browser/ui/sharing/activity_services/activity_service_controller_egtest.mm
index 553ff03..fc41225 100644
--- a/ios/chrome/browser/ui/sharing/activity_services/activity_service_controller_egtest.mm
+++ b/ios/chrome/browser/ui/sharing/activity_services/activity_service_controller_egtest.mm
@@ -7,11 +7,13 @@
#import <memory>
#import "base/ios/ios_util.h"
+#import "base/test/ios/wait_util.h"
#import "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/ui/popup_menu/overflow_menu/feature_flags.h"
#import "ios/chrome/browser/ui/popup_menu/popup_menu_constants.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey.h"
+#import "ios/chrome/test/earl_grey/chrome_earl_grey_app_interface.h"
#import "ios/chrome/test/earl_grey/chrome_earl_grey_ui.h"
#import "ios/chrome/test/earl_grey/chrome_matchers.h"
#import "ios/chrome/test/earl_grey/web_http_server_chrome_test_case.h"
@@ -41,13 +43,52 @@
[ChromeEarlGreyUI openShareMenu];
// Verify that the share menu is up and contains a Copy action.
-
[ChromeEarlGrey verifyActivitySheetVisible];
// Start the Copy action and verify that the share menu gets dismissed.
[ChromeEarlGrey tapButtonInActivitySheetWithID:@"Copy"];
[ChromeEarlGrey verifyActivitySheetNotVisible];
}
+// Tests that the open extension opens a new tab.
+- (void)testOpenActivityServiceControllerAndOpenExtension {
+ // EG does not support tapping on action extension before iOS17.
+ if (@available(iOS 17.0, *)) {
+ // Set up mock http server.
+ std::map<GURL, std::string> responses;
+ GURL url = web::test::HttpServer::MakeUrl("http://potato");
+ responses[url] = "tomato";
+ web::test::SetUpSimpleHttpServer(responses);
+
+ // Open page and open the share menu.
+ [ChromeEarlGrey loadURL:url];
+ [ChromeEarlGreyUI openShareMenu];
+
+ [ChromeEarlGrey verifyActivitySheetVisible];
+ [ChromeEarlGrey tapButtonInActivitySheetWithID:@"EGOpenExtension"];
+
+ GREYCondition* tabCountCheck = [GREYCondition
+ conditionWithName:@"Tab count"
+ block:^{
+ return [ChromeEarlGreyAppInterface mainTabCount] == 2;
+ }];
+ if (![tabCountCheck
+ waitWithTimeout:base::test::ios::kWaitForUIElementTimeout
+ .InSecondsF()]) {
+ // If the tab is not opened, it is very likely due to a system popup.
+ // Try to find it and open on the "Open" button.
+ XCUIApplication* springboardApplication = [[XCUIApplication alloc]
+ initWithBundleIdentifier:@"com.apple.springboard"];
+ auto button = springboardApplication.buttons[@"Open"];
+ if ([button waitForExistenceWithTimeout:
+ base::test::ios::kWaitForUIElementTimeout.InSecondsF()]) {
+ [button tap];
+ }
+ [ChromeEarlGrey waitForMainTabCount:2];
+ }
+ [ChromeEarlGrey verifyActivitySheetNotVisible];
+ }
+}
+
// Verifies that Tools Menu > Share Chrome brings up the "share sheet".
- (void)testShareChromeApp {
if (@available(iOS 15.0, *)) {
diff --git a/ios/chrome/common/BUILD.gn b/ios/chrome/common/BUILD.gn
index 5b01bd6..e9201d8 100644
--- a/ios/chrome/common/BUILD.gn
+++ b/ios/chrome/common/BUILD.gn
@@ -41,6 +41,18 @@
frameworks = [ "Foundation.framework" ]
}
+source_set("extension_open_url") {
+ sources = [
+ "extension_open_url.h",
+ "extension_open_url.mm",
+ ]
+
+ frameworks = [
+ "Foundation.framework",
+ "UIKit.framework",
+ ]
+}
+
source_set("string_util") {
sources = [
"string_util.h",
diff --git a/ios/chrome/common/extension_open_url.h b/ios/chrome/common/extension_open_url.h
new file mode 100644
index 0000000..0ea1b93
--- /dev/null
+++ b/ios/chrome/common/extension_open_url.h
@@ -0,0 +1,20 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef IOS_CHROME_COMMON_EXTENSION_OPEN_URL_H_
+#define IOS_CHROME_COMMON_EXTENSION_OPEN_URL_H_
+
+#import <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+
+using BlockWithBoolean = void (^)(BOOL success);
+
+// Open `url` function for extensions. If `pre_open_block` is not nil, it will
+// be called just before the actual call to openURL, and hence before the
+// application switch is done.
+BOOL ExtensionOpenURL(NSURL* url,
+ UIResponder* responder,
+ BlockWithBoolean pre_open_block);
+
+#endif // IOS_CHROME_COMMON_EXTENSION_OPEN_URL_H_
diff --git a/ios/chrome/common/extension_open_url.mm b/ios/chrome/common/extension_open_url.mm
new file mode 100644
index 0000000..a96a194
--- /dev/null
+++ b/ios/chrome/common/extension_open_url.mm
@@ -0,0 +1,23 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "ios/chrome/common/extension_open_url.h"
+
+bool ExtensionOpenURL(NSURL* url,
+ UIResponder* responder,
+ BlockWithBoolean pre_open_block) {
+ while ((responder = responder.nextResponder)) {
+ if ([responder respondsToSelector:@selector(openURL:)]) {
+ if (pre_open_block) {
+ pre_open_block(YES);
+ }
+ [responder performSelector:@selector(openURL:) withObject:url];
+ return YES;
+ }
+ }
+ if (pre_open_block) {
+ pre_open_block(NO);
+ }
+ return NO;
+}
diff --git a/ios/chrome/open_extension/BUILD.gn b/ios/chrome/open_extension/BUILD.gn
index 2efc60f6..6978a27 100644
--- a/ios/chrome/open_extension/BUILD.gn
+++ b/ios/chrome/open_extension/BUILD.gn
@@ -33,6 +33,7 @@
deps = [
":system_strings",
"//base",
+ "//ios/chrome/common:extension_open_url",
"//ios/chrome/common/app_group:app_group",
"//ios/chrome/common/app_group:command",
"//ios/chrome/common/crash_report",
diff --git a/ios/chrome/open_extension/open_view_controller.mm b/ios/chrome/open_extension/open_view_controller.mm
index ab05111..dbac04c 100644
--- a/ios/chrome/open_extension/open_view_controller.mm
+++ b/ios/chrome/open_extension/open_view_controller.mm
@@ -13,6 +13,7 @@
#import "base/strings/sys_string_conversions.h"
#import "ios/chrome/common/app_group/app_group_command.h"
#import "ios/chrome/common/app_group/app_group_constants.h"
+#import "ios/chrome/common/extension_open_url.h"
// Type for completion handler to fetch the components of the share items.
// `idResponse` type depends on the element beeing fetched.
@@ -128,28 +129,28 @@
}
}
-- (void)openInChrome {
- UIResponder* responder = self;
-
- while ((responder = responder.nextResponder)) {
- if ([responder respondsToSelector:@selector(openURL:)]) {
+- (void)performOpenURL:(NSURL*)openURL {
+ bool result = ExtensionOpenURL(openURL, self, ^(BOOL success) {
+ if (success) {
LogOutcome(app_group::OpenExtensionOutcome::kSuccess);
- [self openInURL:responder];
- [self.extensionContext completeRequestReturningItems:@[ _openInItem ]
- completionHandler:nil];
- return;
}
+ });
+ if (result) {
+ [self.extensionContext completeRequestReturningItems:@[ _openInItem ]
+ completionHandler:nil];
+ return;
}
// Display the error view when Open in is not found
[self displayErrorViewForOutcome:app_group::OpenExtensionOutcome::
kFailureOpenInNotFound];
}
-- (void)openInURL:(UIResponder*)responder {
+- (void)openInChrome {
+ __weak OpenViewController* weakSelf = self;
AppGroupCommand* command = [[AppGroupCommand alloc]
initWithSourceApp:app_group::kOpenCommandSourceOpenExtension
URLOpenerBlock:^(NSURL* openURL) {
- [responder performSelector:@selector(openURL:) withObject:openURL];
+ [weakSelf performOpenURL:openURL];
}];
[command prepareToOpenURL:_openInURL];
[command executeInApp];
diff --git a/ios/chrome/share_extension/BUILD.gn b/ios/chrome/share_extension/BUILD.gn
index ec9e687..c0f9adb6 100644
--- a/ios/chrome/share_extension/BUILD.gn
+++ b/ios/chrome/share_extension/BUILD.gn
@@ -34,6 +34,7 @@
deps = [
":system_strings",
"//base",
+ "//ios/chrome/common:extension_open_url",
"//ios/chrome/common/app_group",
"//ios/chrome/common/app_group:client",
"//ios/chrome/common/app_group:command",
diff --git a/ios/chrome/share_extension/share_view_controller.mm b/ios/chrome/share_extension/share_view_controller.mm
index e984d75..8f08e63 100644
--- a/ios/chrome/share_extension/share_view_controller.mm
+++ b/ios/chrome/share_extension/share_view_controller.mm
@@ -13,6 +13,7 @@
#import "ios/chrome/common/app_group/app_group_command.h"
#import "ios/chrome/common/app_group/app_group_constants.h"
#import "ios/chrome/common/crash_report/crash_helper.h"
+#import "ios/chrome/common/extension_open_url.h"
#import "ios/chrome/share_extension/share_extension_view.h"
#import "ios/chrome/share_extension/ui_util.h"
@@ -32,21 +33,20 @@
} // namespace
-@interface ShareViewController ()<ShareExtensionViewActionTarget> {
- // This constrains the center of the widget to be vertically in the center
- // of the the screen. It has to be modified for the appearance and dismissal
- // animation.
- NSLayoutConstraint* _widgetVerticalPlacementConstraint;
-
- NSURL* _shareURL;
- NSString* _shareTitle;
- UIImage* _image;
- NSExtensionItem* _shareItem;
-}
+@interface ShareViewController () <ShareExtensionViewActionTarget>
@property(nonatomic, weak) UIView* maskView;
@property(nonatomic, weak) ShareExtensionView* shareView;
@property(nonatomic, assign) app_group::ShareExtensionItemType itemType;
+@property(nonatomic, strong) NSExtensionItem* shareItem;
+@property(nonatomic, strong) NSURL* shareURL;
+@property(nonatomic, copy) NSString* shareTitle;
+@property(nonatomic, strong) UIImage* image;
+// This constrains the center of the widget to be vertically in the center
+// of the the screen. It has to be modified for the appearance and dismissal
+// animation.
+@property(nonatomic, strong)
+ NSLayoutConstraint* widgetVerticalPlacementConstraint;
// Creates a files in `app_group::ShareExtensionItemsFolder()` containing a
// serialized NSDictionary.
@@ -131,17 +131,19 @@
if (_image) {
[self.shareView setScreenshot:_image];
}
+ __weak ShareViewController* weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
// Center the widget.
- [self->_widgetVerticalPlacementConstraint setActive:NO];
- self->_widgetVerticalPlacementConstraint = [self->_shareView.centerYAnchor
- constraintEqualToAnchor:self.view.centerYAnchor];
- [self->_widgetVerticalPlacementConstraint setActive:YES];
- [self.maskView setAlpha:0];
+ [weakSelf.widgetVerticalPlacementConstraint setActive:NO];
+ weakSelf.widgetVerticalPlacementConstraint =
+ [weakSelf.shareView.centerYAnchor
+ constraintEqualToAnchor:self.view.centerYAnchor];
+ [weakSelf.widgetVerticalPlacementConstraint setActive:YES];
+ [weakSelf.maskView setAlpha:0];
[UIView animateWithDuration:ui_util::kAnimationDuration
animations:^{
- [self.maskView setAlpha:1];
- [self.view layoutIfNeeded];
+ [weakSelf.maskView setAlpha:1];
+ [weakSelf.view layoutIfNeeded];
}];
});
}
@@ -169,6 +171,7 @@
[UIAlertController alertControllerWithTitle:errorMessage
message:[_shareURL absoluteString]
preferredStyle:UIAlertControllerStyleAlert];
+ __weak ShareViewController* weakSelf = self;
UIAlertAction* defaultAction = [UIAlertAction
actionWithTitle:okButton
style:UIAlertActionStyleDefault
@@ -177,7 +180,7 @@
[NSError errorWithDomain:NSURLErrorDomain
code:NSURLErrorUnsupportedURL
userInfo:nil];
- [self dismissAndReturnItem:nil error:unsupportedURLError];
+ [weakSelf dismissAndReturnItem:nil error:unsupportedURLError];
}];
[alert addAction:defaultAction];
[self presentViewController:alert animated:YES completion:nil];
@@ -215,8 +218,40 @@
forAxis:UILayoutConstraintAxisHorizontal];
}
+- (void)handleURL:(id)idURL
+ forItem:(NSExtensionItem*)item
+ withError:(NSError*)error {
+ NSURL* URL = base::apple::ObjCCast<NSURL>(idURL);
+ if (!URL) {
+ [self displayErrorView];
+ return;
+ }
+ self.shareItem = item;
+ self.shareURL = URL;
+ self.shareTitle = [[item attributedContentText] string];
+ if ([self.shareTitle length] == 0) {
+ self.shareTitle = [URL host];
+ }
+ if ([[self.shareURL scheme] isEqualToString:@"http"] ||
+ [[self.shareURL scheme] isEqualToString:@"https"]) {
+ [self displayShareView];
+ } else {
+ [self displayErrorView];
+ }
+}
+
+- (void)handleImage:(id)idImage
+ forItem:(NSExtensionItem*)item
+ withError:(NSError*)error {
+ self.image = base::apple::ObjCCast<UIImage>(idImage);
+ if (self.image && self.shareView) {
+ [self.shareView setScreenshot:self.image];
+ }
+}
+
- (void)loadElementsFromContext {
NSString* typeURL = UTTypeURL.identifier;
+ __weak ShareViewController* weakSelf = self;
// TODO(crbug.com/1472758): Reorganize sharing extension handler.
BOOL foundMatch = false;
for (NSExtensionItem* item in self.extensionContext.inputItems) {
@@ -224,24 +259,10 @@
if ([itemProvider hasItemConformingToTypeIdentifier:typeURL]) {
foundMatch = true;
ItemBlock URLCompletion = ^(id idURL, NSError* error) {
- NSURL* URL = base::apple::ObjCCast<NSURL>(idURL);
- if (!URL) {
- [self displayErrorView];
- return;
- }
+ // Crash reports showed that this block can be called on a background
+ // thread. Move back the UI updating code to main thread.
dispatch_async(dispatch_get_main_queue(), ^{
- self->_shareItem = [item copy];
- self->_shareURL = [URL copy];
- self->_shareTitle = [[[item attributedContentText] string] copy];
- if ([self->_shareTitle length] == 0) {
- self->_shareTitle = [URL host];
- }
- if ([[self->_shareURL scheme] isEqualToString:@"http"] ||
- [[self->_shareURL scheme] isEqualToString:@"https"]) {
- [self displayShareView];
- } else {
- [self displayErrorView];
- }
+ [weakSelf handleURL:idURL forItem:item withError:error];
});
};
[itemProvider loadItemForTypeIdentifier:typeURL
@@ -252,12 +273,11 @@
valueWithCGSize:CGSizeMake(kScreenShotWidth, kScreenShotHeight)]
};
ItemBlock imageCompletion = ^(id imageData, NSError* error) {
- self->_image = base::apple::ObjCCast<UIImage>(imageData);
- if (self->_image && self.shareView) {
- dispatch_async(dispatch_get_main_queue(), ^{
- [self.shareView setScreenshot:self->_image];
- });
- }
+ // Crash reports showed that this block can be called on a background
+ // thread. Move back the UI updating code to main thread.
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [weakSelf handleImage:imageData forItem:item withError:error];
+ });
};
[itemProvider loadPreviewImageWithOptions:imageOptions
completionHandler:imageCompletion];
@@ -284,18 +304,19 @@
[_shareView.topAnchor constraintEqualToAnchor:self.view.bottomAnchor];
}
[_widgetVerticalPlacementConstraint setActive:YES];
+ __weak ShareViewController* weakSelf = self;
[UIView animateWithDuration:ui_util::kAnimationDuration
animations:^{
- [self.maskView setAlpha:0];
- [self.view layoutIfNeeded];
+ [weakSelf.maskView setAlpha:0];
+ [weakSelf.view layoutIfNeeded];
}
completion:^(BOOL finished) {
NSArray* returnItem = item ? @[ item ] : @[];
if (error) {
- [self.extensionContext cancelRequestWithError:error];
+ [weakSelf.extensionContext cancelRequestWithError:error];
} else {
- [self.extensionContext completeRequestReturningItems:returnItem
- completionHandler:nil];
+ [weakSelf.extensionContext completeRequestReturningItems:returnItem
+ completionHandler:nil];
}
}];
}
@@ -362,13 +383,14 @@
#pragma mark - ShareExtensionViewActionTarget
- (void)shareExtensionViewDidSelectCancel:(id)sender {
+ __weak ShareViewController* weakSelf = self;
[self
queueActionItemURL:nil
title:nil
action:app_group::READING_LIST_ITEM // Ignored
cancel:YES
completion:^{
- [self
+ [weakSelf
dismissAndReturnItem:nil
error:
[NSError
@@ -379,46 +401,43 @@
}
- (void)shareExtensionViewDidSelectAddToReadingList:(id)sender {
+ __weak ShareViewController* weakSelf = self;
[self queueActionItemURL:_shareURL
title:_shareTitle
action:app_group::READING_LIST_ITEM
cancel:NO
completion:^{
- [self dismissAndReturnItem:self->_shareItem error:nil];
+ [weakSelf dismissAndReturnItem:weakSelf.shareItem error:nil];
}];
}
- (void)shareExtensionViewDidSelectAddToBookmarks:(id)sender {
+ __weak ShareViewController* weakSelf = self;
[self queueActionItemURL:_shareURL
title:_shareTitle
action:app_group::BOOKMARK_ITEM
cancel:NO
completion:^{
- [self dismissAndReturnItem:self->_shareItem error:nil];
+ [weakSelf dismissAndReturnItem:weakSelf.shareItem error:nil];
}];
}
- (void)shareExtensionViewDidSelectOpenInChrome:(id)sender {
- UIResponder* responder = self;
- while ((responder = responder.nextResponder)) {
- if ([responder respondsToSelector:@selector(openURL:)]) {
- AppGroupCommand* command = [[AppGroupCommand alloc]
- initWithSourceApp:app_group::kOpenCommandSourceShareExtension
- URLOpenerBlock:^(NSURL* openURL) {
- [responder performSelector:@selector(openURL:)
- withObject:openURL];
- }];
- [command prepareToOpenURL:_shareURL];
- [command executeInApp];
- break;
- }
- }
+ __weak ShareViewController* weakSelf = self;
+ AppGroupCommand* command = [[AppGroupCommand alloc]
+ initWithSourceApp:app_group::kOpenCommandSourceShareExtension
+ URLOpenerBlock:^(NSURL* openURL) {
+ ExtensionOpenURL(openURL, weakSelf, nil);
+ }];
+ [command prepareToOpenURL:_shareURL];
+ [command executeInApp];
+
[self queueActionItemURL:_shareURL
title:_shareTitle
action:app_group::OPEN_IN_CHROME_ITEM
cancel:NO
completion:^{
- [self dismissAndReturnItem:self->_shareItem error:nil];
+ [weakSelf dismissAndReturnItem:weakSelf.shareItem error:nil];
}];
}
diff --git a/ios/chrome/test/earl_grey2/BUILD.gn b/ios/chrome/test/earl_grey2/BUILD.gn
index 286b574..f0e6dc43 100644
--- a/ios/chrome/test/earl_grey2/BUILD.gn
+++ b/ios/chrome/test/earl_grey2/BUILD.gn
@@ -26,6 +26,9 @@
}
chrome_ios_eg2_test_app_host("ios_chrome_eg2tests") {
+ eg_extension_target =
+ "//ios/chrome/test/eg_open_extension:appex(${current_toolchain}_app_ext)"
+ eg_extension_name = "eg_open_extension.appex"
}
chrome_ios_eg2_test_app_host("ios_chrome_multitasking_eg2tests") {
diff --git a/ios/chrome/test/earl_grey2/chrome_ios_eg2_test.gni b/ios/chrome/test/earl_grey2/chrome_ios_eg2_test.gni
index dc6ab7de..4ec103d1 100644
--- a/ios/chrome/test/earl_grey2/chrome_ios_eg2_test.gni
+++ b/ios/chrome/test/earl_grey2/chrome_ios_eg2_test.gni
@@ -6,6 +6,7 @@
import("//build/apple/tweak_info_plist.gni")
import("//build/config/ios/ios_sdk.gni")
import("//build/config/ios/rules.gni")
+import("//build/ios/extension_bundle_data.gni")
import("//ios/build/chrome_build.gni")
import("//ios/chrome/app/chrome_app.gni")
import("//ios/public/provider/chrome/browser/build_config.gni")
@@ -66,6 +67,16 @@
}
}
+ if (defined(invoker.eg_extension_target)) {
+ assert(defined(invoker.eg_extension_name),
+ "eg_extension_name must be defined too")
+ _eg_extension_bundle_data_target = target_name + "_appex_bundle_data"
+ extension_bundle_data(_eg_extension_bundle_data_target) {
+ extension_name = invoker.eg_extension_name
+ extension_target = invoker.eg_extension_target
+ }
+ }
+
ios_eg2_test_app_host(target_name) {
forward_variables_from(invoker,
[
@@ -118,6 +129,10 @@
ios_provider_target,
]
+ if (defined(invoker.eg_extension_target)) {
+ deps += [ ":$_eg_extension_bundle_data_target" ]
+ }
+
if (!defined(bundle_deps)) {
bundle_deps = []
}
@@ -132,7 +147,7 @@
"CHROMIUM_BUNDLE_ID=$bundle_identifier",
"CHROMIUM_HANDOFF_ID=$chromium_handoff_id",
"CHROMIUM_SHORT_NAME=$target_name",
- "CHROMIUM_URL_CHANNEL_SCHEME=$url_channel_scheme",
+ "CHROMIUM_URL_CHANNEL_SCHEME=${url_channel_scheme}-eg",
"CHROMIUM_URL_SCHEME_1=$url_unsecure_scheme",
"CHROMIUM_URL_SCHEME_2=$url_secure_scheme",
"CHROMIUM_URL_SCHEME_3=$url_x_callback_scheme",
diff --git a/ios/chrome/test/eg_open_extension/BUILD.gn b/ios/chrome/test/eg_open_extension/BUILD.gn
new file mode 100644
index 0000000..0b7ceae
--- /dev/null
+++ b/ios/chrome/test/eg_open_extension/BUILD.gn
@@ -0,0 +1,55 @@
+# Copyright 2023 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/apple/compile_entitlements.gni")
+import("//build/apple/tweak_info_plist.gni")
+import("//build/config/ios/rules.gni")
+import("//ios/build/chrome_build.gni")
+import("//ios/build/config.gni")
+
+tweak_info_plist("tweak_info_plist") {
+ info_plist = "Info.plist"
+}
+
+compile_entitlements("compile_entitlements") {
+ substitutions = [ "IOS_BUNDLE_ID_PREFIX=$ios_app_bundle_id_prefix" ]
+ output_name = "$target_gen_dir/eg_open_extension.appex.entitlements"
+ entitlements_templates = [ "eg_open_extension.appex.entitlements" ]
+}
+
+ios_appex_bundle("appex") {
+ output_name = "eg_open_extension"
+ deps = [ ":eg_open" ]
+
+ bundle_deps_filter = [ "//third_party/icu:icudata" ]
+ assert_no_deps = ios_extension_assert_no_deps
+
+ extra_substitutions = [
+ "CHROME_CHANNEL_SCHEME=${url_channel_scheme}-eg",
+ "CHROMIUM_SHORT_NAME=$chromium_short_name",
+ ]
+
+ entitlements_target = ":compile_entitlements"
+ info_plist_target = ":tweak_info_plist"
+ bundle_identifier = "${shared_bundle_id_for_test_apps}.EGOpenExtension"
+}
+
+source_set("eg_open") {
+ sources = [
+ "eg_open_view_controller.h",
+ "eg_open_view_controller.mm",
+ ]
+
+ deps = [
+ "//base",
+ "//ios/chrome/common:extension_open_url",
+ ]
+
+ frameworks = [
+ "Foundation.framework",
+ "MobileCoreServices.framework",
+ "UIKit.framework",
+ "UniformTypeIdentifiers.framework",
+ ]
+}
diff --git a/ios/chrome/test/eg_open_extension/Info.plist b/ios/chrome/test/eg_open_extension/Info.plist
new file mode 100644
index 0000000..aa1678c
--- /dev/null
+++ b/ios/chrome/test/eg_open_extension/Info.plist
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleIconFile</key>
+ <string>ExtensionIcon</string>
+ <key>LSApplicationCategoryType</key>
+ <string></string>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleDisplayName</key>
+ <string>EGOpenExtension</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>${BUNDLE_IDENTIFIER}</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundlePackageType</key>
+ <string>XPC!</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+ <key>KSChannelChromeScheme</key>
+ <string>${CHROME_CHANNEL_SCHEME}</string>
+ <key>NSExtension</key>
+ <dict>
+ <key>XSAppIconAssets</key>
+ <string></string>
+ <key>NSExtensionAttributes</key>
+ <dict>
+ <key>NSExtensionActivationRule</key>
+ <string>
+ SUBQUERY (
+ extensionItems,
+ $extensionItem,
+ SUBQUERY (
+ $extensionItem.attachments,
+ $attachment,
+ (
+ ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url"
+ )
+ AND (
+ NOT (
+ ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.file-url"
+ )
+ )
+ ).@count == 1
+ ).@count == 1
+ </string>
+ </dict>
+ <key>NSExtensionPrincipalClass</key>
+ <string>EGOpenViewController</string>
+ <key>NSExtensionPointIdentifier</key>
+ <string>com.apple.ui-services</string>
+ </dict>
+ <key>UIRequiredDeviceCapabilities</key>
+ <array>
+ <string>arm64</string>
+ </array>
+</dict>
+</plist>
diff --git a/ios/chrome/test/eg_open_extension/eg_open_extension.appex.entitlements b/ios/chrome/test/eg_open_extension/eg_open_extension.appex.entitlements
new file mode 100644
index 0000000..061639e
--- /dev/null
+++ b/ios/chrome/test/eg_open_extension/eg_open_extension.appex.entitlements
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>application-identifier</key>
+ <string>$(AppIdentifierPrefix)$(CFBundleIdentifier)</string>
+</dict>
+</plist>
diff --git a/ios/chrome/test/eg_open_extension/eg_open_view_controller.h b/ios/chrome/test/eg_open_extension/eg_open_view_controller.h
new file mode 100644
index 0000000..0c7d748
--- /dev/null
+++ b/ios/chrome/test/eg_open_extension/eg_open_view_controller.h
@@ -0,0 +1,14 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef IOS_CHROME_TEST_EG_OPEN_EXTENSION_EG_OPEN_VIEW_CONTROLLER_H_
+#define IOS_CHROME_TEST_EG_OPEN_EXTENSION_EG_OPEN_VIEW_CONTROLLER_H_
+
+#import <UIKit/UIKit.h>
+
+@interface EGOpenViewController : UIViewController
+
+@end
+
+#endif // IOS_CHROME_TEST_EG_OPEN_EXTENSION_EG_OPEN_VIEW_CONTROLLER_H_
diff --git a/ios/chrome/test/eg_open_extension/eg_open_view_controller.mm b/ios/chrome/test/eg_open_extension/eg_open_view_controller.mm
new file mode 100644
index 0000000..62218f2
--- /dev/null
+++ b/ios/chrome/test/eg_open_extension/eg_open_view_controller.mm
@@ -0,0 +1,87 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "ios/chrome/test/eg_open_extension/eg_open_view_controller.h"
+
+#import <Foundation/Foundation.h>
+#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
+
+#import "base/apple/bundle_locations.h"
+#import "base/apple/foundation_util.h"
+#import "ios/chrome/common/extension_open_url.h"
+
+// Type for completion handler to fetch the components of the share items.
+// `idResponse` type depends on the element beeing fetched.
+using ItemBlock = void (^)(id idResponse, NSError* error);
+
+@implementation EGOpenViewController
+
+- (void)viewDidLoad {
+ [super viewDidLoad];
+ [self loadElementsFromContext];
+}
+
+- (void)loadElementsFromContext {
+ NSString* typeURL = UTTypeURL.identifier;
+ BOOL foundMatch = false;
+ for (NSExtensionItem* item in self.extensionContext.inputItems) {
+ for (NSItemProvider* itemProvider in item.attachments) {
+ if ([itemProvider hasItemConformingToTypeIdentifier:typeURL]) {
+ foundMatch = true;
+ __weak __typeof(self) weakSelf = self;
+ ItemBlock URLCompletion = ^(id idURL, NSError* error) {
+ NSURL* URL = base::apple::ObjCCast<NSURL>(idURL);
+ if (!URL) {
+ [weakSelf cancel];
+ return;
+ }
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [weakSelf shareItem:item url:URL];
+ });
+ };
+ [itemProvider loadItemForTypeIdentifier:typeURL
+ options:nil
+ completionHandler:URLCompletion];
+ }
+ }
+ }
+ if (!foundMatch) {
+ [self cancel];
+ }
+}
+
+- (void)cancel {
+ [self.extensionContext
+ cancelRequestWithError:[NSError errorWithDomain:NSURLErrorDomain
+ code:NSURLErrorUnknown
+ userInfo:nil]];
+}
+
+- (void)shareItem:(NSExtensionItem*)item url:(NSURL*)URL {
+ NSString* scheme =
+ base::apple::ObjCCast<NSString>([base::apple::FrameworkBundle()
+ objectForInfoDictionaryKey:@"KSChannelChromeScheme"]);
+ // KSChannelChromeScheme opens the URLs in HTTPS by default, but EG tests only
+ // support HTTP. Embed the URL in x-callback-url to force HTTP.
+ NSString* encodedURL =
+ [[URL absoluteString] stringByAddingPercentEncodingWithAllowedCharacters:
+ [NSCharacterSet URLQueryAllowedCharacterSet]];
+ NSURL* urlToOpen = [NSURL
+ URLWithString:[NSString
+ stringWithFormat:@"%@://x-callback-url/open?url=%@",
+ scheme, encodedURL]];
+ if (!scheme) {
+ [self cancel];
+ return;
+ }
+ bool result = ExtensionOpenURL(urlToOpen, self, nil);
+ if (!result) {
+ [self cancel];
+ return;
+ }
+ [self.extensionContext completeRequestReturningItems:@[ item ]
+ completionHandler:nil];
+}
+
+@end