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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ This monorepo develops two packages on a shared version line that is independent
| Package | Responsibility | Use it for |
| --- | --- | --- |
| [`contao/installation-recipe`](installation-recipe/) | Defines and applies portable recipes containing Composer requirements, configuration, database fixtures, and project files. The host application supplies operations such as dependency installation and migration. | Building an installer or importer that consumes recipes. |
| [`contao/e2e-testing`](e2e-testing/) | Consumes those recipes to provision isolated Contao Managed Editions for tests, with a database, installation cache, web server, and HTTP and browser clients. | Testing a Contao project through HTTP requests or a real browser from PHPUnit. |
| [`contao/e2e-testing`](e2e-testing/) | Provides browser testing for existing web applications and consumes recipes to provision isolated Contao Managed Editions with a database, installation cache, web server, and HTTP clients. | Testing Contao extensions, complete Contao projects, or other web applications from PHPUnit. |

The dependency goes from `contao/e2e-testing` to `contao/installation-recipe`. They live together so changes to the recipe model and its test consumer can be tested atomically. Both packages are released independently of `contao/contao`, and the consuming project selects the Contao version to test.

Expand Down
72 changes: 66 additions & 6 deletions e2e-testing/README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,63 @@
# Contao E2E testing

`contao/e2e-testing` owns the test runtime. It consumes recipes from `contao/installation-recipe` to prepare a real Contao Managed Edition and migrate an isolated MySQL/MariaDB database. Tests can make direct HTTP requests, use Symfony BrowserKit for HTTP tests without JavaScript, or drive a real browser with Playwright. The test suite selects the Contao version in its recipe because this library does not require a Contao bundle.
`contao/e2e-testing` owns the test runtime for Contao and other web applications. It can test an already running application through its base URL, or consume recipes from `contao/installation-recipe` to prepare a real Contao Managed Edition and migrate an isolated MySQL/MariaDB database. Tests can make direct HTTP requests, use Symfony BrowserKit for HTTP tests without JavaScript, or drive a real browser with Playwright. Managed Edition tests select the Contao version in their recipe because this library does not require a Contao bundle.

Install it as a development dependency in the project under test. Composer also installs `contao/installation-recipe`, which provides the recipe model used below:

```shell
composer require --dev contao/e2e-testing
```

## Test an existing application

Use `AbstractApplicationTestCase` to test any running web application. The application can be a complete Contao project or use another framework or language. Start its server before PHPUnit, then supply its base URL:

```php
use Contao\E2eTesting\Application\AbstractApplicationTestCase;
use Contao\E2eTesting\Application\ApplicationConfig;

final class HomepageTest extends AbstractApplicationTestCase
{
protected static function createApplicationConfig(): ApplicationConfig
{
return ApplicationConfig::create(
getenv('E2E_BASE_URL') ?: 'http://localhost:8080',
)->withTraceDirectory(dirname(__DIR__, 2).'/.contao-e2e/traces');
}

public function testHomepage(): void
{
$browser = self::application()->createBrowser();
$browser->visit('/');

$this->assertSelectorTextContains('h1', 'Welcome');
}
}
```

