212 lines
6.5 KiB
PHP
212 lines
6.5 KiB
PHP
<?php
|
|
|
|
namespace Tests;
|
|
|
|
use Facebook\WebDriver\Chrome\ChromeOptions;
|
|
use Facebook\WebDriver\Remote\DesiredCapabilities;
|
|
use Facebook\WebDriver\Remote\RemoteWebDriver;
|
|
use Laravel\Dusk\TestCase as BaseTestCase;
|
|
use PHPUnit\Framework\Attributes\BeforeClass;
|
|
|
|
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::waitForTcpPort('127.0.0.1', 9515, 8.0, 'ChromeDriver');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create the RemoteWebDriver instance.
|
|
*/
|
|
protected function driver(): RemoteWebDriver
|
|
{
|
|
$options = (new ChromeOptions())->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
|
|
{
|
|
$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.
|
|
*
|
|
* 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, 0755);
|
|
|
|
return;
|
|
}
|
|
|
|
if (! is_dir($binDirectory)) {
|
|
@mkdir($binDirectory, 0755, 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');
|
|
}
|
|
));
|
|
|
|
usort($candidates, static function (string $left, string $right): int {
|
|
$score = static fn (string $path): int =>
|
|
str_contains($path, 'mac-arm64') ? 0
|
|
: (str_contains($path, 'mac-arm') ? 1
|
|
: (str_contains($path, 'mac') ? 2 : 3));
|
|
|
|
return $score($left) <=> $score($right);
|
|
});
|
|
|
|
if ($candidates !== []) {
|
|
@copy($candidates[0], $expected);
|
|
@chmod($expected, 0755);
|
|
}
|
|
}
|
|
}
|