diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b918b4e..6a6e205 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -15,16 +15,12 @@ jobs:
matrix:
php: [8.2, 8.3, 8.4, 8.5]
deps: [highest]
- symfony: [6.4.*, 7.4.*, 8.0.*, 8.1.*]
+ symfony: [6.4.*, 7.4.*, 8.1.*]
include:
- php: 8.2
deps: lowest
symfony: '*'
exclude:
- - php: 8.2
- symfony: 8.0.*
- - php: 8.3
- symfony: 8.0.*
- php: 8.2
symfony: 8.1.*
- php: 8.3
@@ -40,6 +36,7 @@ jobs:
php: ${{ matrix.php }}
symfony: ${{ matrix.symfony }}
deps: ${{ matrix.deps }}
+ phpunit: paratest --functional
code-coverage:
name: Code Coverage
diff --git a/README.md b/README.md
index c0925b7..98c1585 100644
--- a/README.md
+++ b/README.md
@@ -120,7 +120,7 @@ class MyTest extends TestCase
}
```
-All browsers have the following methods:
+Both browsers have the following methods:
```php
/** @var \Zenstruck\Browser $browser **/
@@ -235,7 +235,7 @@ $browser
### Authentication
-All browsers have helpers and assertions for authentication:
+Both browsers have helpers and assertions for authentication:
```php
/** @var \Zenstruck\Browser $browser **/
@@ -277,7 +277,7 @@ previous request didn't perform any security-related operations. Possible soluti
### Exceptions
Exceptions thrown while handling a request are caught and converted to a response, as they are in
-production. The `KernelBrowser` and `PlaywrightBrowser` can both turn this off:
+production. Both browsers can turn this off:
```php
/** @var \Zenstruck\Browser $browser **/
@@ -299,8 +299,7 @@ $browser
### Redirects
-By default, redirects are followed. Both the `KernelBrowser` and `PlaywrightBrowser` can stop on
-them instead:
+By default, redirects are followed. Both browsers can stop on them instead:
```php
/** @var \Zenstruck\Browser $browser **/
@@ -336,7 +335,7 @@ $browser
### Profiling
-The Symfony profiler is available to both browsers:
+Both browsers expose the Symfony profiler:
```php
/** @var \Zenstruck\Browser $browser **/
@@ -619,6 +618,19 @@ Each `playwrightBrowser()` call gets its own browser context, isolated from the
and storage, while sharing one browser process. They also share the kernel booted for the test, so
they see the same application state, just as separate browsers hitting one webserver would.
+### Parallel Testing
+
+Both browsers work with [ParaTest](https://github.com/paratestphp/paratest). Each worker is a
+separate PHP process with its own browser, launched only if that worker runs a test needing one.
+
+Saved artifacts work as usual: each worker writes to the configured directories, and a failure
+still saves the source, screenshot and console log. The summary printed at the end of a serial run
+is not shown, as ParaTest does not surface worker output by then.
+
+> [!NOTE]
+> Expect one browser per worker: `--processes 8` means up to eight browsers, each with its own Node
+> process, so pick a number your machine and CI runner can carry.
+
## Configuration
There are several environment variables available to configure:
diff --git a/attachment.zip b/attachment.zip
deleted file mode 100644
index e6ebe53..0000000
Binary files a/attachment.zip and /dev/null differ
diff --git a/composer.json b/composer.json
index dadb29d..277677b 100644
--- a/composer.json
+++ b/composer.json
@@ -22,10 +22,11 @@
"zenstruck/callback": "^1.4.2"
},
"require-dev": {
+ "brianium/paratest": "^6.11|^7.0",
"justinrainbow/json-schema": "^5.3",
"mtdowling/jmespath.php": "^2.6",
"phpstan/phpstan": "^2.0",
- "phpunit/phpunit": "^9.6.21|^10.4",
+ "phpunit/phpunit": "^9.6.21|^10.4|^11.5|^12.0|^13.0",
"playwright-php/playwright": "^1.4",
"playwright-php/playwright-symfony": "^0.10",
"symfony/console": "^6.4|^7.0|^8.0",
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index f33392d..773c2c4 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,7 +1,7 @@
+
+
+
+
./tests/
diff --git a/tests/BrowserTests.php b/tests/BrowserTests.php
index bac4df6..35e6d43 100644
--- a/tests/BrowserTests.php
+++ b/tests/BrowserTests.php
@@ -12,6 +12,8 @@
namespace Zenstruck\Browser\Tests;
use PHPUnit\Framework\AssertionFailedError;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use Psr\Container\ContainerInterface;
use Symfony\Component\BrowserKit\AbstractBrowser;
use Symfony\Component\BrowserKit\Cookie;
@@ -26,6 +28,7 @@
use Zenstruck\Assert;
use Zenstruck\Browser;
use Zenstruck\Browser\Test\HasBrowser;
+use Zenstruck\Browser\Test\LegacyExtension;
use Zenstruck\Browser\Tests\Fixture\TestComponent1;
use Zenstruck\Browser\Tests\Fixture\TestComponent2;
use Zenstruck\Callback\Exception\UnresolveableArgument;
@@ -42,6 +45,7 @@ trait BrowserTests
/**
* @test
*/
+ #[Test]
public function fails_if_trying_to_manipulate_exception_page(): void
{
Assert::that(function() {
@@ -69,6 +73,7 @@ public function fails_if_trying_to_manipulate_exception_page(): void
/**
* @test
*/
+ #[Test]
public function can_enable_exception_throwing(): void
{
$this->expectException(\Exception::class);
@@ -83,6 +88,7 @@ public function can_enable_exception_throwing(): void
/**
* @test
*/
+ #[Test]
public function can_re_enable_catching_exceptions(): void
{
$browser = $this->browser();
@@ -105,6 +111,7 @@ public function can_re_enable_catching_exceptions(): void
/**
* @test
*/
+ #[Test]
public function exceptions_are_caught_by_default(): void
{
$this->browser()
@@ -116,6 +123,7 @@ public function exceptions_are_caught_by_default(): void
/**
* @test
*/
+ #[Test]
public function fails_if_expected_exception_not_thrown(): void
{
// visit
@@ -160,6 +168,7 @@ function() {
/**
* @test
*/
+ #[Test]
public function can_expect_exception_for_form_submit(): void
{
$this->browser()
@@ -172,6 +181,7 @@ public function can_expect_exception_for_form_submit(): void
/**
* @test
*/
+ #[Test]
public function can_expect_exception_for_link_click(): void
{
$this->browser()
@@ -186,6 +196,7 @@ public function can_expect_exception_for_link_click(): void
/**
* @test
*/
+ #[Test]
public function multiple_browsers(): void
{
$browser1 = $this->browser()
@@ -205,6 +216,7 @@ public function multiple_browsers(): void
/**
* @test
*/
+ #[Test]
public function assert_on(): void
{
$this->browser()
@@ -226,6 +238,8 @@ public function assert_on(): void
*
* @dataProvider encodedUrlProvider
*/
+ #[Test]
+ #[DataProvider('encodedUrlProvider')]
public function assert_on_encoded($url, $expected): void
{
$this->browser()
@@ -249,6 +263,7 @@ public static function encodedUrlProvider(): iterable
/**
* @test
*/
+ #[Test]
public function can_use_current_browser(): void
{
$browser = $this->browser();
@@ -269,6 +284,7 @@ public function can_use_current_browser(): void
/**
* @test
*/
+ #[Test]
public function can_use_components(): void
{
$this->browser()
@@ -282,6 +298,7 @@ public function can_use_components(): void
/**
* @test
*/
+ #[Test]
public function component_pre_assertions_and_actions_are_called(): void
{
$this->browser()
@@ -295,6 +312,7 @@ public function component_pre_assertions_and_actions_are_called(): void
/**
* @test
*/
+ #[Test]
public function can_use_crawler(): void
{
$this->browser()
@@ -308,6 +326,7 @@ public function can_use_crawler(): void
/**
* @test
*/
+ #[Test]
public function can_use_cookie_jar(): void
{
$this->browser()
@@ -321,6 +340,7 @@ public function can_use_cookie_jar(): void
/**
* @test
*/
+ #[Test]
public function can_manipulate_cookies(): void
{
$expires = \time() + 3600;
@@ -366,6 +386,7 @@ public function can_manipulate_cookies(): void
/**
* @test
*/
+ #[Test]
public function with_can_accept_multiple_browsers_and_components(): void
{
$browser = $this->browser();
@@ -385,6 +406,7 @@ public function with_can_accept_multiple_browsers_and_components(): void
/**
* @test
*/
+ #[Test]
public function invalid_use_callback_parameter_throws_type_error(): void
{
$this->expectException(UnresolveableArgument::class);
@@ -395,6 +417,7 @@ public function invalid_use_callback_parameter_throws_type_error(): void
/**
* @test
*/
+ #[Test]
public function following_redirect_follows_all_by_default(): void
{
$this->browser()
@@ -410,6 +433,7 @@ public function following_redirect_follows_all_by_default(): void
/**
* @test
*/
+ #[Test]
public function can_re_enable_following_redirects(): void
{
$this->browser()
@@ -425,6 +449,7 @@ public function can_re_enable_following_redirects(): void
/**
* @test
*/
+ #[Test]
public function calling_follow_redirects_when_the_response_is_a_redirect_follows_the_redirect(): void
{
$this->browser()
@@ -442,6 +467,7 @@ public function calling_follow_redirects_when_the_response_is_a_redirect_follows
/**
* @test
*/
+ #[Test]
public function calling_follow_redirects_before_a_request_has_been_made_just_enables_following_redirects(): void
{
$this->browser()
@@ -454,6 +480,7 @@ public function calling_follow_redirects_before_a_request_has_been_made_just_ena
/**
* @test
*/
+ #[Test]
public function can_limit_redirects_followed(): void
{
$this->browser()
@@ -476,6 +503,7 @@ public function can_limit_redirects_followed(): void
/**
* @test
*/
+ #[Test]
public function assert_redirected_to_follows_all_redirects_by_default(): void
{
$this->browser()
@@ -488,6 +516,7 @@ public function assert_redirected_to_follows_all_redirects_by_default(): void
/**
* @test
*/
+ #[Test]
public function assert_redirected_to_can_configure_number_of_redirects_to_follow(): void
{
$this->browser()
@@ -500,6 +529,7 @@ public function assert_redirected_to_can_configure_number_of_redirects_to_follow
/**
* @test
*/
+ #[Test]
public function exception_thrown_if_asserting_redirected_and_not_intercepting_redirects(): void
{
$this->expectException(\RuntimeException::class);
@@ -514,6 +544,7 @@ public function exception_thrown_if_asserting_redirected_and_not_intercepting_re
/**
* @test
*/
+ #[Test]
public function exception_thrown_if_asserting_redirected_to_and_not_intercepting_redirects(): void
{
$this->expectException(\RuntimeException::class);
@@ -528,6 +559,7 @@ public function exception_thrown_if_asserting_redirected_to_and_not_intercepting
/**
* @test
*/
+ #[Test]
public function click_and_intercept(): void
{
$this->browser()
@@ -544,6 +576,7 @@ public function click_and_intercept(): void
/**
* @test
*/
+ #[Test]
public function redirects_are_followed_by_default(): void
{
$this->browser()
@@ -555,6 +588,7 @@ public function redirects_are_followed_by_default(): void
/**
* @test
*/
+ #[Test]
public function response_header_assertions(): void
{
$this->browser()
@@ -568,6 +602,7 @@ public function response_header_assertions(): void
/**
* @test
*/
+ #[Test]
public function response_status_assertions(): void
{
$this->browser()
@@ -581,6 +616,7 @@ public function response_status_assertions(): void
/**
* @test
*/
+ #[Test]
public function can_use_container_as_typehint(): void
{
$this->browser()
@@ -594,6 +630,7 @@ public function can_use_container_as_typehint(): void
/**
* @test
*/
+ #[Test]
public function can_act_as_user(): void
{
$this->browser()
@@ -606,6 +643,7 @@ public function can_act_as_user(): void
/**
* @test
*/
+ #[Test]
public function can_make_authentication_assertions(): void
{
$username = 'kevin';
@@ -626,6 +664,7 @@ public function can_make_authentication_assertions(): void
/**
* @test
*/
+ #[Test]
public function can_check_if_not_authenticated_after_request(): void
{
$this->browser()
@@ -638,6 +677,7 @@ public function can_check_if_not_authenticated_after_request(): void
/**
* @test
*/
+ #[Test]
public function can_login_with_a_form_and_be_remembered(): void
{
$this->browser()
@@ -666,6 +706,7 @@ public function can_login_with_a_form_and_be_remembered(): void
/**
* @test
*/
+ #[Test]
public function can_enable_the_profiler(): void
{
$profile = $this->browser()
@@ -680,6 +721,26 @@ public function can_enable_the_profiler(): void
/**
* @test
*/
+ #[Test]
+ public function extension_saves_the_browser_state_when_a_test_fails(): void
+ {
+ // drives the hooks phpunit calls: saveBrowserStates() swallows its exceptions, so a
+ // regression there is invisible without asserting the artifacts land
+ $extension = new LegacyExtension();
+ $extension->executeBeforeFirstTest();
+ $extension->executeBeforeTest('X::y');
+
+ $this->browser()->visit('/page1');
+
+ $extension->executeAfterTestFailure('X::y', 'the failure message', 0.0);
+
+ $this->assertFileExists(__DIR__.'/../var/browser/source/failure_X__y__0.html');
+ }
+
+ /**
+ * @test
+ */
+ #[Test]
public function can_profile_multiple_requests(): void
{
$browser = $this->browser();
@@ -695,6 +756,7 @@ public function can_profile_multiple_requests(): void
/**
* @test
*/
+ #[Test]
public function can_access_the_profiler(): void
{
$profile = $this->browser()
@@ -709,6 +771,7 @@ public function can_access_the_profiler(): void
/**
* @test
*/
+ #[Test]
public function can_use_data_collector(): void
{
$this->browser()
@@ -723,6 +786,7 @@ public function can_use_data_collector(): void
/**
* @test
*/
+ #[Test]
public function content_assertions(): void
{
$this->browser()
@@ -735,6 +799,7 @@ public function content_assertions(): void
/**
* @test
*/
+ #[Test]
public function can_dump_response(): void
{
$output = self::catchVarDumperOutput(function() {
@@ -752,6 +817,7 @@ public function can_dump_response(): void
/**
* @test
*/
+ #[Test]
public function can_save_source(): void
{
$contents = self::catchFileContents(__DIR__.'/../var/browser/source/source.txt', function() {
@@ -768,6 +834,7 @@ public function can_save_source(): void
/**
* @test
*/
+ #[Test]
public function can_save_source_as_zip(): void
{
$contents = self::catchFileContents(__DIR__.'/../var/browser/source/attachment.zip', function() {
@@ -786,6 +853,7 @@ public function can_save_source_as_zip(): void
/**
* @test
*/
+ #[Test]
public function html_assertions(): void
{
$this->browser()
@@ -803,6 +871,7 @@ public function html_assertions(): void
/**
* @test
*/
+ #[Test]
public function html_head_assertions(): void
{
$this->browser()
@@ -817,6 +886,7 @@ public function html_head_assertions(): void
/**
* @test
*/
+ #[Test]
public function form_assertions(): void
{
$this->browser()
@@ -862,6 +932,7 @@ public function form_assertions(): void
/**
* @test
*/
+ #[Test]
public function link_action(): void
{
$this->browser()
@@ -874,6 +945,7 @@ public function link_action(): void
/**
* @test
*/
+ #[Test]
public function click_on_element(): void
{
$this->browser()
@@ -886,6 +958,7 @@ public function click_on_element(): void
/**
* @test
*/
+ #[Test]
public function form_actions_by_field_label(): void
{
$this->browser()
@@ -914,6 +987,7 @@ public function form_actions_by_field_label(): void
/**
* @test
*/
+ #[Test]
public function form_actions_by_field_id(): void
{
$this->browser()
@@ -942,6 +1016,7 @@ public function form_actions_by_field_id(): void
/**
* @test
*/
+ #[Test]
public function form_actions_by_field_name(): void
{
$this->browser()
@@ -968,6 +1043,7 @@ public function form_actions_by_field_name(): void
/**
* @test
*/
+ #[Test]
public function select_field(): void
{
$this->browser()
@@ -988,6 +1064,7 @@ public function select_field(): void
/**
* @test
*/
+ #[Test]
public function can_submit_form_with_different_submit_buttons(): void
{
// Submit and Submit B, have the same field name but different values
@@ -1022,6 +1099,7 @@ public function can_submit_form_with_different_submit_buttons(): void
*
* @test
*/
+ #[Test]
public function can_submit_filled_form_with_different_submit_buttons(): void
{
// Submit and Submit B, have the same field name but different values
@@ -1062,6 +1140,7 @@ public function can_submit_filled_form_with_different_submit_buttons(): void
/**
* @test
*/
+ #[Test]
public function cannot_attach_file_that_does_not_exist(): void
{
$this->expectException(\InvalidArgumentException::class);
@@ -1075,6 +1154,7 @@ public function cannot_attach_file_that_does_not_exist(): void
/**
* @test
*/
+ #[Test]
public function can_attach_multiple_files(): void
{
$this->browser()
@@ -1088,6 +1168,7 @@ public function can_attach_multiple_files(): void
/**
* @test
*/
+ #[Test]
public function cannot_attach_multiple_files_to_a_non_multiple_input(): void
{
$this->expectException(\InvalidArgumentException::class);
@@ -1101,6 +1182,7 @@ public function cannot_attach_multiple_files_to_a_non_multiple_input(): void
/**
* @test
*/
+ #[Test]
public function can_dump_html_element(): void
{
$output = self::catchVarDumperOutput(function() {
@@ -1117,6 +1199,7 @@ public function can_dump_html_element(): void
/**
* @test
*/
+ #[Test]
public function if_dump_selector_matches_multiple_elements_all_are_dumped(): void
{
$output = self::catchVarDumperOutput(function() {
@@ -1134,6 +1217,7 @@ public function if_dump_selector_matches_multiple_elements_all_are_dumped(): voi
/**
* @test
*/
+ #[Test]
public function can_access_the_html_crawler(): void
{
$crawler = $this->browser()
@@ -1148,6 +1232,7 @@ public function can_access_the_html_crawler(): void
/**
* @test
*/
+ #[Test]
public function can_get_content(): void
{
$content = $this->browser()->visit('/text')->content();
diff --git a/tests/ConfigureBrowserTest.php b/tests/ConfigureBrowserTest.php
index c129394..19861cc 100644
--- a/tests/ConfigureBrowserTest.php
+++ b/tests/ConfigureBrowserTest.php
@@ -11,6 +11,7 @@
namespace Zenstruck\Browser\Tests;
+use PHPUnit\Framework\Attributes\Test;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Zenstruck\Browser\KernelBrowser;
use Zenstruck\Browser\Test\HasBrowser;
@@ -25,6 +26,7 @@ final class ConfigureBrowserTest extends WebTestCase
/**
* @test
*/
+ #[Test]
public function browser_has_been_configured(): void
{
$this->page1Browser()->assertOn('/page1');
diff --git a/tests/HttpOptionsTest.php b/tests/HttpOptionsTest.php
index bf9a0ab..493df41 100644
--- a/tests/HttpOptionsTest.php
+++ b/tests/HttpOptionsTest.php
@@ -11,6 +11,7 @@
namespace Zenstruck\Browser\Tests;
+use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Zenstruck\Browser\HttpOptions;
@@ -22,6 +23,7 @@ final class HttpOptionsTest extends TestCase
/**
* @test
*/
+ #[Test]
public function defaults(): void
{
$options = new HttpOptions();
@@ -35,6 +37,7 @@ public function defaults(): void
/**
* @test
*/
+ #[Test]
public function can_configure_with_constructor_array(): void
{
$options = new HttpOptions([
@@ -56,6 +59,7 @@ public function can_configure_with_constructor_array(): void
/**
* @test
*/
+ #[Test]
public function can_configure_via_withers(): void
{
$options = (new HttpOptions())
@@ -83,6 +87,7 @@ public function can_configure_via_withers(): void
/**
* @test
*/
+ #[Test]
public function can_configure_json_and_ajax_with_constructor_array(): void
{
$options = new HttpOptions([
@@ -108,6 +113,7 @@ public function can_configure_json_and_ajax_with_constructor_array(): void
/**
* @test
*/
+ #[Test]
public function ajax_constructor(): void
{
$options = HttpOptions::ajax();
@@ -124,6 +130,7 @@ public function ajax_constructor(): void
/**
* @test
*/
+ #[Test]
public function json_constructor_with_value(): void
{
$options = HttpOptions::json('value');
@@ -141,6 +148,7 @@ public function json_constructor_with_value(): void
/**
* @test
*/
+ #[Test]
public function json_constructor_with_no_value(): void
{
$options = HttpOptions::json();
@@ -158,6 +166,7 @@ public function json_constructor_with_no_value(): void
/**
* @test
*/
+ #[Test]
public function json_ajax_constructor_with_value(): void
{
$options = HttpOptions::jsonAjax('value');
@@ -176,6 +185,7 @@ public function json_ajax_constructor_with_value(): void
/**
* @test
*/
+ #[Test]
public function json_ajax_constructor_with_no_value(): void
{
$options = HttpOptions::jsonAjax();
@@ -194,6 +204,7 @@ public function json_ajax_constructor_with_no_value(): void
/**
* @test
*/
+ #[Test]
public function create_with_self(): void
{
$options = new class extends HttpOptions {};
@@ -204,6 +215,7 @@ public function create_with_self(): void
/**
* @test
*/
+ #[Test]
public function can_merge_with_array(): void
{
$options = HttpOptions::create([
@@ -239,6 +251,7 @@ public function can_merge_with_array(): void
/**
* @test
*/
+ #[Test]
public function can_merge_with_http_options_object(): void
{
$options = HttpOptions::create([
@@ -269,6 +282,7 @@ public function can_merge_with_http_options_object(): void
/**
* @test
*/
+ #[Test]
public function can_override_json_and_ajax_headers(): void
{
$options = HttpOptions::jsonAjax()
@@ -291,6 +305,7 @@ public function can_override_json_and_ajax_headers(): void
/**
* @test
*/
+ #[Test]
public function dots_in_query_string_are_preserved(): void
{
$this->assertSame('/?nested.param=value&deeper.nested.param=y', (new HttpOptions())->addQueryToUrl('/?nested.param=value&deeper.nested.param=y'));
diff --git a/tests/InvalidTestCaseTest.php b/tests/InvalidTestCaseTest.php
index b7b8ce3..f45a6e3 100644
--- a/tests/InvalidTestCaseTest.php
+++ b/tests/InvalidTestCaseTest.php
@@ -11,6 +11,7 @@
namespace Zenstruck\Browser\Tests;
+use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Zenstruck\Browser\Test\HasBrowser;
@@ -24,6 +25,7 @@ final class InvalidTestCaseTest extends TestCase
/**
* @test
*/
+ #[Test]
public function cannot_create_browser(): void
{
$this->expectException(\LogicException::class);
diff --git a/tests/JsonTest.php b/tests/JsonTest.php
index 31a26c0..0e88573 100644
--- a/tests/JsonTest.php
+++ b/tests/JsonTest.php
@@ -14,6 +14,8 @@
namespace Zenstruck\Browser\Tests;
use PHPUnit\Framework\AssertionFailedError;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Zenstruck\Browser\Json;
@@ -24,6 +26,8 @@ class JsonTest extends TestCase
*
* @dataProvider selectorExistsProvider
*/
+ #[Test]
+ #[DataProvider('selectorExistsProvider')]
public function assert_has_passes_if_selector_exists(string $json, string $selector): void
{
(new Json($json))->assertHas($selector);
@@ -34,6 +38,8 @@ public function assert_has_passes_if_selector_exists(string $json, string $selec
*
* @dataProvider selectorDoesNotExistProvider
*/
+ #[Test]
+ #[DataProvider('selectorDoesNotExistProvider')]
public function assert_has_fails_if_selector_does_not_exist(string $json, string $selector): void
{
$this->expectException(AssertionFailedError::class);
@@ -47,6 +53,8 @@ public function assert_has_fails_if_selector_does_not_exist(string $json, string
*
* @dataProvider selectorDoesNotExistProvider
*/
+ #[Test]
+ #[DataProvider('selectorDoesNotExistProvider')]
public function assert_missing_passes_if_selector_does_not_exist(string $json, string $selector): void
{
(new Json($json))->assertMissing($selector);
@@ -57,6 +65,8 @@ public function assert_missing_passes_if_selector_does_not_exist(string $json, s
*
* @dataProvider selectorExistsProvider
*/
+ #[Test]
+ #[DataProvider('selectorExistsProvider')]
public function assert_missing_fails_if_selector_exists(string $json, string $selector): void
{
$this->expectException(AssertionFailedError::class);
@@ -92,6 +102,8 @@ public static function selectorDoesNotExistProvider(): iterable
*
* @dataProvider selectHasCountProvider
*/
+ #[Test]
+ #[DataProvider('selectHasCountProvider')]
public function can_assert_a_selector_has_count(string $json, int $expectedCount): void
{
(new Json($json))->hasCount($expectedCount);
@@ -106,6 +118,7 @@ public static function selectHasCountProvider(): iterable
/**
* @test
*/
+ #[Test]
public function can_perform_assertions_on_itself(): void
{
(new Json('["foo","bar"]'))->contains('bar')->doesNotContain('food');
@@ -116,6 +129,8 @@ public function can_perform_assertions_on_itself(): void
*
* @dataProvider scalarChildAssertionProvider
*/
+ #[Test]
+ #[DataProvider('scalarChildAssertionProvider')]
public function can_perform_assertion_on_scalar_child(string $json, string $selector, callable $asserter): void
{
(new Json($json))->assertThat($selector, $asserter);
@@ -134,6 +149,8 @@ public static function scalarChildAssertionProvider(): iterable
*
* @dataProvider arrayChildAssertionProvider
*/
+ #[Test]
+ #[DataProvider('arrayChildAssertionProvider')]
public function can_perform_assertion_on_array_child(string $json, string $selector, callable $asserter): void
{
(new Json($json))->assertThatEach($selector, $asserter);
@@ -150,6 +167,8 @@ public static function arrayChildAssertionProvider(): iterable
*
* @dataProvider invalidArrayChildAssertionProvider
*/
+ #[Test]
+ #[DataProvider('invalidArrayChildAssertionProvider')]
public function assert_that_each_throws_if_invalid_array_given(string $json, string $selector, callable $asserter): void
{
$this->expectException(AssertionFailedError::class);
@@ -165,6 +184,7 @@ public static function invalidArrayChildAssertionProvider(): iterable
}
/** @test */
+ #[Test]
public function can_match_json_schema(): void
{
(new Json('{"foo1": "bar", "foo2": [1, 2], "foo3": {"bar": "baz"}, "foo4": [{"bar": "baz"}]}'))->assertMatchesSchema(
@@ -190,6 +210,7 @@ public function can_match_json_schema(): void
}
/** @test */
+ #[Test]
public function assoc_array_equals()
{
(new Json('{"foo": "bar", "bar": "baz"}'))->assertMatches('@', ['bar' => 'baz', 'foo' => 'bar']);
diff --git a/tests/KernelBrowserTests.php b/tests/KernelBrowserTests.php
index de3a95d..e95a735 100644
--- a/tests/KernelBrowserTests.php
+++ b/tests/KernelBrowserTests.php
@@ -11,6 +11,7 @@
namespace Zenstruck\Browser\Tests;
+use PHPUnit\Framework\Attributes\Test;
use Psr\Container\ContainerInterface;
use Zenstruck\Browser\HttpOptions;
use Zenstruck\Browser\Json;
@@ -27,6 +28,7 @@ trait KernelBrowserTests
/**
* @test
*/
+ #[Test]
public function can_use_kernel_browser_as_typehint(): void
{
$this->browser()
@@ -40,6 +42,7 @@ public function can_use_kernel_browser_as_typehint(): void
/**
* @test
*/
+ #[Test]
public function reboots_the_kernel_between_requests_by_default(): void
{
$containers = [];
@@ -59,6 +62,7 @@ public function reboots_the_kernel_between_requests_by_default(): void
/**
* @test
*/
+ #[Test]
public function can_disable_reboot(): void
{
$containers = [];
@@ -78,6 +82,7 @@ public function can_disable_reboot(): void
/**
* @test
*/
+ #[Test]
public function can_re_enable_reboot(): void
{
$containers = [];
@@ -98,6 +103,7 @@ public function can_re_enable_reboot(): void
/**
* @test
*/
+ #[Test]
public function http_method_actions(): void
{
$this->browser()
@@ -150,6 +156,7 @@ public function http_method_actions(): void
/**
* @test
*/
+ #[Test]
public function can_set_default_http_options(): void
{
$this->browser()
@@ -165,6 +172,7 @@ public function can_set_default_http_options(): void
/**
* @test
*/
+ #[Test]
public function can_handle_any_content_type(): void
{
$this->browser()
@@ -179,6 +187,7 @@ public function can_handle_any_content_type(): void
/**
* @test
*/
+ #[Test]
public function can_assert_json_matches(): void
{
$this->browser()
@@ -201,6 +210,7 @@ public function can_assert_json_matches(): void
/**
* @test
*/
+ #[Test]
public function assert_content_types(): void
{
$this->browser()
@@ -219,6 +229,7 @@ public function assert_content_types(): void
/**
* @test
*/
+ #[Test]
public function can_dump_empty_json_request(): void
{
$output = self::catchVarDumperOutput(function() {
@@ -234,6 +245,7 @@ public function can_dump_empty_json_request(): void
/**
* @test
*/
+ #[Test]
public function can_dump_json_response_as_array(): void
{
$output = self::catchVarDumperOutput(function() {
@@ -249,6 +261,7 @@ public function can_dump_json_response_as_array(): void
/**
* @test
*/
+ #[Test]
public function dump_includes_headers_and_status(): void
{
$output = self::catchVarDumperOutput(function() {
@@ -265,6 +278,7 @@ public function dump_includes_headers_and_status(): void
/**
* @test
*/
+ #[Test]
public function can_dump_json_array_key(): void
{
$output = self::catchVarDumperOutput(function() {
@@ -280,6 +294,7 @@ public function can_dump_json_array_key(): void
/**
* @test
*/
+ #[Test]
public function can_dump_json_path_expression(): void
{
$output = self::catchVarDumperOutput(function() {
@@ -301,6 +316,7 @@ public function can_dump_json_path_expression(): void
/**
* @test
*/
+ #[Test]
public function can_save_formatted_json_source(): void
{
$contents = self::catchFileContents(__DIR__.'/../var/browser/source/source.txt', function() {
@@ -317,6 +333,7 @@ public function can_save_formatted_json_source(): void
/**
* @test
*/
+ #[Test]
public function can_save_source_when_exception(): void
{
$contents = self::catchFileContents(__DIR__.'/../var/browser/source/source.txt', function() {
@@ -333,6 +350,7 @@ public function can_save_source_when_exception(): void
/**
* @test
*/
+ #[Test]
public function can_access_json_object(): void
{
$json = $this->browser()
@@ -347,6 +365,7 @@ public function can_access_json_object(): void
/**
* @test
*/
+ #[Test]
public function can_use_json_object(): void
{
$this->browser()
@@ -361,6 +380,7 @@ public function can_use_json_object(): void
/**
* @test
*/
+ #[Test]
public function can_dump_xml_selector(): void
{
$output = self::catchVarDumperOutput(function() {
@@ -378,6 +398,7 @@ public function can_dump_xml_selector(): void
/**
* @test
*/
+ #[Test]
public function can_access_the_xml_crawler(): void
{
$crawler = $this->browser()
@@ -392,6 +413,7 @@ public function can_access_the_xml_crawler(): void
/**
* @test
*/
+ #[Test]
public function can_expect_exception_for_http_request(): void
{
$this->browser()
diff --git a/tests/KernelBrowserWebTestCaseTest.php b/tests/KernelBrowserWebTestCaseTest.php
index c38c58b..714be38 100644
--- a/tests/KernelBrowserWebTestCaseTest.php
+++ b/tests/KernelBrowserWebTestCaseTest.php
@@ -11,6 +11,7 @@
namespace Zenstruck\Browser\Tests;
+use PHPUnit\Framework\Attributes\Test;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
/**
@@ -23,6 +24,7 @@ final class KernelBrowserWebTestCaseTest extends WebTestCase
/**
* @test
*/
+ #[Test]
public function calling_browser_ensures_kernel_is_shutdown(): void
{
static::bootKernel();
@@ -36,6 +38,7 @@ public function calling_browser_ensures_kernel_is_shutdown(): void
/**
* @test
*/
+ #[Test]
public function can_use_native_web_test_case_assertions(): void
{
$this->browser()
diff --git a/tests/NormalizationTest.php b/tests/NormalizationTest.php
index 5a97619..652322d 100644
--- a/tests/NormalizationTest.php
+++ b/tests/NormalizationTest.php
@@ -11,6 +11,8 @@
namespace Zenstruck\Browser\Tests;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Zenstruck\Browser;
use Zenstruck\Browser\Test\LegacyExtension;
@@ -22,6 +24,9 @@ final class NormalizationTest extends TestCase
* @dataProvider namesProvider
* @dataProvider edgeCaseTestNames
*/
+ #[Test]
+ #[DataProvider('namesProvider')]
+ #[DataProvider('edgeCaseTestNames')]
public function can_normalize_test_names(string $testName, string $expectedOutput): void
{
$browser = $this->createMock(Browser::class);
diff --git a/tests/PlaywrightBrowserTest.php b/tests/PlaywrightBrowserTest.php
index 6c76a1d..0847b52 100644
--- a/tests/PlaywrightBrowserTest.php
+++ b/tests/PlaywrightBrowserTest.php
@@ -12,15 +12,19 @@
namespace Zenstruck\Browser\Tests;
use PHPUnit\Framework\AssertionFailedError;
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\Test;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Zenstruck\Browser\PlaywrightBrowser;
use Zenstruck\Browser\Test\HasBrowser;
+use Zenstruck\Browser\Test\LegacyExtension;
/**
* @author Kevin Bond
*
* @group playwright
*/
+#[Group('playwright')]
class PlaywrightBrowserTest extends KernelTestCase
{
use BrowserTests, HasBrowser;
@@ -28,6 +32,7 @@ class PlaywrightBrowserTest extends KernelTestCase
/**
* @test
*/
+ #[Test]
public function can_use_playwright_browser_as_typehint(): void
{
$this->browser()
@@ -41,6 +46,26 @@ public function can_use_playwright_browser_as_typehint(): void
/**
* @test
*/
+ #[Test]
+ public function extension_saves_a_screenshot_and_console_log_when_a_test_fails(): void
+ {
+ $extension = new LegacyExtension();
+ $extension->executeBeforeFirstTest();
+ $extension->executeBeforeTest('X::y');
+
+ $this->browser()->visit('/javascript');
+
+ $extension->executeAfterTestError('X::y', 'the error message', 0.0);
+
+ $this->assertFileExists(__DIR__.'/../var/browser/source/error_X__y__0.html');
+ $this->assertFileExists(__DIR__.'/../var/browser/screenshots/error_X__y__0.png');
+ $this->assertFileExists(__DIR__.'/../var/browser/console-logs/error_X__y__0.log');
+ }
+
+ /**
+ * @test
+ */
+ #[Test]
public function can_take_screenshot(): void
{
self::catchFileContents(__DIR__.'/../var/browser/screenshots/screen.png', function() {
@@ -54,6 +79,7 @@ public function can_take_screenshot(): void
/**
* @test
*/
+ #[Test]
public function can_wait(): void
{
$this->browser()
@@ -67,6 +93,7 @@ public function can_wait(): void
/**
* @test
*/
+ #[Test]
public function can_wait_until_visible_and_not_visible(): void
{
$this->browser()
@@ -95,6 +122,7 @@ public function can_wait_until_visible_and_not_visible(): void
/**
* @test
*/
+ #[Test]
public function can_wait_until_see_in_and_not_see_in(): void
{
$this->browser()
@@ -124,6 +152,7 @@ public function can_wait_until_see_in_and_not_see_in(): void
/**
* @test
*/
+ #[Test]
public function can_check_if_element_is_visible_and_not_visible(): void
{
$this->browser()
@@ -137,6 +166,7 @@ public function can_check_if_element_is_visible_and_not_visible(): void
/**
* @test
*/
+ #[Test]
public function can_save_console_log(): void
{
$contents = self::catchFileContents(__DIR__.'/../var/browser/console-logs/console.log', function() {
@@ -154,6 +184,7 @@ public function can_save_console_log(): void
/**
* @test
*/
+ #[Test]
public function can_dump_console_log_with_console_error(): void
{
$output = self::catchVarDumperOutput(function() {
@@ -170,6 +201,7 @@ public function can_dump_console_log_with_console_error(): void
/**
* @test
*/
+ #[Test]
public function can_dump_console_log_with_throw_error(): void
{
$this->markTestSkipped('Uncaught JS errors are a Playwright "pageerror" event, which playwright-php does not expose.');
@@ -188,6 +220,7 @@ public function can_dump_console_log_with_throw_error(): void
/**
* @test
*/
+ #[Test]
public function cannot_follow_invisible_link(): void
{
$this->expectException(AssertionFailedError::class);
@@ -202,7 +235,8 @@ public function cannot_follow_invisible_link(): void
/**
* @test
*/
- public function double_click_on_element(): void
+ #[Test]
+ public function can_double_click_an_element(): void
{
$this->browser()
->visit('/page1')
@@ -220,6 +254,7 @@ public function double_click_on_element(): void
/**
* @test
*/
+ #[Test]
public function context_menu_on_element(): void
{
$this->browser()
diff --git a/tests/Session/Playwright/CookieJarTest.php b/tests/Session/Playwright/CookieJarTest.php
index 279a1b3..088ea14 100644
--- a/tests/Session/Playwright/CookieJarTest.php
+++ b/tests/Session/Playwright/CookieJarTest.php
@@ -11,6 +11,8 @@
namespace Zenstruck\Browser\Tests\Session\Playwright;
+use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Playwright\Page\PageInterface;
use Symfony\Component\BrowserKit\Response;
@@ -26,12 +28,14 @@ final class CookieJarTest extends TestCase
*
* @dataProvider unsupportedMethodProvider
*/
+ #[Test]
+ #[DataProvider('unsupportedMethodProvider')]
public function methods_the_browser_cannot_back_throw(string $method, callable $call): void
{
$this->expectException(\BadMethodCallException::class);
$this->expectExceptionMessage($method.'() is not supported by the real browser.');
- $call(new CookieJar($this->createMock(PageInterface::class)));
+ $call(new CookieJar($this->createStub(PageInterface::class)));
}
public static function unsupportedMethodProvider(): iterable
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index fd62587..833b24c 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -13,4 +13,11 @@
require __DIR__.'/../vendor/autoload.php';
-(new Filesystem())->remove(__DIR__.'/../var');
+// paratest includes this bootstrap in every worker: only the parent process cleans up, and the
+// cache dir is created up front so booting kernels do not race to create it. PARATEST is set for
+// every worker, TEST_TOKEN only when tokens are enabled
+if (!isset($_SERVER['PARATEST'])) {
+ (new Filesystem())->remove(__DIR__.'/../var');
+}
+
+@\mkdir(__DIR__.'/../var/cache/'.($_SERVER['APP_ENV'] ?? 'test'), 0777, true);