[iOS] Add Support for Running Web Tests on iOS Blink Port

In the iOS Blink port, running web tests is essential
to assess the level of support for various web features
and functionalities. This change includes necessary
additions to enable the execution of web tests on the
iOS Blink port.

co-author: mkim@ solved the timeout issue by adding
a custom UIApplication that overrides IsRunningTests.

Bug: 1421239
Change-Id: I8835097f628280c76d1213a16a29d6cdddd19ecb
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4712370
Reviewed-by: Stephen McGruer <[email protected]>
Reviewed-by: Dave Tapuska <[email protected]>
Reviewed-by: Rohit Rao <[email protected]>
Commit-Queue: Gyuyoung Kim <[email protected]>
Reviewed-by: Peter Beverloo <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1191108}
diff --git a/BUILD.gn b/BUILD.gn
index 99a80c7..ce09710 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -1041,7 +1041,7 @@
   }
 }
 
-if (!is_ios && !is_cronet_build) {
+if (use_blink && !is_cronet_build) {
   group("chromedriver_group") {
     testonly = true
 
@@ -1056,7 +1056,7 @@
       if (is_android && !is_cast_android) {
         deps += [ "//chrome/test/chromedriver/test/webview_shell:chromedriver_webview_shell_apk" ]
       }
-    } else if (!is_castos) {
+    } else if (!is_castos && !is_ios) {
       deps = [
         "//chrome/test/chromedriver:chromedriver_server",
         "//chrome/test/chromedriver:chromedriver_unittests",
diff --git a/content/shell/BUILD.gn b/content/shell/BUILD.gn
index 9451cb0..f770bd9 100644
--- a/content/shell/BUILD.gn
+++ b/content/shell/BUILD.gn
@@ -120,6 +120,8 @@
     sources += [
       "app/ios/shell_application_ios.h",
       "app/ios/shell_application_ios.mm",
+      "app/ios/web_tests_support_ios.h",
+      "app/ios/web_tests_support_ios.mm",
     ]
   }
   defines = [
@@ -650,6 +652,7 @@
 
     deps = [
       ":content_shell_app",
+      ":content_shell_lib",
       ":pak",
       "//build/win:default_exe_manifest",
       "//content/public/app",
diff --git a/content/shell/app/ios/web_tests_support_ios.h b/content/shell/app/ios/web_tests_support_ios.h
new file mode 100644
index 0000000..5467978
--- /dev/null
+++ b/content/shell/app/ios/web_tests_support_ios.h
@@ -0,0 +1,10 @@
+// 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 CONTENT_SHELL_APP_IOS_WEB_TESTS_SUPPORT_IOS_H_
+#define CONTENT_SHELL_APP_IOS_WEB_TESTS_SUPPORT_IOS_H_
+
+int RunWebTestsFromIOSApp(int argc, const char* _Nullable* _Nullable argv);
+
+#endif  // CONTENT_SHELL_APP_IOS_WEB_TESTS_SUPPORT_IOS_H_
diff --git a/content/shell/app/ios/web_tests_support_ios.mm b/content/shell/app/ios/web_tests_support_ios.mm
new file mode 100644
index 0000000..10b2b32
--- /dev/null
+++ b/content/shell/app/ios/web_tests_support_ios.mm
@@ -0,0 +1,92 @@
+// 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 "content/shell/app/ios/web_tests_support_ios.h"
+
+#import <UIKit/UIKit.h>
+
+#include "base/message_loop/message_pump.h"
+#include "base/message_loop/message_pump_apple.h"
+#include "content/public/app/content_main.h"
+#include "content/public/app/content_main_runner.h"
+#include "content/public/common/main_function_params.h"
+#include "content/shell/app/shell_main_delegate.h"
+
+static int g_argc = 0;
+static const char** g_argv = nullptr;
+static std::unique_ptr<content::ContentMainRunner> g_main_runner;
+static std::unique_ptr<content::ShellMainDelegate> g_main_delegate;
+
+@interface WebTestApplication : UIApplication
+- (BOOL)isRunningTests;
+@end
+
+@implementation WebTestApplication
+// Need to return YES for testing. If not, a lot of timeouts happen during the
+// test.
+- (BOOL)isRunningTests {
+  return YES;
+}
+@end
+
+@interface WebTestDelegate : UIResponder <UIApplicationDelegate>
+@end
+
+@implementation WebTestDelegate
+
+- (UISceneConfiguration*)application:(UIApplication*)application
+    configurationForConnectingSceneSession:
+        (UISceneSession*)connectingSceneSession
+                                   options:(UISceneConnectionOptions*)options {
+  UISceneConfiguration* configuration =
+      [[UISceneConfiguration alloc] initWithName:nil
+                                     sessionRole:connectingSceneSession.role];
+  return configuration;
+}
+
+- (BOOL)application:(UIApplication*)application
+    willFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
+  g_main_delegate = std::make_unique<content::ShellMainDelegate>();
+  content::ContentMainParams params(g_main_delegate.get());
+  params.argc = g_argc;
+  params.argv = g_argv;
+  g_main_runner = content::ContentMainRunner::Create();
+  content::RunContentProcess(std::move(params), g_main_runner.get());
+  return YES;
+}
+
+- (BOOL)application:(UIApplication*)application
+    didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
+  return YES;
+}
+
+- (BOOL)application:(UIApplication*)application
+    shouldSaveSecureApplicationState:(NSCoder*)coder {
+  return YES;
+}
+
+@end
+
+namespace {
+
+std::unique_ptr<base::MessagePump> CreateMessagePumpForUIForTests() {
+  return std::unique_ptr<base::MessagePump>(new base::MessagePumpCFRunLoop());
+}
+
+}  // namespace
+
+void InitIOSWebTestMessageLoop() {
+  base::MessagePump::OverrideMessagePumpForUIFactory(
+      &CreateMessagePumpForUIForTests);
+}
+
+int RunWebTestsFromIOSApp(int argc, const char** argv) {
+  g_argc = argc;
+  g_argv = argv;
+  InitIOSWebTestMessageLoop();
+  @autoreleasepool {
+    return UIApplicationMain(argc, const_cast<char**>(argv),
+                             @"WebTestApplication", @"WebTestDelegate");
+  }
+}
diff --git a/content/shell/app/shell_main.cc b/content/shell/app/shell_main.cc
index 55d06f7..80d6fe7 100644
--- a/content/shell/app/shell_main.cc
+++ b/content/shell/app/shell_main.cc
@@ -18,6 +18,8 @@
 #include "base/command_line.h"                            // nogncheck
 #include "content/public/common/content_switches.h"       // nogncheck
 #include "content/shell/app/ios/shell_application_ios.h"
+#include "content/shell/app/ios/web_tests_support_ios.h"
+#include "content/shell/common/shell_switches.h"
 #endif
 
 #if BUILDFLAG(IS_WIN)
@@ -58,8 +60,13 @@
 
   // The browser process has no --process-type argument.
   if (type.empty()) {
-    // We will create the ContentMainRunner once the UIApplication is ready.
-    return RunShellApplication(argc, argv);
+    if (switches::IsRunWebTestsSwitchPresent()) {
+      // We create a simple UIApplication to run the web tests.
+      return RunWebTestsFromIOSApp(argc, argv);
+    } else {
+      // We will create the ContentMainRunner once the UIApplication is ready.
+      return RunShellApplication(argc, argv);
+    }
   } else {
     content::ShellMainDelegate delegate;
     content::ContentMainParams params(&delegate);
diff --git a/content/shell/app/shell_main_delegate.cc b/content/shell/app/shell_main_delegate.cc
index 6ca54d4..27048f8 100644
--- a/content/shell/app/shell_main_delegate.cc
+++ b/content/shell/app/shell_main_delegate.cc
@@ -248,6 +248,18 @@
   base::trace_event::TraceLog::GetInstance()->SetProcessSortIndex(
       kTraceEventBrowserProcessSortIndex);
 
+#if !BUILDFLAG(IS_ANDROID)
+  if (switches::IsRunWebTestsSwitchPresent()) {
+    // Web tests implement their own BrowserMain() replacement.
+    web_test_runner_->RunBrowserMain(std::move(main_function_params));
+    web_test_runner_.reset();
+    // Returning 0 to indicate that we have replaced BrowserMain() and the
+    // caller should not call BrowserMain() itself. Web tests do not ever
+    // return an error.
+    return 0;
+  }
+#endif
+
 #if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS)
   // On Android and iOS, we defer to the system message loop when the stack
   // unwinds. So here we only create (and leak) a BrowserMainRunner. The
@@ -267,16 +279,6 @@
   // to the |ui_task| for browser tests.
   return 0;
 #else
-  if (switches::IsRunWebTestsSwitchPresent()) {
-    // Web tests implement their own BrowserMain() replacement.
-    web_test_runner_->RunBrowserMain(std::move(main_function_params));
-    web_test_runner_.reset();
-    // Returning 0 to indicate that we have replaced BrowserMain() and the
-    // caller should not call BrowserMain() itself. Web tests do not ever
-    // return an error.
-    return 0;
-  }
-
   // On non-Android, we can return the |main_function_params| back and have the
   // caller run BrowserMain() normally.
   return std::move(main_function_params);
diff --git a/content/web_test/browser/test_info_extractor.cc b/content/web_test/browser/test_info_extractor.cc
index e94c6d6..5961cef 100644
--- a/content/web_test/browser/test_info_extractor.cc
+++ b/content/web_test/browser/test_info_extractor.cc
@@ -17,6 +17,13 @@
 #include "content/web_test/common/web_test_switches.h"
 #include "net/base/filename_util.h"
 
+#if BUILDFLAG(IS_IOS)
+#include <fstream>
+
+#include "base/files/file_util.h"
+#include "base/threading/platform_thread.h"
+#endif
+
 namespace content {
 
 namespace {
@@ -78,6 +85,21 @@
                                     protocol_mode);
 }
 
+#if BUILDFLAG(IS_IOS)
+std::ifstream GetFileStreamToReadTestFileName() {
+  base::FilePath temp_dir;
+  if (!base::GetTempDir(&temp_dir)) {
+    LOG(ERROR) << "GetTempDir failed.";
+    return std::ifstream();
+  }
+
+  std::string test_input_file_path =
+      temp_dir.AppendASCII("webtest_test_name").value();
+  std::ifstream file_name_input_stream(test_input_file_path);
+  return file_name_input_stream;
+}
+#endif
+
 }  // namespace
 
 TestInfo::TestInfo(const GURL& url,
@@ -105,11 +127,31 @@
   std::string test_string;
   bool protocol_mode = false;
   if (cmdline_args_[cmdline_position_] == FILE_PATH_LITERAL("-")) {
+#if BUILDFLAG(IS_IOS)
+    // TODO(crbug.com/1421239): iOS port reads the test file through a file
+    // stream until using sockets for the communication between run_web_tests.py
+    // and content_shell.
+    std::ifstream file_name_input = GetFileStreamToReadTestFileName();
+    if (!file_name_input.is_open()) {
+      return nullptr;
+    }
+    do {
+      // Need to wait for a while to wait until write function of
+      // |server_process.py| writes a test name in the file.
+      base::PlatformThread::Sleep(base::Milliseconds(10));
+      bool success = !!std::getline(file_name_input, test_string, '\n');
+      if (!success) {
+        return nullptr;
+      }
+    } while (test_string.empty());
+    file_name_input.close();
+#else
     do {
       bool success = !!std::getline(std::cin, test_string, '\n');
       if (!success)
         return nullptr;
     } while (test_string.empty());
+#endif  // BUILDFLAG(IS_IOS)
     protocol_mode = true;
   } else {
 #if BUILDFLAG(IS_WIN)
diff --git a/docs/testing/web_test_expectations.md b/docs/testing/web_test_expectations.md
index bca7b48..afb336c0 100644
--- a/docs/testing/web_test_expectations.md
+++ b/docs/testing/web_test_expectations.md
@@ -288,7 +288,7 @@
 * If specified, modifiers must be one of `Fuchsia`, `Mac`, `Mac10.13`,
   `Mac10.14`, `Mac10.15`, `Mac11`, `Mac11-arm64`, `Mac12`, `Mac12-arm64`,
   `Mac13`, `Mac13-arm64`, `Linux`, `Trusty`, `Win`, `Win10.20h2`,
-  `Win11`, and, optionally, `Release`, or `Debug`. Check the top of
+  `Win11`, `iOS16-Simulator`, and, optionally, `Release`, or `Debug`. Check the top of
   [TestExpectations](../../third_party/blink/web_tests/TestExpectations) or the
   `ALL_SYSTEMS` macro in
   [third_party/blink/tools/blinkpy/web_tests/port/base.py](../../third_party/blink/tools/blinkpy/web_tests/port/base.py)
diff --git a/testing/iossim/iossim.mm b/testing/iossim/iossim.mm
index 4696b411..0104025 100644
--- a/testing/iossim/iossim.mm
+++ b/testing/iossim/iossim.mm
@@ -280,6 +280,76 @@
   [task run];
 }
 
+NSString* GetBundleIdentifierFromPath(NSString* app_path) {
+  NSFileManager* file_manager = [NSFileManager defaultManager];
+  NSString* info_plist_path =
+      [app_path stringByAppendingPathComponent:@"Info.plist"];
+  if (![file_manager fileExistsAtPath:info_plist_path]) {
+    return nil;
+  }
+
+  NSDictionary* info_dictionary =
+      [NSDictionary dictionaryWithContentsOfFile:info_plist_path];
+  NSString* bundle_identifier = info_dictionary[@"CFBundleIdentifier"];
+  return bundle_identifier;
+}
+
+void PrepareWebTests(NSString* udid, NSString* app_path) {
+  NSString* bundle_identifier = GetBundleIdentifierFromPath(app_path);
+  XCRunTask* uninstall_task = [[XCRunTask alloc]
+      initWithArguments:@[ @"simctl", @"uninstall", udid, bundle_identifier ]];
+  [uninstall_task run];
+
+  XCRunTask* install_task = [[XCRunTask alloc]
+      initWithArguments:@[ @"simctl", @"install", udid, app_path ]];
+  [install_task run];
+
+  XCRunTask* launch_task = [[XCRunTask alloc]
+      initWithArguments:@[ @"simctl", @"launch", udid, bundle_identifier ]];
+  [launch_task run];
+
+  XCRunTask* terminate_task = [[XCRunTask alloc]
+      initWithArguments:@[ @"simctl", @"terminate", udid, bundle_identifier ]];
+  [terminate_task run];
+}
+
+int RunWebTest(NSString* app_path, NSString* udid, NSMutableArray* cmd_args) {
+  NSMutableArray* arguments = [NSMutableArray array];
+  [arguments addObject:@"simctl"];
+  [arguments addObject:@"launch"];
+  [arguments addObject:@"--console"];
+  [arguments addObject:@"--terminate-running-process"];
+  [arguments addObject:udid];
+  [arguments addObject:GetBundleIdentifierFromPath(app_path)];
+  if (cmd_args.count == 1) {
+    for (NSString* arg in [cmd_args[0] componentsSeparatedByString:@" "]) {
+      [arguments addObject:arg];
+    }
+  }
+  [arguments addObject:@"-"];
+  XCRunTask* task = [[XCRunTask alloc] initWithArguments:arguments];
+
+  // The following stderr message causes a lot of test faiures on the web
+  // tests. Strip the message here.
+  NSArray* ignore_strings = @[ @"Class SwapLayerEAGL" ];
+  NSPipe* stderr_pipe = [NSPipe pipe];
+  stderr_pipe.fileHandleForReading.readabilityHandler =
+      ^(NSFileHandle* handle) {
+        NSString* log = [[NSString alloc] initWithData:handle.availableData
+                                              encoding:NSUTF8StringEncoding];
+        for (NSString* ignore_string in ignore_strings) {
+          if ([log rangeOfString:ignore_string].location != NSNotFound) {
+            return;
+          }
+        }
+        fprintf(stderr, "%s", log.UTF8String);
+      };
+  task.standardError = stderr_pipe;
+
+  [task run];
+  return [task terminationStatus];
+}
+
 int RunApplication(NSString* app_path,
                    NSString* xctest_path,
                    NSString* udid,
@@ -390,6 +460,8 @@
   NSString* device_name = @"iPhone 6s";
   bool wants_wipe = false;
   bool wants_print_home = false;
+  bool run_web_test = false;
+  bool prepare_web_test = false;
   NSDictionary* simctl_list = GetSimulatorList();
   float sdk = 0;
   for (NSDictionary* runtime in Runtimes(simctl_list)) {
@@ -450,6 +522,23 @@
     }
   }
 
+  NSRange range;
+  for (NSString* cmd_arg in cmd_args) {
+    range = [cmd_arg rangeOfString:@"--run-web-tests"];
+    if (range.location != NSNotFound) {
+      run_web_test = true;
+      break;
+    }
+  }
+
+  for (NSString* cmd_arg in cmd_args) {
+    range = [cmd_arg rangeOfString:@"--prepare-web-tests"];
+    if (range.location != NSNotFound) {
+      prepare_web_test = true;
+      break;
+    }
+  }
+
   if (udid == nil) {
     udid = GetDeviceBySDKAndName(simctl_list, device_name, sdk_version);
     if (udid == nil) {
@@ -476,7 +565,12 @@
     exit(kExitSuccess);
   }
 
-  KillSimulator();
+  // To run the web test, the simulator should work. So we do not kill the
+  // simulator when running the web tests.
+  if (!run_web_test && !prepare_web_test) {
+    KillSimulator();
+  }
+
   if (wants_wipe) {
     WipeDevice(udid);
     printf("Device wiped.\n");
@@ -511,8 +605,22 @@
     exit(kExitInvalidArguments);
   }
 
-  int return_code = RunApplication(app_path, xctest_path, udid, app_env,
-                                   cmd_args, tests_filter);
-  KillSimulator();
+  if (prepare_web_test) {
+    PrepareWebTests(udid, app_path);
+    exit(kExitSuccess);
+  }
+
+  int return_code = -1;
+  if (run_web_test) {
+    return_code = RunWebTest(app_path, udid, cmd_args);
+  } else {
+    return_code = RunApplication(app_path, xctest_path, udid, app_env, cmd_args,
+                                 tests_filter);
+  }
+
+  if (!run_web_test) {
+    KillSimulator();
+  }
+
   return return_code;
 }
diff --git a/third_party/blink/tools/blinkpy/web_tests/port/base.py b/third_party/blink/tools/blinkpy/web_tests/port/base.py
index 2ad1008..217d35a 100644
--- a/third_party/blink/tools/blinkpy/web_tests/port/base.py
+++ b/third_party/blink/tools/blinkpy/web_tests/port/base.py
@@ -166,6 +166,7 @@
         ('win11', 'x86_64'),
         ('linux', 'x86_64'),
         ('fuchsia', 'x86_64'),
+        ('ios16-simulator', 'x86_64'),
     )
 
     CONFIGURATION_SPECIFIER_MACROS = {
@@ -173,6 +174,7 @@
             'mac10.15', 'mac11', 'mac11-arm64', 'mac12', 'mac12-arm64',
             'mac13', 'mac13-arm64'
         ],
+        'ios': ['ios16-simulator'],
         'win': ['win10.20h2', 'win11-arm64', 'win11'],
         'linux': ['linux'],
         'fuchsia': ['fuchsia'],
diff --git a/third_party/blink/tools/blinkpy/web_tests/port/factory.py b/third_party/blink/tools/blinkpy/web_tests/port/factory.py
index 1305060..940097d 100644
--- a/third_party/blink/tools/blinkpy/web_tests/port/factory.py
+++ b/third_party/blink/tools/blinkpy/web_tests/port/factory.py
@@ -42,6 +42,7 @@
     PORT_CLASSES = (
         'android.AndroidPort',
         'fuchsia.FuchsiaPort',
+        'ios.IOSPort',
         'linux.LinuxPort',
         'mac.MacPort',
         'mock_drt.MockDRTPort',
diff --git a/third_party/blink/tools/blinkpy/web_tests/port/ios.py b/third_party/blink/tools/blinkpy/web_tests/port/ios.py
new file mode 100644
index 0000000..63486b4
--- /dev/null
+++ b/third_party/blink/tools/blinkpy/web_tests/port/ios.py
@@ -0,0 +1,119 @@
+# 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.
+"""Chromium iOS implementation of the Port interface."""
+
+import logging
+
+from blinkpy.web_tests.port.ios_simulator_server_process import IOSSimulatorServerProcess
+from blinkpy.web_tests.port import base
+from blinkpy.web_tests.port import driver
+
+_log = logging.getLogger(__name__)
+
+
+class IOSPort(base.Port):
+    SUPPORTED_VERSIONS = ('ios16-simulator', )
+
+    port_name = 'ios'
+
+    FALLBACK_PATHS = {}
+
+    FALLBACK_PATHS['ios16-simulator'] = ['ios']
+
+    CONTENT_SHELL_NAME = 'content_shell'
+
+    BUILD_REQUIREMENTS_URL = 'https://chromium.googlesource.com/chromium/src/+/main/docs/ios_build_instructions.md'
+
+    @classmethod
+    def determine_full_port_name(cls, host, options, port_name):
+        if port_name.endswith('ios'):
+            parts = [port_name, 'ios16-simulator']
+            return '-'.join(parts)
+        return port_name
+
+    def __init__(self, host, port_name, **kwargs):
+        super(IOSPort, self).__init__(host, port_name, **kwargs)
+        self.server_process_constructor = IOSSimulatorServerProcess
+        self._version = port_name[port_name.index('ios-') + len('ios-'):]
+
+    def check_build(self, needs_http, printer):
+        result = super(IOSPort, self).check_build(needs_http, printer)
+        if result:
+            _log.error('For complete ios build requirements, please see:')
+            _log.error('')
+            _log.error(BUILD_REQUIREMENTS_URL)
+
+        return result
+
+    def cmd_line(self):
+        return [
+            self._path_to_simulator(), '-d',
+            self.device_name(), '-c',
+            '%s -' % self.additional_driver_flags()
+        ]
+
+    def reinstall_cmd_line(self):
+        return [
+            self._path_to_simulator(), '-d',
+            self.device_name(), '-c', '--prepare-web-tests',
+            self.path_to_driver()
+        ]
+
+    def _path_to_simulator(self, target=None):
+        return self.build_path('iossim', target=target)
+
+    def path_to_driver(self, target=None):
+        return self.build_path(self.driver_name() + '.app', target=target)
+
+    def device_name(self, target=None):
+        return 'iPhone 13'
+
+    def _driver_class(self):
+        return ChromiumIOSDriver
+
+    #
+    # PROTECTED METHODS
+    #
+
+    def operating_system(self):
+        return 'ios'
+
+    def additional_driver_flags(self):
+        flags = (
+            '--run-web-tests --ignore-certificate-errors-spki-list=%s,%s,%s --webtransport-developer-mode --user-data-dir') % \
+            (base.WPT_FINGERPRINT, base.SXG_FINGERPRINT, base.SXG_WPT_FINGERPRINT)
+        return flags
+
+    def path_to_apache(self):
+        import platform
+        if platform.machine() == 'arm64':
+            return self._path_from_chromium_base('third_party',
+                                                 'apache-mac-arm64', 'bin',
+                                                 'httpd')
+        return self._path_from_chromium_base('third_party', 'apache-mac',
+                                             'bin', 'httpd')
+
+    def path_to_apache_config_file(self):
+        config_file_basename = 'apache2-httpd-%s-php7.conf' % (
+            self._apache_version(), )
+        return self._filesystem.join(self.apache_config_directory(),
+                                     config_file_basename)
+
+    def setup_test_run(self):
+        super(IOSPort, self).setup_test_run()
+        # Because the tests are being run on a simulator rather than directly on this
+        # device, re-deploy the content shell app to the simulator to ensure it is up
+        # to date.
+        self.host.executive.run_command(self.reinstall_cmd_line())
+
+
+class ChromiumIOSDriver(driver.Driver):
+    def __init__(self, port, worker_number, no_timeout=False):
+        super(ChromiumIOSDriver, self).__init__(port, worker_number,
+                                                no_timeout)
+
+    def cmd_line(self, per_test_args):
+        cmd = self._port.cmd_line()
+        cmd += self._base_cmd_line()
+        return cmd
diff --git a/third_party/blink/tools/blinkpy/web_tests/port/ios_simulator_server_process.py b/third_party/blink/tools/blinkpy/web_tests/port/ios_simulator_server_process.py
new file mode 100644
index 0000000..312ee1d
--- /dev/null
+++ b/third_party/blink/tools/blinkpy/web_tests/port/ios_simulator_server_process.py
@@ -0,0 +1,126 @@
+# 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 json
+import logging
+
+from blinkpy.web_tests.port.server_process import ServerProcess
+
+_log = logging.getLogger(__name__)
+
+
+# Define a custom version of ServerProcess for running tests on the iOS
+# simulator. The default ServerProcess does not work as it uses
+# stdin/stdout/stderr to communicate, which the iOS simulator does not
+# allow for security.
+#
+# TODO(crbug.com/1421239): iOS port communicates with the content shell
+# through a file handle temporarily. Socket connection should be used
+# instead.
+class IOSSimulatorServerProcess(ServerProcess):
+    def __init__(self,
+                 port_obj,
+                 name,
+                 cmd,
+                 env=None,
+                 treat_no_data_as_crash=False,
+                 more_logging=False):
+        super(IOSSimulatorServerProcess,
+              self).__init__(port_obj, name, cmd, env, treat_no_data_as_crash,
+                             more_logging)
+
+        self._web_test_path_file = self._get_web_test_file_path()
+        if not self._web_test_path_file:
+            raise RuntimeError('_web_test_path_file does not exist.')
+
+        # Create a file at the path.
+        test_file_handle = open(self._web_test_path_file, 'wb')
+        test_file_handle.close()
+
+    def _get_web_test_file_path(self):
+        simulator_root = self._host.filesystem.expanduser(
+            "~/Library/Developer/CoreSimulator/Devices/")
+        udid = self._get_simulator_udid()
+        if not udid:
+            raise RuntimeError('The udid value of the Simulator is none.')
+
+        app_data_path = self._host.filesystem.join(
+            simulator_root, udid, "data/Containers/Data/Application")
+
+        content_shell_dir = self._get_content_shell_dir(app_data_path)
+        if not content_shell_dir:
+            _log.error('Cannot find the content shell directory.')
+            return None
+
+        content_shell_data_path = self._host.filesystem.join(
+            app_data_path, content_shell_dir)
+        content_shell_tmp_path = self._host.filesystem.join(
+            content_shell_data_path, "tmp")
+        if not self._host.filesystem.exists(content_shell_tmp_path):
+            raise RuntimeError('%s path does not exist.' %
+                               content_shell_tmp_path)
+
+        return self._host.filesystem.join(content_shell_tmp_path,
+                                          "webtest_test_name")
+
+    def _get_content_shell_dir(self, app_data_path):
+        for app_dir in self._host.filesystem.listdir(app_data_path):
+            # Check if |app_dir| has the content shell directory.
+            content_shell_dir = self._host.filesystem.join(
+                app_data_path, app_dir,
+                "Library/Application Support/Chromium Content Shell")
+            if self._host.filesystem.exists(content_shell_dir):
+                return app_dir
+        return None
+
+    def _get_simulator_udid(self):
+        device = self._get_device(self._port.device_name())
+        if not device:
+            _log.error('There is no available device.')
+            return None
+        udid = device.get("udid")
+        if not udid:
+            _log.error('Cannot find the udid of the iOS simulator.')
+            return None
+        return udid
+
+    def _get_device(self, device_name):
+        devices = json.loads(self._simctl('devices available'))
+        if len(devices) == 0:
+            raise RuntimeError('No available device in the iOS simulator.')
+        runtime = self._latest_runtime()
+        return next(
+            (d
+             for d in devices['devices'][runtime] if d['name'] == device_name),
+            None)
+
+    def _latest_runtime(self):
+        runtimes = json.loads(self._simctl('runtimes available'))
+        valid_runtimes = [
+            runtime for runtime in runtimes['runtimes']
+            if 'identifier' in runtime and runtime['identifier'].startswith(
+                'com.apple.CoreSimulator.SimRuntime')
+        ]
+        if len(valid_runtimes) == 0:
+            raise RuntimeError('No valid runtime in the iOS simulator.')
+
+        valid_runtimes.sort(key=lambda runtime: float(runtime['version']),
+                            reverse=True)
+        return valid_runtimes[0]['identifier']
+
+    def _simctl(self, command):
+        prefix_commands = ['/usr/bin/xcrun', 'simctl', 'list', '-j']
+        command_array = prefix_commands + command.split()
+        return self._host.executive.run_command(command_array)
+
+    #
+    # PROTECTED METHODS
+    #
+
+    def write(self, bytes):
+        super().write(bytes)
+        # iOS application can't communicate with the Mac host through stdin yet.
+        # Instead a file stream is used for the communication temporarily.
+        self._host.filesystem.write_binary_file(self._web_test_path_file,
+                                                bytes)
diff --git a/third_party/blink/web_tests/TestExpectations b/third_party/blink/web_tests/TestExpectations
index f319b86..46d023e3 100644
--- a/third_party/blink/web_tests/TestExpectations
+++ b/third_party/blink/web_tests/TestExpectations
@@ -1,4 +1,4 @@
-# tags: [ Fuchsia Linux Mac Mac10.15 Mac11 Mac11-arm64 Mac12 Mac12-arm64 Mac13 Mac13-arm64 Win Win10.20h2 Win11 Win11-arm64 ]
+# tags: [ Fuchsia Linux Mac Mac10.15 Mac11 Mac11-arm64 Mac12 Mac12-arm64 Mac13 Mac13-arm64 Win Win10.20h2 Win11 Win11-arm64 iOS16-simulator]
 # tags: [ Release Debug ]
 # results: [ Timeout Crash Pass Failure Skip ]