feat(tools): Arbitrary browser flags (closes #65575)  (#104935)

diff --git a/packages/flutter_tools/lib/src/commands/drive.dart b/packages/flutter_tools/lib/src/commands/drive.dart
index ff1d1a5..3e4ca94 100644
--- a/packages/flutter_tools/lib/src/commands/drive.dart
+++ b/packages/flutter_tools/lib/src/commands/drive.dart
@@ -279,6 +279,7 @@
         packageConfig,
         chromeBinary: stringArgDeprecated('chrome-binary'),
         headless: boolArgDeprecated('headless'),
+        webBrowserFlags: stringsArg(FlutterOptions.kWebBrowserFlag),
         browserDimension: stringArgDeprecated('browser-dimension')!.split(','),
         browserName: stringArgDeprecated('browser-name'),
         driverPort: stringArgDeprecated('driver-port') != null
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart
index d986f7c..8b05c2c 100644
--- a/packages/flutter_tools/lib/src/commands/run.dart
+++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -202,9 +202,12 @@
   @protected
   Future<DebuggingOptions> createDebuggingOptions(bool webMode) async {
     final BuildInfo buildInfo = await getBuildInfo();
-    final int? browserDebugPort = featureFlags.isWebEnabled && argResults!.wasParsed('web-browser-debug-port')
+    final int? webBrowserDebugPort = featureFlags.isWebEnabled && argResults!.wasParsed('web-browser-debug-port')
       ? int.parse(stringArgDeprecated('web-browser-debug-port')!)
       : null;
+    final List<String> webBrowserFlags = featureFlags.isWebEnabled
+        ? stringsArg(FlutterOptions.kWebBrowserFlag)
+        : const <String>[];
     if (buildInfo.mode.isRelease) {
       return DebuggingOptions.disabled(
         buildInfo,
@@ -216,7 +219,8 @@
         webUseSseForInjectedClient: featureFlags.isWebEnabled && stringArgDeprecated('web-server-debug-injected-client-protocol') == 'sse',
         webEnableExposeUrl: featureFlags.isWebEnabled && boolArgDeprecated('web-allow-expose-url'),
         webRunHeadless: featureFlags.isWebEnabled && boolArgDeprecated('web-run-headless'),
-        webBrowserDebugPort: browserDebugPort,
+        webBrowserDebugPort: webBrowserDebugPort,
+        webBrowserFlags: webBrowserFlags,
         enableImpeller: enableImpeller,
         uninstallFirst: uninstallFirst,
       );
@@ -253,7 +257,8 @@
         webUseSseForInjectedClient: featureFlags.isWebEnabled && stringArgDeprecated('web-server-debug-injected-client-protocol') == 'sse',
         webEnableExposeUrl: featureFlags.isWebEnabled && boolArgDeprecated('web-allow-expose-url'),
         webRunHeadless: featureFlags.isWebEnabled && boolArgDeprecated('web-run-headless'),
-        webBrowserDebugPort: browserDebugPort,
+        webBrowserDebugPort: webBrowserDebugPort,
+        webBrowserFlags: webBrowserFlags,
         webEnableExpressionEvaluation: featureFlags.isWebEnabled && boolArgDeprecated('web-enable-expression-evaluation'),
         webLaunchUrl: featureFlags.isWebEnabled ? stringArgDeprecated('web-launch-url') : null,
         vmserviceOutFile: stringArgDeprecated('vmservice-out-file'),
diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart
index 4fbcc64..9eeef33 100644
--- a/packages/flutter_tools/lib/src/device.dart
+++ b/packages/flutter_tools/lib/src/device.dart
@@ -785,6 +785,7 @@
     this.webUseSseForInjectedClient = true,
     this.webRunHeadless = false,
     this.webBrowserDebugPort,
+    this.webBrowserFlags = const <String>[],
     this.webEnableExpressionEvaluation = false,
     this.webLaunchUrl,
     this.vmserviceOutFile,
@@ -805,6 +806,7 @@
       this.webUseSseForInjectedClient = true,
       this.webRunHeadless = false,
       this.webBrowserDebugPort,
+      this.webBrowserFlags = const <String>[],
       this.webLaunchUrl,
       this.cacheSkSL = false,
       this.traceAllowlist,
@@ -871,6 +873,7 @@
     required this.webUseSseForInjectedClient,
     required this.webRunHeadless,
     required this.webBrowserDebugPort,
+    required this.webBrowserFlags,
     required this.webEnableExpressionEvaluation,
     required this.webLaunchUrl,
     required this.vmserviceOutFile,
@@ -930,6 +933,9 @@
   /// The port the browser should use for its debugging protocol.
   final int? webBrowserDebugPort;
 
+  /// Arbitrary browser flags.
+  final List<String> webBrowserFlags;
+
   /// Enable expression evaluation for web target.
   final bool webEnableExpressionEvaluation;
 
@@ -983,6 +989,7 @@
     'webUseSseForInjectedClient': webUseSseForInjectedClient,
     'webRunHeadless': webRunHeadless,
     'webBrowserDebugPort': webBrowserDebugPort,
+    'webBrowserFlags': webBrowserFlags,
     'webEnableExpressionEvaluation': webEnableExpressionEvaluation,
     'webLaunchUrl': webLaunchUrl,
     'vmserviceOutFile': vmserviceOutFile,
@@ -1027,6 +1034,7 @@
       webUseSseForInjectedClient: (json['webUseSseForInjectedClient'] as bool?)!,
       webRunHeadless: (json['webRunHeadless'] as bool?)!,
       webBrowserDebugPort: json['webBrowserDebugPort'] as int?,
+      webBrowserFlags: ((json['webBrowserFlags'] as List<dynamic>?)?.cast<String>())!,
       webEnableExpressionEvaluation: (json['webEnableExpressionEvaluation'] as bool?)!,
       webLaunchUrl: json['webLaunchUrl'] as String?,
       vmserviceOutFile: json['vmserviceOutFile'] as String?,
diff --git a/packages/flutter_tools/lib/src/drive/drive_service.dart b/packages/flutter_tools/lib/src/drive/drive_service.dart
index 756c6ec..91f0560 100644
--- a/packages/flutter_tools/lib/src/drive/drive_service.dart
+++ b/packages/flutter_tools/lib/src/drive/drive_service.dart
@@ -97,6 +97,7 @@
     String? browserName,
     bool? androidEmulator,
     int? driverPort,
+    List<String> webBrowserFlags,
     List<String>? browserDimension,
     String? profileMemory,
   });
@@ -254,6 +255,7 @@
     String? browserName,
     bool? androidEmulator,
     int? driverPort,
+    List<String> webBrowserFlags = const <String>[],
     List<String>? browserDimension,
     String? profileMemory,
   }) async {
diff --git a/packages/flutter_tools/lib/src/drive/web_driver_service.dart b/packages/flutter_tools/lib/src/drive/web_driver_service.dart
index e686a08..2df3709 100644
--- a/packages/flutter_tools/lib/src/drive/web_driver_service.dart
+++ b/packages/flutter_tools/lib/src/drive/web_driver_service.dart
@@ -136,6 +136,7 @@
     String? browserName,
     bool? androidEmulator,
     int? driverPort,
+    List<String> webBrowserFlags = const <String>[],
     List<String>? browserDimension,
     String? profileMemory,
   }) async {
@@ -144,7 +145,12 @@
     try {
       webDriver = await async_io.createDriver(
         uri: Uri.parse('http://localhost:$driverPort/'),
-        desired: getDesiredCapabilities(browser, headless, chromeBinary),
+        desired: getDesiredCapabilities(
+          browser,
+          headless,
+          webBrowserFlags: webBrowserFlags,
+          chromeBinary: chromeBinary,
+        ),
       );
     } on SocketException catch (error) {
       _logger.printTrace('$error');
@@ -234,10 +240,15 @@
   safari,
 }
 
-/// Returns desired capabilities for given [browser], [headless] and
-/// [chromeBinary].
+/// Returns desired capabilities for given [browser], [headless], [chromeBinary]
+/// and [webBrowserFlags].
 @visibleForTesting
-Map<String, dynamic> getDesiredCapabilities(Browser browser, bool? headless, [String? chromeBinary]) {
+Map<String, dynamic> getDesiredCapabilities(
+  Browser browser,
+  bool? headless, {
+  List<String> webBrowserFlags = const <String>[],
+  String? chromeBinary,
+}) {
   switch (browser) {
     case Browser.chrome:
       return <String, dynamic>{
@@ -262,6 +273,7 @@
             '--no-sandbox',
             '--no-first-run',
             if (headless!) '--headless',
+            ...webBrowserFlags,
           ],
           'perfLoggingPrefs': <String, String>{
             'traceCategories':
@@ -278,6 +290,7 @@
         'moz:firefoxOptions' : <String, dynamic>{
           'args': <String>[
             if (headless!) '-headless',
+            ...webBrowserFlags,
           ],
           'prefs': <String, dynamic>{
             'dom.file.createInChild': true,
@@ -313,7 +326,10 @@
         'platformName': 'android',
         'goog:chromeOptions': <String, dynamic>{
           'androidPackage': 'com.android.chrome',
-          'args': <String>['--disable-fullscreen'],
+          'args': <String>[
+            '--disable-fullscreen',
+            ...webBrowserFlags,
+          ],
         },
       };
   }
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart
index f48d084..c2f8d59 100644
--- a/packages/flutter_tools/lib/src/runner/flutter_command.dart
+++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -119,6 +119,7 @@
   static const String kAssumeInitializeFromDillUpToDate = 'assume-initialize-from-dill-up-to-date';
   static const String kFatalWarnings = 'fatal-warnings';
   static const String kUseApplicationBinary = 'use-application-binary';
+  static const String kWebBrowserFlag = 'web-browser-flag';
 }
 
 /// flutter command categories for usage.
@@ -270,6 +271,15 @@
       help: 'The URL to provide to the browser. Defaults to an HTTP URL with the host '
           'name of "--web-hostname", the port of "--web-port", and the path set to "/".',
     );
+    argParser.addMultiOption(
+      FlutterOptions.kWebBrowserFlag,
+      help: 'Additional flag to pass to a browser instance at startup.\n'
+          'Chrome: https://www.chromium.org/developers/how-tos/run-chromium-with-flags/\n'
+          'Firefox: https://wiki.mozilla.org/Firefox/CommandLineOptions\n'
+          'Multiple flags can be passed by repeating "--${FlutterOptions.kWebBrowserFlag}" multiple times.',
+      valueHelp: '--foo=bar',
+      hide: !verboseHelp,
+    );
   }
 
   void usesTargetOption() {
diff --git a/packages/flutter_tools/lib/src/test/flutter_web_platform.dart b/packages/flutter_tools/lib/src/test/flutter_web_platform.dart
index fc20e51..587a2c5 100644
--- a/packages/flutter_tools/lib/src/test/flutter_web_platform.dart
+++ b/packages/flutter_tools/lib/src/test/flutter_web_platform.dart
@@ -659,6 +659,8 @@
   ///
   /// The browser will start in headless mode if [headless] is true.
   ///
+  /// Add arbitrary browser flags via [webBrowserFlags].
+  ///
   /// The [settings] indicate how to invoke this browser's executable.
   ///
   /// Returns the browser manager, or throws an [ApplicationException] if a
@@ -670,8 +672,13 @@
     Future<WebSocketChannel> future, {
     bool debug = false,
     bool headless = true,
+    List<String> webBrowserFlags = const <String>[],
   }) async {
-    final Chromium chrome = await chromiumLauncher.launch(url.toString(), headless: headless);
+    final Chromium chrome = await chromiumLauncher.launch(
+      url.toString(),
+      headless: headless,
+      webBrowserFlags: webBrowserFlags,
+    );
     final Completer<BrowserManager> completer = Completer<BrowserManager>();
 
     unawaited(chrome.onExit.then((int? browserExitCode) {
diff --git a/packages/flutter_tools/lib/src/web/chrome.dart b/packages/flutter_tools/lib/src/web/chrome.dart
index 0984d09..43a1e2f 100644
--- a/packages/flutter_tools/lib/src/web/chrome.dart
+++ b/packages/flutter_tools/lib/src/web/chrome.dart
@@ -164,11 +164,14 @@
   /// port is picked automatically.
   ///
   /// [skipCheck] does not attempt to make a devtools connection before returning.
+  ///
+  /// [webBrowserFlags] add arbitrary browser flags.
   Future<Chromium> launch(String url, {
     bool headless = false,
     int? debugPort,
     bool skipCheck = false,
     Directory? cacheDir,
+    List<String> webBrowserFlags = const <String>[],
   }) async {
     if (currentCompleter.isCompleted) {
       throwToolExit('Only one instance of chrome can be started.');
@@ -215,6 +218,7 @@
           '--no-sandbox',
           '--window-size=2400,1800',
         ],
+      ...webBrowserFlags,
       url,
     ];
 
diff --git a/packages/flutter_tools/lib/src/web/web_device.dart b/packages/flutter_tools/lib/src/web/web_device.dart
index 902ab99..1482686 100644
--- a/packages/flutter_tools/lib/src/web/web_device.dart
+++ b/packages/flutter_tools/lib/src/web/web_device.dart
@@ -149,6 +149,7 @@
             .childDirectory('chrome-device'),
         headless: debuggingOptions.webRunHeadless,
         debugPort: debuggingOptions.webBrowserDebugPort,
+        webBrowserFlags: debuggingOptions.webBrowserFlags,
       );
     }
     _logger.sendEvent('app.webLaunchUrl', <String, Object>{'url': url, 'launched': launchChrome});
diff --git a/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart
index 1da0451..529e76c 100644
--- a/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart
+++ b/packages/flutter_tools/test/commands.shard/hermetic/drive_test.dart
@@ -354,6 +354,7 @@
       String browserName,
       bool androidEmulator,
       int driverPort,
+      List<String> webBrowserFlags,
       List<String> browserDimension,
       String profileMemory,
     }) async => 1;
diff --git a/packages/flutter_tools/test/general.shard/drive/web_driver_service_test.dart b/packages/flutter_tools/test/general.shard/drive/web_driver_service_test.dart
index dad555c..03a4773 100644
--- a/packages/flutter_tools/test/general.shard/drive/web_driver_service_test.dart
+++ b/packages/flutter_tools/test/general.shard/drive/web_driver_service_test.dart
@@ -24,6 +24,18 @@
 import '../../src/common.dart';
 import '../../src/context.dart';
 
+const List<String> kChromeArgs = <String>[
+  '--bwsi',
+  '--disable-background-timer-throttling',
+  '--disable-default-apps',
+  '--disable-extensions',
+  '--disable-popup-blocking',
+  '--disable-translate',
+  '--no-default-browser-check',
+  '--no-sandbox',
+  '--no-first-run',
+];
+
 void main() {
   testWithoutContext('getDesiredCapabilities Chrome with headless on', () {
     final Map<String, dynamic> expected = <String, dynamic>{
@@ -36,15 +48,7 @@
       'chromeOptions': <String, dynamic>{
         'w3c': false,
         'args': <String>[
-          '--bwsi',
-          '--disable-background-timer-throttling',
-          '--disable-default-apps',
-          '--disable-extensions',
-          '--disable-popup-blocking',
-          '--disable-translate',
-          '--no-default-browser-check',
-          '--no-sandbox',
-          '--no-first-run',
+          ...kChromeArgs,
           '--headless',
         ],
         'perfLoggingPrefs': <String, String>{
@@ -71,17 +75,7 @@
       'chromeOptions': <String, dynamic>{
         'binary': chromeBinary,
         'w3c': false,
-        'args': <String>[
-          '--bwsi',
-          '--disable-background-timer-throttling',
-          '--disable-default-apps',
-          '--disable-extensions',
-          '--disable-popup-blocking',
-          '--disable-translate',
-          '--no-default-browser-check',
-          '--no-sandbox',
-          '--no-first-run',
-        ],
+        'args': kChromeArgs,
         'perfLoggingPrefs': <String, String>{
           'traceCategories':
           'devtools.timeline,'
@@ -91,10 +85,43 @@
       },
     };
 
-    expect(getDesiredCapabilities(Browser.chrome, false, chromeBinary), expected);
+    expect(getDesiredCapabilities(Browser.chrome, false, chromeBinary: chromeBinary), expected);
 
   });
 
+  testWithoutContext('getDesiredCapabilities Chrome with browser flags', () {
+    const List<String> webBrowserFlags = <String>[
+      '--autoplay-policy=no-user-gesture-required',
+      '--incognito',
+      '--auto-select-desktop-capture-source="Entire screen"',
+    ];
+    final Map<String, dynamic> expected = <String, dynamic>{
+      'acceptInsecureCerts': true,
+      'browserName': 'chrome',
+      'goog:loggingPrefs': <String, String>{
+        sync_io.LogType.browser: 'INFO',
+        sync_io.LogType.performance: 'ALL',
+      },
+      'chromeOptions': <String, dynamic>{
+        'w3c': false,
+        'args': <String>[
+          ...kChromeArgs,
+          '--autoplay-policy=no-user-gesture-required',
+          '--incognito',
+          '--auto-select-desktop-capture-source="Entire screen"',
+        ],
+        'perfLoggingPrefs': <String, String>{
+          'traceCategories':
+          'devtools.timeline,'
+              'v8,blink.console,benchmark,blink,'
+              'blink.user_timing',
+        },
+      },
+    };
+
+    expect(getDesiredCapabilities(Browser.chrome, false, webBrowserFlags: webBrowserFlags), expected);
+  });
+
   testWithoutContext('getDesiredCapabilities Firefox with headless on', () {
     final Map<String, dynamic> expected = <String, dynamic>{
       'acceptInsecureCerts': true,
@@ -141,6 +168,36 @@
     expect(getDesiredCapabilities(Browser.firefox, false), expected);
   });
 
+  testWithoutContext('getDesiredCapabilities Firefox with browser flags', () {
+    const List<String> webBrowserFlags = <String>[
+      '-url=https://example.com',
+      '-private',
+    ];
+    final Map<String, dynamic> expected = <String, dynamic>{
+      'acceptInsecureCerts': true,
+      'browserName': 'firefox',
+      'moz:firefoxOptions' : <String, dynamic>{
+        'args': <String>[
+          '-url=https://example.com',
+          '-private',
+        ],
+        'prefs': <String, dynamic>{
+          'dom.file.createInChild': true,
+          'dom.timeout.background_throttling_max_budget': -1,
+          'media.autoplay.default': 0,
+          'media.gmp-manager.url': '',
+          'media.gmp-provider.enabled': false,
+          'network.captive-portal-service.enabled': false,
+          'security.insecure_field_warning.contextual.enabled': false,
+          'test.currentTimeOffsetSeconds': 11491200,
+        },
+        'log': <String, String>{'level': 'trace'},
+      },
+    };
+
+    expect(getDesiredCapabilities(Browser.firefox, false, webBrowserFlags: webBrowserFlags), expected);
+  });
+
   testWithoutContext('getDesiredCapabilities Edge', () {
     final Map<String, dynamic> expected = <String, dynamic>{
       'acceptInsecureCerts': true,
@@ -169,16 +226,24 @@
   });
 
   testWithoutContext('getDesiredCapabilities android chrome', () {
+    const List<String> webBrowserFlags = <String>[
+      '--autoplay-policy=no-user-gesture-required',
+      '--incognito',
+    ];
     final Map<String, dynamic> expected = <String, dynamic>{
       'browserName': 'chrome',
       'platformName': 'android',
       'goog:chromeOptions': <String, dynamic>{
         'androidPackage': 'com.android.chrome',
-        'args': <String>['--disable-fullscreen'],
+        'args': <String>[
+          '--disable-fullscreen',
+          '--autoplay-policy=no-user-gesture-required',
+          '--incognito',
+        ],
       },
     };
 
-    expect(getDesiredCapabilities(Browser.androidChrome, false), expected);
+    expect(getDesiredCapabilities(Browser.androidChrome, false, webBrowserFlags: webBrowserFlags), expected);
   });
 
   testUsingContext('WebDriverService starts and stops an app', () async {
diff --git a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart
index 00f08a2..6eccf1a 100644
--- a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart
+++ b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart
@@ -1307,7 +1307,14 @@
   bool get hasChromeInstance => _hasInstance;
 
   @override
-  Future<Chromium> launch(String url, {bool headless = false, int debugPort, bool skipCheck = false, Directory cacheDir}) async {
+  Future<Chromium> launch(
+    String url, {
+    bool headless = false,
+    int debugPort,
+    bool skipCheck = false,
+    Directory cacheDir,
+    List<String> webBrowserFlags = const <String>[],
+  }) async {
     return currentCompleter.future;
   }
 
diff --git a/packages/flutter_tools/test/general.shard/web/devices_test.dart b/packages/flutter_tools/test/general.shard/web/devices_test.dart
index 17c5b3a..270da31 100644
--- a/packages/flutter_tools/test/general.shard/web/devices_test.dart
+++ b/packages/flutter_tools/test/general.shard/web/devices_test.dart
@@ -389,7 +389,14 @@
   bool get hasChromeInstance => _hasInstance;
 
   @override
-  Future<Chromium> launch(String url, {bool headless = false, int? debugPort, bool skipCheck = false, Directory? cacheDir}) async {
+  Future<Chromium> launch(
+    String url, {
+    bool headless = false,
+    int? debugPort,
+    bool skipCheck = false,
+    Directory? cacheDir,
+    List<String> webBrowserFlags = const <String>[],
+  }) async {
     return currentCompleter.future;
   }
 
diff --git a/packages/flutter_tools/test/web.shard/chrome_test.dart b/packages/flutter_tools/test/web.shard/chrome_test.dart
index 9cd9cb3..378a699 100644
--- a/packages/flutter_tools/test/web.shard/chrome_test.dart
+++ b/packages/flutter_tools/test/web.shard/chrome_test.dart
@@ -328,6 +328,32 @@
     );
   });
 
+  testWithoutContext('can launch chrome with arbitrary flags', () async {
+    processManager.addCommand(const FakeCommand(
+      command: <String>[
+        'example_chrome',
+        '--user-data-dir=/.tmp_rand0/flutter_tools_chrome_device.rand0',
+        '--remote-debugging-port=12345',
+        ...kChromeArgs,
+        '--autoplay-policy=no-user-gesture-required',
+        '--incognito',
+        '--auto-select-desktop-capture-source="Entire screen"',
+        'example_url',
+      ],
+      stderr: kDevtoolsStderr,
+    ));
+
+    await expectReturnsNormallyLater(chromeLauncher.launch(
+      'example_url',
+      skipCheck: true,
+      webBrowserFlags: <String>[
+        '--autoplay-policy=no-user-gesture-required',
+        '--incognito',
+        '--auto-select-desktop-capture-source="Entire screen"',
+      ],
+    ));
+  });
+
   testWithoutContext('can launch chrome headless', () async {
     processManager.addCommand(const FakeCommand(
       command: <String>[