From 2bccfeecb22b2d1d8c685faa78563c9123d2e977 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Mon, 24 Aug 2026 08:48:56 -0400 Subject: [PATCH 1/5] test: support phpunit 11, 12 and 13 Test metadata is duplicated as attributes: 9.6 only reads the annotations, 13 only reads the attributes. --- composer.json | 2 +- tests/BrowserTests.php | 65 ++++++++++++++++++++++ tests/ConfigureBrowserTest.php | 2 + tests/HttpOptionsTest.php | 15 +++++ tests/InvalidTestCaseTest.php | 2 + tests/JsonTest.php | 21 +++++++ tests/KernelBrowserTests.php | 22 ++++++++ tests/KernelBrowserWebTestCaseTest.php | 3 + tests/NormalizationTest.php | 5 ++ tests/PlaywrightBrowserTest.php | 15 +++++ tests/Session/Playwright/CookieJarTest.php | 6 +- 11 files changed, 156 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index dadb29d..92a3b2a 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "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/tests/BrowserTests.php b/tests/BrowserTests.php index bac4df6..93242f2 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; @@ -42,6 +44,7 @@ trait BrowserTests /** * @test */ + #[Test] public function fails_if_trying_to_manipulate_exception_page(): void { Assert::that(function() { @@ -69,6 +72,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 +87,7 @@ public function can_enable_exception_throwing(): void /** * @test */ + #[Test] public function can_re_enable_catching_exceptions(): void { $browser = $this->browser(); @@ -105,6 +110,7 @@ public function can_re_enable_catching_exceptions(): void /** * @test */ + #[Test] public function exceptions_are_caught_by_default(): void { $this->browser() @@ -116,6 +122,7 @@ public function exceptions_are_caught_by_default(): void /** * @test */ + #[Test] public function fails_if_expected_exception_not_thrown(): void { // visit @@ -160,6 +167,7 @@ function() { /** * @test */ + #[Test] public function can_expect_exception_for_form_submit(): void { $this->browser() @@ -172,6 +180,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 +195,7 @@ public function can_expect_exception_for_link_click(): void /** * @test */ + #[Test] public function multiple_browsers(): void { $browser1 = $this->browser() @@ -205,6 +215,7 @@ public function multiple_browsers(): void /** * @test */ + #[Test] public function assert_on(): void { $this->browser() @@ -226,6 +237,8 @@ public function assert_on(): void * * @dataProvider encodedUrlProvider */ + #[Test] + #[DataProvider('encodedUrlProvider')] public function assert_on_encoded($url, $expected): void { $this->browser() @@ -249,6 +262,7 @@ public static function encodedUrlProvider(): iterable /** * @test */ + #[Test] public function can_use_current_browser(): void { $browser = $this->browser(); @@ -269,6 +283,7 @@ public function can_use_current_browser(): void /** * @test */ + #[Test] public function can_use_components(): void { $this->browser() @@ -282,6 +297,7 @@ public function can_use_components(): void /** * @test */ + #[Test] public function component_pre_assertions_and_actions_are_called(): void { $this->browser() @@ -295,6 +311,7 @@ public function component_pre_assertions_and_actions_are_called(): void /** * @test */ + #[Test] public function can_use_crawler(): void { $this->browser() @@ -308,6 +325,7 @@ public function can_use_crawler(): void /** * @test */ + #[Test] public function can_use_cookie_jar(): void { $this->browser() @@ -321,6 +339,7 @@ public function can_use_cookie_jar(): void /** * @test */ + #[Test] public function can_manipulate_cookies(): void { $expires = \time() + 3600; @@ -366,6 +385,7 @@ public function can_manipulate_cookies(): void /** * @test */ + #[Test] public function with_can_accept_multiple_browsers_and_components(): void { $browser = $this->browser(); @@ -385,6 +405,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 +416,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 +432,7 @@ public function following_redirect_follows_all_by_default(): void /** * @test */ + #[Test] public function can_re_enable_following_redirects(): void { $this->browser() @@ -425,6 +448,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 +466,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 +479,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 +502,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 +515,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 +528,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 +543,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 +558,7 @@ public function exception_thrown_if_asserting_redirected_to_and_not_intercepting /** * @test */ + #[Test] public function click_and_intercept(): void { $this->browser() @@ -544,6 +575,7 @@ public function click_and_intercept(): void /** * @test */ + #[Test] public function redirects_are_followed_by_default(): void { $this->browser() @@ -555,6 +587,7 @@ public function redirects_are_followed_by_default(): void /** * @test */ + #[Test] public function response_header_assertions(): void { $this->browser() @@ -568,6 +601,7 @@ public function response_header_assertions(): void /** * @test */ + #[Test] public function response_status_assertions(): void { $this->browser() @@ -581,6 +615,7 @@ public function response_status_assertions(): void /** * @test */ + #[Test] public function can_use_container_as_typehint(): void { $this->browser() @@ -594,6 +629,7 @@ public function can_use_container_as_typehint(): void /** * @test */ + #[Test] public function can_act_as_user(): void { $this->browser() @@ -606,6 +642,7 @@ public function can_act_as_user(): void /** * @test */ + #[Test] public function can_make_authentication_assertions(): void { $username = 'kevin'; @@ -626,6 +663,7 @@ public function can_make_authentication_assertions(): void /** * @test */ + #[Test] public function can_check_if_not_authenticated_after_request(): void { $this->browser() @@ -638,6 +676,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 +705,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 +720,7 @@ public function can_enable_the_profiler(): void /** * @test */ + #[Test] public function can_profile_multiple_requests(): void { $browser = $this->browser(); @@ -695,6 +736,7 @@ public function can_profile_multiple_requests(): void /** * @test */ + #[Test] public function can_access_the_profiler(): void { $profile = $this->browser() @@ -709,6 +751,7 @@ public function can_access_the_profiler(): void /** * @test */ + #[Test] public function can_use_data_collector(): void { $this->browser() @@ -723,6 +766,7 @@ public function can_use_data_collector(): void /** * @test */ + #[Test] public function content_assertions(): void { $this->browser() @@ -735,6 +779,7 @@ public function content_assertions(): void /** * @test */ + #[Test] public function can_dump_response(): void { $output = self::catchVarDumperOutput(function() { @@ -752,6 +797,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 +814,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 +833,7 @@ public function can_save_source_as_zip(): void /** * @test */ + #[Test] public function html_assertions(): void { $this->browser() @@ -803,6 +851,7 @@ public function html_assertions(): void /** * @test */ + #[Test] public function html_head_assertions(): void { $this->browser() @@ -817,6 +866,7 @@ public function html_head_assertions(): void /** * @test */ + #[Test] public function form_assertions(): void { $this->browser() @@ -862,6 +912,7 @@ public function form_assertions(): void /** * @test */ + #[Test] public function link_action(): void { $this->browser() @@ -874,6 +925,7 @@ public function link_action(): void /** * @test */ + #[Test] public function click_on_element(): void { $this->browser() @@ -886,6 +938,7 @@ public function click_on_element(): void /** * @test */ + #[Test] public function form_actions_by_field_label(): void { $this->browser() @@ -914,6 +967,7 @@ public function form_actions_by_field_label(): void /** * @test */ + #[Test] public function form_actions_by_field_id(): void { $this->browser() @@ -942,6 +996,7 @@ public function form_actions_by_field_id(): void /** * @test */ + #[Test] public function form_actions_by_field_name(): void { $this->browser() @@ -968,6 +1023,7 @@ public function form_actions_by_field_name(): void /** * @test */ + #[Test] public function select_field(): void { $this->browser() @@ -988,6 +1044,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 +1079,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 +1120,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 +1134,7 @@ public function cannot_attach_file_that_does_not_exist(): void /** * @test */ + #[Test] public function can_attach_multiple_files(): void { $this->browser() @@ -1088,6 +1148,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 +1162,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 +1179,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 +1197,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 +1212,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..5b28d98 100644 --- a/tests/PlaywrightBrowserTest.php +++ b/tests/PlaywrightBrowserTest.php @@ -12,6 +12,8 @@ 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; @@ -21,6 +23,7 @@ * * @group playwright */ +#[Group('playwright')] class PlaywrightBrowserTest extends KernelTestCase { use BrowserTests, HasBrowser; @@ -28,6 +31,7 @@ class PlaywrightBrowserTest extends KernelTestCase /** * @test */ + #[Test] public function can_use_playwright_browser_as_typehint(): void { $this->browser() @@ -41,6 +45,7 @@ public function can_use_playwright_browser_as_typehint(): void /** * @test */ + #[Test] public function can_take_screenshot(): void { self::catchFileContents(__DIR__.'/../var/browser/screenshots/screen.png', function() { @@ -54,6 +59,7 @@ public function can_take_screenshot(): void /** * @test */ + #[Test] public function can_wait(): void { $this->browser() @@ -67,6 +73,7 @@ public function can_wait(): void /** * @test */ + #[Test] public function can_wait_until_visible_and_not_visible(): void { $this->browser() @@ -95,6 +102,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 +132,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 +146,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 +164,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 +181,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 +200,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,6 +215,7 @@ public function cannot_follow_invisible_link(): void /** * @test */ + #[Test] public function double_click_on_element(): void { $this->browser() @@ -220,6 +234,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 From e0d7b34beb909475767e601cd98ca8088978eb64 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Mon, 24 Aug 2026 09:46:20 -0400 Subject: [PATCH 2/5] test: run the suite with paratest The bootstrap wiped var/ in every worker, so workers cleared each others cache and artifacts mid-run: only the parent cleans up now. A test was also renamed, since paratest filters each chunk with /name$/ and a name ending with another test name in the same class runs twice. --- .github/workflows/ci.yml | 1 + README.md | 23 +++++++++++++++++------ composer.json | 1 + tests/PlaywrightBrowserTest.php | 2 +- tests/bootstrap.php | 9 ++++++++- 5 files changed, 28 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b918b4e..8f2b4d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,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..98c708f 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,18 @@ 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. + +> [!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/composer.json b/composer.json index 92a3b2a..277677b 100644 --- a/composer.json +++ b/composer.json @@ -22,6 +22,7 @@ "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", diff --git a/tests/PlaywrightBrowserTest.php b/tests/PlaywrightBrowserTest.php index 5b28d98..6024ae6 100644 --- a/tests/PlaywrightBrowserTest.php +++ b/tests/PlaywrightBrowserTest.php @@ -216,7 +216,7 @@ public function cannot_follow_invisible_link(): void * @test */ #[Test] - public function double_click_on_element(): void + public function can_double_click_an_element(): void { $this->browser() ->visit('/page1') 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); From 00cb74a168c36619b22dcbe7d602318101d823ef Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Mon, 24 Aug 2026 09:54:42 -0400 Subject: [PATCH 3/5] chore: remove stray `attachment.zip` from the project root Committed by accident in #121 alongside the fixture it duplicates: only `tests/Fixture/files/attachment.zip` is read, by the `/zip` route. --- attachment.zip | Bin 182 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 attachment.zip diff --git a/attachment.zip b/attachment.zip deleted file mode 100644 index e6ebe532758411c348ef9485ecec1bb071e64f6e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 182 zcmWIWW@h1HW&nYXkTC5jGkQgUOc3S+;>41Y#N>?J)VvbCl8O=pPa!!!uLLMs%oX6x z$Yjrq%LEmm86cq0u%r=0BTN99$Hkxk5@cXxkN}b!lU!VHGBW5sN(R$Vet Date: Mon, 24 Aug 2026 10:28:07 -0400 Subject: [PATCH 4/5] test: enable and cover the browser extension The extension was never registered here, so nothing exercised it. The new tests drive the hooks phpunit calls, because saveBrowserStates() swallows every Throwable and a broken save would otherwise go unnoticed. --- README.md | 3 ++- phpunit.xml.dist | 6 +++++- tests/BrowserTests.php | 20 ++++++++++++++++++++ tests/PlaywrightBrowserTest.php | 20 ++++++++++++++++++++ 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 98c708f..98c1585 100644 --- a/README.md +++ b/README.md @@ -624,7 +624,8 @@ Both browsers work with [ParaTest](https://github.com/paratestphp/paratest). Eac 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. +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 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 93242f2..35e6d43 100644 --- a/tests/BrowserTests.php +++ b/tests/BrowserTests.php @@ -28,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; @@ -717,6 +718,25 @@ public function can_enable_the_profiler(): void $this->assertTrue($profile->hasCollector('request')); } + /** + * @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 */ diff --git a/tests/PlaywrightBrowserTest.php b/tests/PlaywrightBrowserTest.php index 6024ae6..0847b52 100644 --- a/tests/PlaywrightBrowserTest.php +++ b/tests/PlaywrightBrowserTest.php @@ -17,6 +17,7 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Browser\PlaywrightBrowser; use Zenstruck\Browser\Test\HasBrowser; +use Zenstruck\Browser\Test\LegacyExtension; /** * @author Kevin Bond @@ -42,6 +43,25 @@ 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 */ From f7df9e319358a77ca889f3f47abf65ed605e2189 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Mon, 24 Aug 2026 10:39:12 -0400 Subject: [PATCH 5/5] chore: drop EOL Symfony 8.0 from the ci matrix --- .github/workflows/ci.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f2b4d4..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