Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion tests/Fixtures/html/forms.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<title>Forms - Playwright PHP Tests</title>
<body>
<h1>Form Tests</h1>
Expand Down Expand Up @@ -43,4 +45,4 @@ <h1>Form Tests</h1>
<button type="reset" id="reset-btn">Reset</button>
</form>
</body>
</html>
</html>
2 changes: 1 addition & 1 deletion tests/Fixtures/html/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ <h1 id="heading">Test Index</h1>
<a href="/events.html">Events</a>
</nav>
</body>
</html>
</html>
22 changes: 21 additions & 1 deletion tests/Fixtures/server.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -34,3 +52,5 @@
echo '<html><head><meta charset="utf-8"><title>404 Not Found</title></head>';
echo '<body><h1>404 Not Found</h1><p>File not found: '.htmlspecialchars($uri, \ENT_QUOTES, 'UTF-8').'</p></body>';
echo '</html>';

return true;
122 changes: 119 additions & 3 deletions tests/Functional/FunctionalTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
}
});
}
}

/**
Expand Down Expand Up @@ -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, '/');
}

/**
Expand Down Expand Up @@ -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;
}
}
74 changes: 74 additions & 0 deletions tests/Integration/Functional/BasicFunctionalTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

declare(strict_types=1);

/*
* This file is part of the community-maintained Playwright PHP project.
* It is not affiliated with or endorsed by Microsoft.
*
* (c) 2025-Present - Playwright PHP - https://github.com/playwright-php
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Playwright\Tests\Integration\Functional;

use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use Playwright\Locator\Locator;
use Playwright\Page\Page;
use Playwright\Tests\Support\FunctionalTestCase;

/**
* Example functional test demonstrating the automatic fixture server.
*
* The fixture server is automatically started before tests run and stopped
* after tests complete. Tests can navigate to fixture URLs using the
* fixtureUrl() helper method.
*/
#[CoversClass(Locator::class)]
#[CoversClass(Page::class)]
final class BasicFunctionalTest extends FunctionalTestCase
{
#[Test]
public function itCanLoadIndexPage(): void
{
$this->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);
}
}
Loading