[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 &quot;public.url&quot;
+						)
+						AND (
+							NOT (
+								ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO &quot;public.file-url&quot;
+							)
+						)
+					).@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