diff --git a/tests/Fixtures/html/forms.html b/tests/Fixtures/html/forms.html index 7dd8707..718cd39 100644 --- a/tests/Fixtures/html/forms.html +++ b/tests/Fixtures/html/forms.html @@ -1,7 +1,9 @@ + + Forms - Playwright PHP Tests

Form Tests

@@ -43,4 +45,4 @@

Form Tests

- \ No newline at end of file + diff --git a/tests/Fixtures/html/index.html b/tests/Fixtures/html/index.html index beb610b..79544bc 100644 --- a/tests/Fixtures/html/index.html +++ b/tests/Fixtures/html/index.html @@ -13,4 +13,4 @@

Test Index

Events - \ No newline at end of file + diff --git a/tests/Fixtures/server.php b/tests/Fixtures/server.php index e74556f..facfff0 100644 --- a/tests/Fixtures/server.php +++ b/tests/Fixtures/server.php @@ -24,7 +24,25 @@ // Serve static file if it exists if (file_exists($file) && is_file($file)) { - return false; // Let PHP's built-in server handle it + // Determine content type + $extension = strtolower(pathinfo($file, PATHINFO_EXTENSION)); + $contentTypes = [ + 'html' => 'text/html; charset=utf-8', + 'css' => 'text/css', + 'js' => 'application/javascript', + 'json' => 'application/json', + 'png' => 'image/png', + 'jpg' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'gif' => 'image/gif', + 'svg' => 'image/svg+xml', + ]; + + $contentType = $contentTypes[$extension] ?? 'application/octet-stream'; + header('Content-Type: '.$contentType); + readfile($file); + + return true; } // 404 for missing files @@ -34,3 +52,5 @@ echo '404 Not Found'; echo '

404 Not Found

File not found: '.htmlspecialchars($uri, \ENT_QUOTES, 'UTF-8').'

