diff --git a/Modules/Company/changelog.yaml b/Modules/Company/changelog.yaml
new file mode 100644
index 00000000..f71301ee
--- /dev/null
+++ b/Modules/Company/changelog.yaml
@@ -0,0 +1,8 @@
+latest: 2026.07.08
+
+releases:
+ 2026.07.08:
+ date: 2026-07-08
+ changes:
+ - Hardened platform company creation with unique slug generation for repeated browser-test runs.
+ - Added deterministic Dusk synchronisation for the inline company creation form.
diff --git a/phpunit.dusk.xml b/phpunit.dusk.xml
index 4ffc7922..a27a8878 100644
--- a/phpunit.dusk.xml
+++ b/phpunit.dusk.xml
@@ -24,5 +24,7 @@
+
+
diff --git a/tests/Browser/PlatformAdministrationBrowserTest.php b/tests/Browser/PlatformAdministrationBrowserTest.php
index 0512f5fa..de7c5114 100644
--- a/tests/Browser/PlatformAdministrationBrowserTest.php
+++ b/tests/Browser/PlatformAdministrationBrowserTest.php
@@ -45,7 +45,8 @@ class PlatformAdministrationBrowserTest extends DuskTestCase
->type('billing_email', $billingEmail)
->select('status', 'active')
->press('Create')
- ->pause(500);
+ ->waitForText('Company created.', 10)
+ ->waitForText($companyName, 10);
});
$this->assertDatabaseHas('companies', [
diff --git a/tests/DuskTestCase.php b/tests/DuskTestCase.php
index dca98cea..29d15740 100644
--- a/tests/DuskTestCase.php
+++ b/tests/DuskTestCase.php
@@ -63,7 +63,25 @@ 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'), '/');
+ // 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;
@@ -83,28 +101,12 @@ abstract class DuskTestCase extends BaseTestCase
$host = $parts['host'] ?? '127.0.0.1';
$port = (int) ($parts['port'] ?? ($scheme === 'https' ? 443 : 80));
- if (static::canConnect($host, $port)) {
- return;
- }
+ $useExternalServer = filter_var(
+ $_ENV['DUSK_USE_EXTERNAL_SERVER'] ?? getenv('DUSK_USE_EXTERNAL_SERVER') ?: false,
+ FILTER_VALIDATE_BOOL,
+ );
- // 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)) {
+ if ($useExternalServer && static::canConnect($host, $port)) {
return;
}
@@ -121,15 +123,79 @@ abstract class DuskTestCase extends BaseTestCase
@mkdir(dirname($logPath), 0775, true);
}
- $php = escapeshellarg(PHP_BINARY);
- $artisan = escapeshellarg($root . '/artisan');
- $hostArg = escapeshellarg($host);
- $portArg = (string) $port;
- $log = escapeshellarg($logPath);
+ $command = sprintf(
+ '%s %s serve --host=%s --port=%d',
+ escapeshellarg(PHP_BINARY),
+ escapeshellarg($root . '/artisan'),
+ escapeshellarg($host),
+ $port,
+ );
- $command = "{$php} {$artisan} serve --host={$hostArg} --port={$portArg} > {$log} 2>&1";
+ $descriptors = [
+ 0 => ['pipe', 'r'],
+ 1 => ['file', $logPath, 'a'],
+ 2 => ['file', $logPath, 'a'],
+ ];
- self::$laravelServerProcess = proc_open($command, [], $pipes, $root);
+ $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('127.0.0.1', $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
@@ -194,17 +260,11 @@ abstract class DuskTestCase extends BaseTestCase
},
));
- 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);
+ if ($candidates === []) {
+ return;
}
+
+ copy($candidates[0], $expected);
+ @chmod($expected, 0755);
}
}