addArguments([ '--disable-gpu', '--headless=new', '--window-size=1920,1080', '--no-sandbox', '--disable-dev-shm-usage', '--disable-search-engine-choice-screen', '--ignore-certificate-errors', '--allow-insecure-localhost', '--disable-features=Translate,BackForwardCache,AcceptCHFrame', ]); return RemoteWebDriver::create( $_ENV['DUSK_DRIVER_URL'] ?? 'http://127.0.0.1:9515', DesiredCapabilities::chrome()->setCapability( ChromeOptions::CAPABILITY, $options, ), ); } private static function configureDuskBaseUrl(): void { // Do not trust an already-listening local server. In local Dusk runs it // may be Octane, Valet, or a stale `artisan serve` process using a // different database. Pick a free port for this PHPUnit run and start a // matching Laravel server below, unless explicitly told to use an // external server. $useExternalServer = filter_var( $_ENV['DUSK_USE_EXTERNAL_SERVER'] ?? getenv('DUSK_USE_EXTERNAL_SERVER') ?: false, FILTER_VALIDATE_BOOL, ); if ($useExternalServer) { $url = rtrim((string) ($_ENV['DUSK_APP_URL'] ?? getenv('DUSK_APP_URL') ?: 'http://127.0.0.1:8000'), '/'); } else { $url = 'http://127.0.0.1:' . static::availablePort(8011, 8099); } $_ENV['DUSK_APP_URL'] = $url; $_SERVER['DUSK_APP_URL'] = $url; putenv('DUSK_APP_URL=' . $url); $_ENV['APP_URL'] = $url; $_SERVER['APP_URL'] = $url; putenv('APP_URL=' . $url); if (property_exists(BaseTestCase::class, 'baseUrl')) { static::$baseUrl = $url; } } private static function ensureLaravelServerIsReachable(): void { $url = rtrim((string) ($_ENV['APP_URL'] ?? 'http://127.0.0.1:8000'), '/'); $parts = parse_url($url); $scheme = $parts['scheme'] ?? 'http'; $host = $parts['host'] ?? self::LOOPBACK; $port = (int) ($parts['port'] ?? ($scheme === 'https' ? 443 : 80)); $useExternalServer = filter_var( $_ENV['DUSK_USE_EXTERNAL_SERVER'] ?? getenv('DUSK_USE_EXTERNAL_SERVER') ?: false, FILTER_VALIDATE_BOOL, ); if ($useExternalServer && static::canConnect($host, $port)) { return; } static::startLaravelServer($host, $port); static::waitForTcpPort($host, $port, 15.0, 'Laravel Dusk application server'); } private static function startLaravelServer(string $host, int $port): void { $root = dirname(__DIR__); $logPath = $root . '/storage/logs/dusk-server.log'; if (! is_dir(dirname($logPath))) { @mkdir(dirname($logPath), 0775, true); } $command = sprintf( '%s %s serve --host=%s --port=%d', escapeshellarg(PHP_BINARY), escapeshellarg($root . '/artisan'), escapeshellarg($host), $port, ); $descriptors = [ 0 => ['pipe', 'r'], 1 => ['file', $logPath, 'a'], 2 => ['file', $logPath, 'a'], ]; $env = static::scalarEnvironment([ ...getenv(), ...$_SERVER, ...$_ENV, 'APP_ENV' => $_ENV['APP_ENV'] ?? 'dusk', 'APP_URL' => $_ENV['APP_URL'] ?? "http://{$host}:{$port}", 'DUSK_APP_URL' => $_ENV['DUSK_APP_URL'] ?? "http://{$host}:{$port}", 'CACHE_STORE' => $_ENV['CACHE_STORE'] ?? 'array', 'QUEUE_CONNECTION' => $_ENV['QUEUE_CONNECTION'] ?? 'sync', 'MAIL_MAILER' => $_ENV['MAIL_MAILER'] ?? 'array', ]); self::$laravelServerProcess = proc_open($command, $descriptors, $pipes, $root, $env); if (isset($pipes[0]) && is_resource($pipes[0])) { fclose($pipes[0]); } if (! is_resource(self::$laravelServerProcess)) { throw new \RuntimeException('Unable to start Laravel Dusk application server. Check storage/logs/dusk-server.log.'); } } /** * @param array $environment * * @return array */ private static function scalarEnvironment(array $environment): array { $filtered = []; foreach ($environment as $key => $value) { if (! is_string($key) || $key === '') { continue; } if (is_bool($value)) { $filtered[$key] = $value ? 'true' : 'false'; continue; } if (is_scalar($value) || $value === null) { $filtered[$key] = (string) $value; } } return $filtered; } private static function availablePort(int $from, int $to): int { for ($port = $from; $port <= $to; $port++) { if (! static::canConnect(self::LOOPBACK, $port)) { return $port; } } throw new \RuntimeException("No free TCP port available between {$from} and {$to} for Dusk."); } private static function canConnect(string $host, int $port): bool { $connection = @fsockopen($host, $port, $errno, $errstr, 0.25); if (! is_resource($connection)) { return false; } fclose($connection); return true; } private static function waitForTcpPort(string $host, int $port, float $seconds, string $service): void { $deadline = microtime(true) + $seconds; do { if (static::canConnect($host, $port)) { return; } usleep(100_000); } while (microtime(true) < $deadline); $log = dirname(__DIR__) . '/storage/logs/dusk-server.log'; $hint = is_file($log) ? " Check {$log}." : ''; throw new \RuntimeException("{$service} is not reachable at {$host}:{$port}.{$hint}"); } /** * Dusk may install a differently named macOS Apple Silicon binary. * * This method intentionally does not use base_path(), app(), or Artisan because * PHPUnit calls BeforeClass before Laravel has booted the application container. */ private static function ensureChromeDriverBinaryAlias(): void { $binDirectory = dirname(__DIR__) . '/vendor/laravel/dusk/bin'; $expected = $binDirectory . '/chromedriver-mac-arm'; if (is_file($expected)) { @chmod($expected, 0750); return; } if (! is_dir($binDirectory)) { @mkdir($binDirectory, 0750, true); } $candidates = array_values(array_filter( glob($binDirectory . '/chromedriver*') ?: [], static function (string $path) use ($expected): bool { return is_file($path) && $path !== $expected && ! str_ends_with($path, '.zip') && ! str_ends_with($path, '.txt'); }, )); if ($candidates === []) { return; } copy($candidates[0], $expected); @chmod($expected, 0750); } }