diff --git a/tests/DuskTestCase.php b/tests/DuskTestCase.php index 88062f9f..7b1f1498 100644 --- a/tests/DuskTestCase.php +++ b/tests/DuskTestCase.php @@ -12,16 +12,26 @@ abstract class DuskTestCase extends BaseTestCase { use CreatesApplication; + /** + * Keep the local Laravel server process alive for the Dusk run. + * + * @var resource|null + */ + private static $laravelServerProcess = null; + /** * Prepare for Dusk test execution. */ #[BeforeClass] public static function prepare(): void { + static::configureDuskBaseUrl(); + static::ensureLaravelServerIsReachable(); + if (! static::runningInSail()) { static::ensureChromeDriverBinaryAlias(); static::startChromeDriver(['--port=9515']); - static::waitForChromeDriver(); + static::waitForTcpPort('127.0.0.1', 9515, 8.0, 'ChromeDriver'); } } @@ -39,10 +49,11 @@ abstract class DuskTestCase extends BaseTestCase '--disable-search-engine-choice-screen', '--ignore-certificate-errors', '--allow-insecure-localhost', + '--disable-features=Translate,BackForwardCache,AcceptCHFrame', ]); return RemoteWebDriver::create( - $_ENV['DUSK_DRIVER_URL'] ?? 'http://localhost:9515', + $_ENV['DUSK_DRIVER_URL'] ?? 'http://127.0.0.1:9515', DesiredCapabilities::chrome()->setCapability( ChromeOptions::CAPABILITY, $options @@ -50,6 +61,108 @@ abstract class DuskTestCase extends BaseTestCase ); } + private static function configureDuskBaseUrl(): void + { + $url = rtrim((string) ($_ENV['DUSK_APP_URL'] ?? getenv('DUSK_APP_URL') ?: 'http://127.0.0.1:8000'), '/'); + + $_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'] ?? '127.0.0.1'; + $port = (int) ($parts['port'] ?? ($scheme === 'https' ? 443 : 80)); + + if (static::canConnect($host, $port)) { + return; + } + + // Dusk should be deterministic in local/CI runs. If no reachable app + // server is already running, start Laravel's built-in server for the + // browser suite instead of failing every test with ERR_CONNECTION_REFUSED. + if (! in_array($host, ['127.0.0.1', 'localhost'], true)) { + $fallbackUrl = 'http://127.0.0.1:8000'; + $_ENV['APP_URL'] = $fallbackUrl; + $_SERVER['APP_URL'] = $fallbackUrl; + putenv('APP_URL='.$fallbackUrl); + + if (property_exists(BaseTestCase::class, 'baseUrl')) { + static::$baseUrl = $fallbackUrl; + } + + $host = '127.0.0.1'; + $port = 8000; + } + + if (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); + } + + $php = escapeshellarg(PHP_BINARY); + $artisan = escapeshellarg($root.'/artisan'); + $hostArg = escapeshellarg($host); + $portArg = (string) $port; + $log = escapeshellarg($logPath); + + $command = "{$php} {$artisan} serve --host={$hostArg} --port={$portArg} > {$log} 2>&1"; + + self::$laravelServerProcess = proc_open($command, [], $pipes, $root); + } + + 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. * @@ -95,24 +208,4 @@ abstract class DuskTestCase extends BaseTestCase @chmod($expected, 0755); } } - - /** - * Wait until ChromeDriver is listening before the first browser session starts. - */ - private static function waitForChromeDriver(): void - { - $deadline = microtime(true) + 5.0; - - do { - $connection = @fsockopen('127.0.0.1', 9515); - - if (is_resource($connection)) { - fclose($connection); - - return; - } - - usleep(100_000); - } while (microtime(true) < $deadline); - } }