Install the browser binaries as described in [Browser tests](#browser-tests), add the tests to your PHPUnit configuration, and run them with the application's URL:

```shell
E2E_BASE_URL=http://localhost:8080 vendor/bin/phpunit --testsuite=e2e
```

`ApplicationTestTrait` supplies the same integration when your tests already extend another PHPUnit base class. Each test starts with fresh browser contexts and independent cookies and storage. The browser engine is reused across the class and closed at the end. Your project controls server startup, application configuration, database fixtures and any application state reset between tests.

`ApplicationConfig::create()` accepts an absolute HTTP or HTTPS URL, including a base path such as `https://example.test/app`. `withTraceDirectory()` returns a cloned configuration. The default trace directory is `.contao-e2e/traces` relative to the working directory. Browser engine selection, `BrowserOptions`, Playwright environment variables, and `CONTAO_E2E_TRACE` work as described below for both application modes.

For an existing Contao project, frontend tests use `createBrowser()` and backend tests can use the usual helpers:

```php
$backend = self::application()->createBackendBrowser();
$backend->visit('/contao/login');
$backend->submitLogin('admin', 'password');
```

For use outside the PHPUnit traits, create `Application` with the configuration, call `createBrowser()` or `createBackendBrowser()`, and call `release()` in a `finally` block. `BrowserRuntime` owns session tracking, current-page access, trace output and cleanup. `ApplicationTestTrait` supplies the shared PHPUnit lifecycle, assertions and tracing for all application tests. `ManagedEditionTestTrait` supplies the Managed Edition configuration type and access to Contao-specific operations. Provisioning and database resets belong to the configuration and application implementations. `ApplicationInterface::resetState()` defines the reset between tests. URL-based applications close their browser contexts, while Managed Editions also restore their database fixtures, including when used directly with `ApplicationTestTrait`. Both configurations implement `ApplicationConfigInterface`, which creates an `ApplicationInterface` for the shared lifecycle. Custom configurations can implement the same contract without changing the trait.

## Database setup

If Docker is available, no database setup is needed. The first test starts a reusable `mariadb:11.4` container on a random loopback port. The last E2E process stops it, and subsequent runs restart the same container. Its `/var/lib/mysql` directory is bind-mounted to `.contao-e2e/database/data`, so all generated database files remain inside the project-local E2E workspace. Parallel test workers keep shared leases and only the final worker stops the database. If a process is killed before PHP can run its shutdown handlers, `database:stop` cleans up any remaining containers.
For Managed Edition tests, no database setup is needed when Docker is available. The first test starts a reusable `mariadb:11.4` container on a random loopback port. The last E2E process stops it, and subsequent runs restart the same container. Its `/var/lib/mysql` directory is bind-mounted to `.contao-e2e/database/data`, so all generated database files remain inside the project-local E2E workspace. Parallel test workers keep shared leases and only the final worker stops the database. If a process is killed before PHP can run its shutdown handlers, `database:stop` cleans up any remaining containers.

Select a database explicitly in the PHPUnit configuration when an extension supports a particular database range:

Expand Down Expand Up @@ -49,7 +96,7 @@ final class ManagedEditionSmokeTest extends TestCase implements DockerServicePro
{
use ManagedEditionTestTrait;

protected static function createManagedEditionConfig(): ManagedEditionConfig
protected static function createApplicationConfig(): ManagedEditionConfig
{
$bundleRoot = dirname(__DIR__, 2);
$composer = ComposerConfig::managedEdition('^5.7')
Expand All @@ -60,7 +107,7 @@ final class ManagedEditionSmokeTest extends TestCase implements DockerServicePro

public static function dockerServices(): iterable
{
return static::createManagedEditionConfig()->dockerServices();
return static::createApplicationConfig()->dockerServices();
}
}
```
Expand Down Expand Up @@ -95,7 +142,7 @@ $devConfig = $config->withAppEnvironment('dev');
$prodConfig = $config->withAppEnvironment('prod');
```

Changing the environment refreshes the cached application setup. The selected environment applies to Contao setup commands, database migration, and HTTP requests. With `ManagedEditionTestTrait`, return the desired configuration from `createManagedEditionConfig()` for each test class.
Changing the environment refreshes the cached application setup. The selected environment applies to Contao setup commands, database migration, and HTTP requests. With `ManagedEditionTestTrait`, return the desired configuration from `createApplicationConfig()` for each test class.

## Browser tests

Expand Down Expand Up @@ -193,7 +240,7 @@ use Contao\InstallationRecipe\Recipe\InstallationRecipe;

final class ManagedEditionSmokeTest extends AbstractManagedEditionTestCase
{
protected static function createManagedEditionConfig(): ManagedEditionConfig
protected static function createApplicationConfig(): ManagedEditionConfig
{
$bundleRoot = dirname(__DIR__, 2);
$composer = ComposerConfig::managedEdition('^5.7')
Expand Down Expand Up @@ -240,6 +287,19 @@ vendor/bin/phpunit --configuration=phpunit.xml.dist tests/E2e/ManagedEditionSmok

The trait works with PHPUnit 10 through 13 and does not impose a test base class. Once the smoke test runs, replace its login-page assertion with checks for your bundle's behavior. Add database fixtures with `InstallationRecipe::withFixtureFile()` when the test needs existing pages or backend users.

### Frontend tests in a Managed Edition

The same isolated Contao installation can serve frontend and backend tests. Prepare the page structure and content through recipe fixtures, and add project files such as templates and assets through recipe file mappings. Then visit a frontend URL with the generic browser:

```php
$browser = self::managedEdition()->createBrowser();
$browser->visit('/');

$this->assertSelectorTextContains('h1', 'Welcome');
```

Use `Origin::http('example.test')` or `Origin::https('example.test')` when a frontend fixture needs a specific page domain. Managed Edition mode builds a recipe-based test installation. URL-based application tests exercise the project served at the supplied URL.

### Test-specific DCA

Use `withDcaFile()` to add a PHP DCA file from the test suite to the Managed Edition. The file is copied to the project's `contao/dca/` directory before Contao setup and database migration. Its basename determines the DCA file name, so a source named `tl_content.php` configures `tl_content`:
Expand Down
20 changes: 20 additions & 0 deletions e2e-testing/src/Application/AbstractApplicationTestCase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

/*
* This file is part of Contao.
*
* (c) Leo Feyer
*
* @license LGPL-3.0-or-later
*/

namespace Contao\E2eTesting\Application;

use PHPUnit\Framework\TestCase;

abstract class AbstractApplicationTestCase extends TestCase
{
use ApplicationTestTrait;
}
56 changes: 56 additions & 0 deletions e2e-testing/src/Application/Application.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

/*
* This file is part of Contao.
*
* (c) Leo Feyer
*
* @license LGPL-3.0-or-later
*/

namespace Contao\E2eTesting\Application;

use Contao\E2eTesting\Browser\BackendBrowser;
use Contao\E2eTesting\Browser\BrowserOptions;
use Contao\E2eTesting\Browser\BrowserRuntime;
use Contao\E2eTesting\Browser\BrowserSession;
use Contao\E2eTesting\Browser\BrowserType;

final class Application implements ApplicationInterface
{
private readonly BrowserRuntime $browserRuntime;

public function __construct(
private readonly ApplicationConfig $config,
BrowserRuntime|null $browserRuntime = null,
) {
$this->browserRuntime = $browserRuntime ?? new BrowserRuntime($config->traceDirectory());
}

public function createBrowser(BrowserType $type = BrowserType::Firefox, BrowserOptions|null $options = null): BrowserSession
{
return $this->browserRuntime->createBrowser($this->config->baseUri, $type, $options);
}

public function createBackendBrowser(BrowserType $type = BrowserType::Firefox, BrowserOptions|null $options = null): BackendBrowser
{
return new BackendBrowser($this->createBrowser($type, $options));
}

public function browserRuntime(): BrowserRuntime
{
return $this->browserRuntime;
}

public function resetState(): void
{
$this->browserRuntime->reset();
}

public function release(): void
{
$this->browserRuntime->close();
}
}
55 changes: 55 additions & 0 deletions e2e-testing/src/Application/ApplicationConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

/*
* This file is part of Contao.
*
* (c) Leo Feyer
*
* @license LGPL-3.0-or-later
*/

namespace Contao\E2eTesting\Application;

final class ApplicationConfig implements ApplicationConfigInterface
{
private function __construct(
public readonly string $baseUri,
private string $traceDirectory = '.contao-e2e/traces',
) {
}

public static function create(string $baseUri): self
{
$parts = parse_url($baseUri);

if (false === $parts || !\in_array($parts['scheme'] ?? null, ['http', 'https'], true) || empty($parts['host']) || (isset($parts['query']) || isset($parts['fragment']))) {
throw new \InvalidArgumentException('The application base URI must be an absolute HTTP or HTTPS URL without a query or fragment.');
}

return new self(rtrim($baseUri, '/'));
}

public function createApplication(): Application
{
return new Application($this);
}

public function traceDirectory(): string
{
return $this->traceDirectory;
}

public function withTraceDirectory(string $traceDirectory): self
{
if ('' === trim($traceDirectory)) {
throw new \InvalidArgumentException('The trace directory must not be empty.');
}

$clone = clone $this;
$clone->traceDirectory = $traceDirectory;

return $clone;
}
}
18 changes: 18 additions & 0 deletions e2e-testing/src/Application/ApplicationConfigInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

/*
* This file is part of Contao.
*
* (c) Leo Feyer
*
* @license LGPL-3.0-or-later
*/

namespace Contao\E2eTesting\Application;

interface ApplicationConfigInterface
{
public function createApplication(): ApplicationInterface;
}
32 changes: 32 additions & 0 deletions e2e-testing/src/Application/ApplicationInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

/*
* This file is part of Contao.
*
* (c) Leo Feyer
*
* @license LGPL-3.0-or-later
*/

namespace Contao\E2eTesting\Application;

use Contao\E2eTesting\Browser\BackendBrowser;
use Contao\E2eTesting\Browser\BrowserOptions;
use Contao\E2eTesting\Browser\BrowserRuntime;
use Contao\E2eTesting\Browser\BrowserSession;
use Contao\E2eTesting\Browser\BrowserType;

interface ApplicationInterface
{
public function createBrowser(BrowserType $type = BrowserType::Firefox, BrowserOptions|null $options = null): BrowserSession;

public function createBackendBrowser(BrowserType $type = BrowserType::Firefox, BrowserOptions|null $options = null): BackendBrowser;

public function browserRuntime(): BrowserRuntime;

public function resetState(): void;

public function release(): void;
}
Loading
Loading