Files
WMS/tests/DuskTestCase.php

274 lines
8.3 KiB
PHP
Executable File

<?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
{
private const LOOPBACK = '127.0.0.1';
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(self::LOOPBACK, 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
{
// 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<mixed> $environment
*
* @return array<string, string>
*/
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);
}
}