'; echo ''; + +return true; diff --git a/tests/Functional/FunctionalTestCase.php b/tests/Functional/FunctionalTestCase.php index a178eaa..4dd58c6 100644 --- a/tests/Functional/FunctionalTestCase.php +++ b/tests/Functional/FunctionalTestCase.php @@ -16,6 +16,7 @@ use PHPUnit\Framework\TestCase; use Playwright\Testing\PlaywrightTestCaseTrait; +use Symfony\Component\Process\Process; /** * Base class for functional tests using real Playwright browsers. @@ -26,12 +27,80 @@ abstract class FunctionalTestCase extends TestCase { use PlaywrightTestCaseTrait; + // Auto-managed fixture server for local runs (when TEST_SERVER_BASE_URL is not set) + protected static ?Process $fixtureServer = null; + protected static ?string $autoBaseUrl = null; + /** - * Start Playwright and launch browser once for all tests in the class. + * Start Playwright and (if needed) a local fixture server once for all tests in the class. */ public static function setUpBeforeClass(): void { parent::setUpBeforeClass(); + + // If CI or caller provides a server URL, do not auto-start + $providedBase = $_ENV['TEST_SERVER_BASE_URL'] + ?? (getenv('TEST_SERVER_BASE_URL') ?: null) + ?? $_ENV['FIXTURE_SERVER_BASE_URL'] + ?? (getenv('FIXTURE_SERVER_BASE_URL') ?: null); + + if (null === $providedBase) { + // Auto-start a local server serving tests/Fixtures via router + $host = '127.0.0.1'; + $port = 8888; + if (!self::isPortAvailable($host, $port)) { + $port = self::findAvailablePort($host) ?: $port; + } + + $fixturesDir = \realpath(__DIR__.'/../Fixtures'); + if (false === $fixturesDir) { + throw new \RuntimeException('Fixtures directory not found'); + } + $router = $fixturesDir.'/server.php'; + if (!\file_exists($router)) { + throw new \RuntimeException('Router script not found: '.$router); + } + + self::$fixtureServer = new Process([ + PHP_BINARY, + '-S', sprintf('%s:%d', $host, $port), + '-t', $fixturesDir, + $router, + ], $fixturesDir); + self::$fixtureServer->setTimeout(null); + self::$fixtureServer->setIdleTimeout(null); + self::$fixtureServer->start(); + + // Wait until ready (index.html should exist according to fixtures) + $deadline = time() + 10; // 10s max + $ready = false; + while (time() < $deadline) { + if (self::isServerReady($host, $port)) { + $ready = true; + break; + } + usleep(100_000); + } + + if (!$ready) { + $stderr = ''; + if (self::$fixtureServer?->isStarted()) { + $stderr = trim(self::$fixtureServer->getErrorOutput()); + } + self::$fixtureServer?->stop(); + throw new \RuntimeException('Fixture server failed to start'.('' !== $stderr ? ' - stderr: '.$stderr : '')); + } + + self::$autoBaseUrl = sprintf('http://%s:%d', $host, $port); + + // Ensure cleanup at process end + register_shutdown_function(static function (): void { + if (null !== self::$fixtureServer && self::$fixtureServer->isRunning()) { + self::$fixtureServer->stop(); + self::$fixtureServer = null; + } + }); + } } /** @@ -61,17 +130,31 @@ public static function tearDownAfterClass(): void { self::closeSharedPlaywright(); + // Stop auto-started server if any + if (null !== self::$fixtureServer && self::$fixtureServer->isRunning()) { + self::$fixtureServer->stop(); + self::$fixtureServer = null; + } + parent::tearDownAfterClass(); } /** * Get the base URL for test fixtures. * - * Override this method if you need a different test server URL. + * Respects TEST_SERVER_BASE_URL (or FIXTURE_SERVER_BASE_URL) env vars; defaults to http://127.0.0.1:8888. + * If auto server is started, returns that server's base URL. */ protected function getBaseUrl(): string { - return 'http://localhost:8888'; + $base = $_ENV['TEST_SERVER_BASE_URL'] + ?? (getenv('TEST_SERVER_BASE_URL') ?: null) + ?? $_ENV['FIXTURE_SERVER_BASE_URL'] + ?? (getenv('FIXTURE_SERVER_BASE_URL') ?: null) + ?? self::$autoBaseUrl + ?? 'http://127.0.0.1:8888'; + + return rtrim($base, '/'); } /** @@ -152,4 +235,37 @@ protected function assertElementHasText(string $selector, string $expectedText): \sprintf('Expected element "%s" to have text "%s", got "%s"', $selector, $expectedText, $actualText) ); } + + private static function isPortAvailable(string $host, int $port): bool + { + $conn = @fsockopen($host, $port, $errno, $errstr, 0.5); + if (is_resource($conn)) { + fclose($conn); + + return false; + } + + return true; + } + + private static function findAvailablePort(string $host): int + { + for ($i = 0; $i < 30; ++$i) { + $port = random_int(8000, 9000); + if (self::isPortAvailable($host, $port)) { + return $port; + } + } + + return 0; + } + + private static function isServerReady(string $host, int $port): bool + { + $url = sprintf('http://%s:%d/index.html', $host, $port); + $ctx = stream_context_create(['http' => ['timeout' => 0.5, 'ignore_errors' => true]]); + $body = @file_get_contents($url, false, $ctx); + + return false !== $body; + } } diff --git a/tests/Integration/Functional/BasicFunctionalTest.php b/tests/Integration/Functional/BasicFunctionalTest.php new file mode 100644 index 0000000..8990b50 --- /dev/null +++ b/tests/Integration/Functional/BasicFunctionalTest.php @@ -0,0 +1,74 @@ +page->goto($this->fixtureUrl('/index.html')); + + $heading = $this->page->locator('#heading')->textContent(); + $this->assertSame('Test Index', $heading); + + $content = $this->page->locator('#content')->textContent(); + $this->assertStringContainsString('Minimal test fixture', $content); + } + + #[Test] + public function itCanNavigateToFormsPage(): void + { + $this->page->goto($this->fixtureUrl('/index.html')); + + $this->page->click('a[href="/forms.html"]'); + $this->page->waitForURL('**/forms.html'); + + $this->assertStringContainsString('/forms.html', $this->page->url()); + } + + #[Test] + public function itReturns404ForNonExistentPage(): void + { + $response = $this->page->goto($this->fixtureUrl('/non-existent.html')); + + $this->assertSame(404, $response->status()); + $this->assertStringContainsString('404 Not Found', $this->page->content()); + } + + #[Test] + public function itDefaultsToIndexHtmlForRoot(): void + { + $this->page->goto($this->fixtureUrl('/')); + + $heading = $this->page->locator('#heading')->textContent(); + $this->assertSame('Test Index', $heading); + } +} diff --git a/tests/Support/FunctionalTestCase.php b/tests/Support/FunctionalTestCase.php new file mode 100644 index 0000000..364d3bf --- /dev/null +++ b/tests/Support/FunctionalTestCase.php @@ -0,0 +1,187 @@ +setTimeout(null); + self::$fixtureServer->setIdleTimeout(null); + self::$fixtureServer->start(); + + // Wait for server to be ready + $maxWaitTime = 5; + $startTime = \time(); + while (\time() <= $startTime + $maxWaitTime) { + if (self::isServerReady(self::$fixtureServerHost, self::$fixtureServerPort)) { + break; + } + \usleep(100000); // 100ms + } + + if (!self::isServerReady(self::$fixtureServerHost, self::$fixtureServerPort)) { + $stderr = ''; + if (self::$fixtureServer?->isStarted()) { + $stderr = \trim(self::$fixtureServer->getErrorOutput()); + } + self::$fixtureServer?->stop(); + throw new \RuntimeException(sprintf('Fixture server did not start within %d seconds%s%s', $maxWaitTime, '' !== $stderr ? ' - stderr: ' : '', $stderr)); + } + + \register_shutdown_function([static::class, 'stopFixtureServer']); + } + + /** + * Stop the fixture server after all tests in the class complete. + */ + public static function tearDownAfterClass(): void + { + self::stopFixtureServer(); + } + + /** + * Stop the fixture server if it's running. + */ + public static function stopFixtureServer(): void + { + if (null !== self::$fixtureServer && self::$fixtureServer->isRunning()) { + self::$fixtureServer->stop(); + self::$fixtureServer = null; + } + } + + /** + * Set up Playwright for each test. + */ + protected function setUp(): void + { + $this->setUpPlaywright(); + } + + /** + * Tear down Playwright after each test. + */ + protected function tearDown(): void + { + $this->tearDownPlaywright(); + } + + /** + * Get the base URL for the fixture server. + */ + protected function fixtureUrl(string $path = '/'): string + { + $path = '/' === $path ? '/' : '/'.ltrim($path, '/'); + + return self::$fixtureServerBaseUrl.$path; + } + + /** + * Check if a port is available for use. + */ + private static function isPortAvailable(string $host, int $port): bool + { + $connection = @\fsockopen($host, $port, $errno, $errstr, 1); + if (\is_resource($connection)) { + \fclose($connection); + + return false; + } + + return true; + } + + /** + * Find a random available port in the range 8000-9000. + */ + private static function findAvailablePort(string $host): int + { + for ($i = 0; $i < 20; ++$i) { + $port = \random_int(8000, 9000); + if (self::isPortAvailable($host, $port)) { + return $port; + } + } + + return 0; + } + + private static function isServerReady(string $host, int $port): bool + { + $url = sprintf('http://%s:%d/', $host, $port); + $ctx = \stream_context_create(['http' => ['timeout' => 0.5, 'ignore_errors' => true]]); + $body = @\file_get_contents($url, false, $ctx); + + // Any HTTP response means the server is up (status may be 404/5xx). + return false !== $body; + } +}