Launch browsers with redirecting files (#2703)

The test runner creates a secret key used for communication with the
browsers to prevent third party processes from connecting as if they
were running test suites, but this key is current leaking through the
command arguments when starting the browser process. We are already
using temp directories which should prevent external reads and the
safari browser launcher already used this pattern. Extend the
`redirect.html` pattern to chromium and firefox browsers.
diff --git a/pkgs/test/CHANGELOG.md b/pkgs/test/CHANGELOG.md
index fa768d3..8df1963 100644
--- a/pkgs/test/CHANGELOG.md
+++ b/pkgs/test/CHANGELOG.md
@@ -9,6 +9,8 @@
   in `GithubReporter`.
 * Only include VM debugger in output when using interactive debugging, suppress
   it when only using coverage.
+* Use a redirect html file for browser tests to avoid leaking websocket details
+  through process starting arguments.
 
 ## 1.31.2
 
diff --git a/pkgs/test/lib/src/runner/browser/chrome.dart b/pkgs/test/lib/src/runner/browser/chrome.dart
index f23d553..43fdcb3 100644
--- a/pkgs/test/lib/src/runner/browser/chrome.dart
+++ b/pkgs/test/lib/src/runner/browser/chrome.dart
@@ -170,8 +170,8 @@
     var tabs = await chromeConnection.getTabs();
     tab = tabs.firstWhereOrNull((tab) => tab.url == url.toString());
     if (tab == null) {
-      await Future<void>.delayed(const Duration(milliseconds: 100));
-      if (attempt > 5) {
+      await Future<void>.delayed(const Duration(milliseconds: 200));
+      if (attempt > 20) {
         throw StateError('Could not connect to test tab with url: $url');
       }
     }
diff --git a/pkgs/test/lib/src/runner/browser/chromium.dart b/pkgs/test/lib/src/runner/browser/chromium.dart
index 4ac6835..f3d1244 100644
--- a/pkgs/test/lib/src/runner/browser/chromium.dart
+++ b/pkgs/test/lib/src/runner/browser/chromium.dart
@@ -3,8 +3,10 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:async';
+import 'dart:convert';
 import 'dart:io';
 
+import 'package:path/path.dart' as p;
 import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
 import 'package:test_core/src/runner/configuration.dart'; // ignore: implementation_imports
 import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports
@@ -29,9 +31,14 @@
     settings ??= defaultSettings[runtime];
 
     var dir = createTempDir();
+    var redirect = p.join(dir, 'redirect.html');
+    File(redirect).writeAsStringSync(
+      '<script>location = ${jsonEncode(url.toString())}</script>',
+    );
+
     var args = [
       '--user-data-dir=$dir',
-      url.toString(),
+      redirect,
       '--enable-logging=stderr',
       '--v=0',
       '--disable-extensions',
diff --git a/pkgs/test/lib/src/runner/browser/firefox.dart b/pkgs/test/lib/src/runner/browser/firefox.dart
index e67fb67..384edc2 100644
--- a/pkgs/test/lib/src/runner/browser/firefox.dart
+++ b/pkgs/test/lib/src/runner/browser/firefox.dart
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'dart:async';
+import 'dart:convert';
 import 'dart:io';
 
 import 'package:path/path.dart' as p;
@@ -44,9 +45,14 @@
     var dir = createTempDir();
     File(p.join(dir, 'prefs.js')).writeAsStringSync(_preferences);
 
+    var redirect = p.join(dir, 'redirect.html');
+    File(redirect).writeAsStringSync(
+      '<script>location = ${jsonEncode(url.toString())}</script>',
+    );
+
     var process = await Process.start(
       settings.executable,
-      ['--profile', dir, url.toString(), '--no-remote', ...settings.arguments],
+      ['--profile', dir, redirect, '--no-remote', ...settings.arguments],
       environment: {'MOZ_CRASHREPORTER_DISABLE': '1', 'MOZ_AUTOMATION': '1'},
     );
 
diff --git a/pkgs/test/test/runner/browser/chrome_test.dart b/pkgs/test/test/runner/browser/chrome_test.dart
index 8663d2e..e2142a8 100644
--- a/pkgs/test/test/runner/browser/chrome_test.dart
+++ b/pkgs/test/test/runner/browser/chrome_test.dart
@@ -6,6 +6,9 @@
 @Tags(['chrome'])
 library;
 
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
 import 'package:test/src/runner/browser/chrome.dart';
 import 'package:test/src/runner/executable_settings.dart';
 import 'package:test/test.dart';
@@ -110,4 +113,30 @@
     expect(test.stdout, emitsThrough(contains('Failed to run Chrome:')));
     await test.shouldExit(1);
   });
+
+  test('does not pass target URL directly in command line arguments', () async {
+    var targetUrl = Uri.parse('http://localhost:12345/secret_token_12345');
+    var argsFile = p.join(d.sandbox, 'args.txt');
+    var scriptFile = p.join(d.sandbox, 'fake_chrome.sh');
+    await d.file('fake_chrome.sh', '''
+#!/bin/sh
+echo "\$@" > "$argsFile"
+''').create();
+    await Process.run('chmod', ['+x', scriptFile]);
+
+    var chrome = Chrome(
+      targetUrl,
+      configuration(),
+      settings: ExecutableSettings(
+        linuxExecutable: scriptFile,
+        macOSExecutable: scriptFile,
+        windowsExecutable: scriptFile,
+      ),
+    );
+    await chrome.onExit.catchError((_) {});
+
+    var argsText = await File(argsFile).readAsString();
+    expect(argsText, isNot(contains('secret_token_12345')));
+    expect(argsText, contains('redirect.html'));
+  }, testOn: '!windows');
 }
diff --git a/pkgs/test/test/runner/browser/firefox_test.dart b/pkgs/test/test/runner/browser/firefox_test.dart
index cf89f9e..581502a 100644
--- a/pkgs/test/test/runner/browser/firefox_test.dart
+++ b/pkgs/test/test/runner/browser/firefox_test.dart
@@ -5,6 +5,9 @@
 @Tags(['firefox'])
 library;
 
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
 import 'package:test/src/runner/browser/firefox.dart';
 import 'package:test/src/runner/executable_settings.dart';
 import 'package:test/test.dart';
@@ -123,4 +126,29 @@
     expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
     await test.shouldExit(0);
   });
+
+  test('does not pass target URL directly in command line arguments', () async {
+    var targetUrl = Uri.parse('http://localhost:12345/secret_token_12345');
+    var argsFile = p.join(d.sandbox, 'args.txt');
+    var scriptFile = p.join(d.sandbox, 'fake_firefox.sh');
+    await d.file('fake_firefox.sh', '''
+#!/bin/sh
+echo "\$@" > "$argsFile"
+''').create();
+    await Process.run('chmod', ['+x', scriptFile]);
+
+    var firefox = Firefox(
+      targetUrl,
+      settings: ExecutableSettings(
+        linuxExecutable: scriptFile,
+        macOSExecutable: scriptFile,
+        windowsExecutable: scriptFile,
+      ),
+    );
+    await firefox.onExit.catchError((_) {});
+
+    var argsText = await File(argsFile).readAsString();
+    expect(argsText, isNot(contains('secret_token_12345')));
+    expect(argsText, contains('redirect.html'));
+  }, testOn: '!windows');
 }