From e215c6b747fa0843e3b754695e07e53d4fd1c714 Mon Sep 17 00:00:00 2001 From: Pascal CESCON - Amoifr Date: Tue, 1 Sep 2026 12:43:58 +0200 Subject: [PATCH 1/3] Keep the query string when comparing the current address --- src/WebAssert.php | 3 ++- tests/WebAssertTest.php | 44 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/WebAssert.php b/src/WebAssert.php index 05e13b9b7..7a9f5831c 100644 --- a/src/WebAssert.php +++ b/src/WebAssert.php @@ -863,10 +863,11 @@ protected function getCurrentUrlPath() protected function cleanUrl(string $url) { $parts = parse_url($url); + $query = empty($parts['query']) ? '' : '?'.$parts['query']; $fragment = empty($parts['fragment']) ? '' : '#'.$parts['fragment']; $path = empty($parts['path']) ? '/' : $parts['path']; - return preg_replace('/^\/[^\.\/]+\.php\//', '/', $path).$fragment; + return preg_replace('/^\/[^\.\/]+\.php\//', '/', $path).$query.$fragment; } /** diff --git a/tests/WebAssertTest.php b/tests/WebAssertTest.php index 9011bc502..446f37cbc 100644 --- a/tests/WebAssertTest.php +++ b/tests/WebAssertTest.php @@ -46,14 +46,54 @@ public function testAddressEquals() ; $this->assertCorrectAssertion(function () { - $this->assert->addressEquals('/sub/url#webapp/nav'); + $this->assert->addressEquals('/sub/url?param=true#webapp/nav'); }); $this->assertWrongAssertion( function () { $this->assert->addressEquals('sub_url'); }, 'Behat\\Mink\\Exception\\ExpectationException', - 'Current page is "/sub/url#webapp/nav", but "sub_url" expected.' + 'Current page is "/sub/url?param=true#webapp/nav", but "sub_url" expected.' + ); + } + + public function testAddressEqualsWithQueryString() + { + $this->session + ->expects($this->exactly(2)) + ->method('getCurrentUrl') + ->will($this->returnValue('http://example.com/login?return_url=/user')) + ; + + $this->assertCorrectAssertion(function () { + $this->assert->addressEquals('/login?return_url=/user'); + }); + $this->assertWrongAssertion( + function () { + $this->assert->addressEquals('/login'); + }, + 'Behat\\Mink\\Exception\\ExpectationException', + 'Current page is "/login?return_url=/user", but "/login" expected.' + ); + } + + public function testAddressNotEqualsWithQueryString() + { + $this->session + ->expects($this->exactly(2)) + ->method('getCurrentUrl') + ->will($this->returnValue('http://example.com/login?return_url=/user')) + ; + + $this->assertCorrectAssertion(function () { + $this->assert->addressNotEquals('/login?return_url=/admin'); + }); + $this->assertWrongAssertion( + function () { + $this->assert->addressNotEquals('/login?return_url=/user'); + }, + 'Behat\\Mink\\Exception\\ExpectationException', + 'Current page is "/login?return_url=/user", but should not be.' ); } From 9b0700160e9e3082365c452b8184ad314fee2a7c Mon Sep 17 00:00:00 2001 From: Pascal CESCON - Amoifr Date: Tue, 1 Sep 2026 22:09:38 +0200 Subject: [PATCH 2/3] Make the query string comparison opt-in --- src/WebAssert.php | 13 +++++++++++-- tests/WebAssertTest.php | 24 ++++++++++++++---------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/WebAssert.php b/src/WebAssert.php index 7a9f5831c..c9bf858fe 100644 --- a/src/WebAssert.php +++ b/src/WebAssert.php @@ -32,14 +32,23 @@ class WebAssert */ protected $session; + /** + * @var bool + */ + private $compareQueryString; + /** * Initializes assertion engine. * * @param Session $session + * @param bool $compareQueryString Whether the address assertions take the query string into + * account. Off by default, so suites that have always compared + * the path alone keep working. */ - public function __construct(Session $session) + public function __construct(Session $session, bool $compareQueryString = false) { $this->session = $session; + $this->compareQueryString = $compareQueryString; } /** @@ -863,7 +872,7 @@ protected function getCurrentUrlPath() protected function cleanUrl(string $url) { $parts = parse_url($url); - $query = empty($parts['query']) ? '' : '?'.$parts['query']; + $query = ($this->compareQueryString && !empty($parts['query'])) ? '?'.$parts['query'] : ''; $fragment = empty($parts['fragment']) ? '' : '#'.$parts['fragment']; $path = empty($parts['path']) ? '/' : $parts['path']; diff --git a/tests/WebAssertTest.php b/tests/WebAssertTest.php index 446f37cbc..2c0e70453 100644 --- a/tests/WebAssertTest.php +++ b/tests/WebAssertTest.php @@ -46,31 +46,33 @@ public function testAddressEquals() ; $this->assertCorrectAssertion(function () { - $this->assert->addressEquals('/sub/url?param=true#webapp/nav'); + $this->assert->addressEquals('/sub/url#webapp/nav'); }); $this->assertWrongAssertion( function () { $this->assert->addressEquals('sub_url'); }, 'Behat\\Mink\\Exception\\ExpectationException', - 'Current page is "/sub/url?param=true#webapp/nav", but "sub_url" expected.' + 'Current page is "/sub/url#webapp/nav", but "sub_url" expected.' ); } public function testAddressEqualsWithQueryString() { + $assert = new WebAssert($this->session, true); + $this->session ->expects($this->exactly(2)) ->method('getCurrentUrl') ->will($this->returnValue('http://example.com/login?return_url=/user')) ; - $this->assertCorrectAssertion(function () { - $this->assert->addressEquals('/login?return_url=/user'); + $this->assertCorrectAssertion(function () use ($assert) { + $assert->addressEquals('/login?return_url=/user'); }); $this->assertWrongAssertion( - function () { - $this->assert->addressEquals('/login'); + function () use ($assert) { + $assert->addressEquals('/login'); }, 'Behat\\Mink\\Exception\\ExpectationException', 'Current page is "/login?return_url=/user", but "/login" expected.' @@ -79,18 +81,20 @@ function () { public function testAddressNotEqualsWithQueryString() { + $assert = new WebAssert($this->session, true); + $this->session ->expects($this->exactly(2)) ->method('getCurrentUrl') ->will($this->returnValue('http://example.com/login?return_url=/user')) ; - $this->assertCorrectAssertion(function () { - $this->assert->addressNotEquals('/login?return_url=/admin'); + $this->assertCorrectAssertion(function () use ($assert) { + $assert->addressNotEquals('/login?return_url=/admin'); }); $this->assertWrongAssertion( - function () { - $this->assert->addressNotEquals('/login?return_url=/user'); + function () use ($assert) { + $assert->addressNotEquals('/login?return_url=/user'); }, 'Behat\\Mink\\Exception\\ExpectationException', 'Current page is "/login?return_url=/user", but should not be.' From ea227355f95cef1530ebe4974c152a4e048e4e17 Mon Sep 17 00:00:00 2001 From: Pascal CESCON - Amoifr Date: Wed, 2 Sep 2026 11:16:15 +0200 Subject: [PATCH 3/3] Move the query string option to the assertion methods A project can need both behaviours in different places, so addressEquals(), addressNotEquals() and addressMatches() take the flag and the constructor goes back to its previous signature. cleanUrl() and getCurrentUrlPath() keep their exact signatures, since adding an optional argument to a protected method is a fatal error for any subclass that overrides it. With the flag off an assertion still goes through getCurrentUrlPath(); with it on, a private helper calls cleanUrl() and splices the query back in front of the fragment. --- src/WebAssert.php | 80 ++++++++++++++++++++++++++++++----------- tests/WebAssertTest.php | 66 +++++++++++++++++++++++++++------- 2 files changed, 114 insertions(+), 32 deletions(-) diff --git a/src/WebAssert.php b/src/WebAssert.php index c9bf858fe..c47440ba6 100644 --- a/src/WebAssert.php +++ b/src/WebAssert.php @@ -32,38 +32,31 @@ class WebAssert */ protected $session; - /** - * @var bool - */ - private $compareQueryString; - /** * Initializes assertion engine. * * @param Session $session - * @param bool $compareQueryString Whether the address assertions take the query string into - * account. Off by default, so suites that have always compared - * the path alone keep working. */ - public function __construct(Session $session, bool $compareQueryString = false) + public function __construct(Session $session) { $this->session = $session; - $this->compareQueryString = $compareQueryString; } /** * Checks that current session address is equals to provided one. * * @param string $page + * @param bool $compareQueryString Whether the query string takes part in the comparison. + * Off by default, so suites comparing the path alone keep working. * * @return void * * @throws ExpectationException */ - public function addressEquals(string $page) + public function addressEquals(string $page, bool $compareQueryString = false) { - $expected = $this->cleanUrl($page); - $actual = $this->getCurrentUrlPath(); + $expected = $this->cleanAddress($page, $compareQueryString); + $actual = $this->getCurrentAddress($compareQueryString); $this->assert($actual === $expected, sprintf('Current page is "%s", but "%s" expected.', $actual, $expected)); } @@ -72,15 +65,17 @@ public function addressEquals(string $page) * Checks that current session address is not equals to provided one. * * @param string $page + * @param bool $compareQueryString Whether the query string takes part in the comparison. + * Off by default, so suites comparing the path alone keep working. * * @return void * * @throws ExpectationException */ - public function addressNotEquals(string $page) + public function addressNotEquals(string $page, bool $compareQueryString = false) { - $expected = $this->cleanUrl($page); - $actual = $this->getCurrentUrlPath(); + $expected = $this->cleanAddress($page, $compareQueryString); + $actual = $this->getCurrentAddress($compareQueryString); $this->assert($actual !== $expected, sprintf('Current page is "%s", but should not be.', $actual)); } @@ -89,14 +84,16 @@ public function addressNotEquals(string $page) * Checks that current session address matches regex. * * @param string $regex + * @param bool $compareQueryString Whether the query string is part of the address the regex is + * matched against. Off by default. * * @return void * * @throws ExpectationException */ - public function addressMatches(string $regex) + public function addressMatches(string $regex, bool $compareQueryString = false) { - $actual = $this->getCurrentUrlPath(); + $actual = $this->getCurrentAddress($compareQueryString); $message = sprintf('Current page "%s" does not match the regex "%s".', $actual, $regex); $this->assert((bool) preg_match($regex, $actual), $message); @@ -872,11 +869,54 @@ protected function getCurrentUrlPath() protected function cleanUrl(string $url) { $parts = parse_url($url); - $query = ($this->compareQueryString && !empty($parts['query'])) ? '?'.$parts['query'] : ''; $fragment = empty($parts['fragment']) ? '' : '#'.$parts['fragment']; $path = empty($parts['path']) ? '/' : $parts['path']; - return preg_replace('/^\/[^\.\/]+\.php\//', '/', $path).$query.$fragment; + return preg_replace('/^\/[^\.\/]+\.php\//', '/', $path).$fragment; + } + + /** + * Gets the current address the way getCurrentUrlPath() does, optionally keeping its query string. + * + * Without the query string this is getCurrentUrlPath() itself, so a subclass overriding it keeps + * deciding what the address looks like for every assertion written before this option existed. + */ + private function getCurrentAddress(bool $compareQueryString): string + { + if (!$compareQueryString) { + return $this->getCurrentUrlPath(); + } + + return $this->cleanAddress($this->session->getCurrentUrl(), true); + } + + /** + * Cleans the URL the way cleanUrl() does, optionally keeping its query string. + * + * The query is spliced back in front of the fragment rather than parsed here, so that a + * subclass overriding cleanUrl() keeps deciding what the rest of the address looks like. + */ + private function cleanAddress(string $url, bool $compareQueryString): string + { + $address = $this->cleanUrl($url); + + if (!$compareQueryString) { + return $address; + } + + $query = parse_url($url, \PHP_URL_QUERY); + + if (!\is_string($query) || '' === $query) { + return $address; + } + + $fragmentPosition = strpos($address, '#'); + + if (false === $fragmentPosition) { + return $address.'?'.$query; + } + + return substr($address, 0, $fragmentPosition).'?'.$query.substr($address, $fragmentPosition); } /** diff --git a/tests/WebAssertTest.php b/tests/WebAssertTest.php index 2c0e70453..741264a22 100644 --- a/tests/WebAssertTest.php +++ b/tests/WebAssertTest.php @@ -59,48 +59,90 @@ function () { public function testAddressEqualsWithQueryString() { - $assert = new WebAssert($this->session, true); - $this->session ->expects($this->exactly(2)) ->method('getCurrentUrl') ->will($this->returnValue('http://example.com/login?return_url=/user')) ; - $this->assertCorrectAssertion(function () use ($assert) { - $assert->addressEquals('/login?return_url=/user'); + $this->assertCorrectAssertion(function () { + $this->assert->addressEquals('/login?return_url=/user', true); }); $this->assertWrongAssertion( - function () use ($assert) { - $assert->addressEquals('/login'); + function () { + $this->assert->addressEquals('/login', true); }, 'Behat\\Mink\\Exception\\ExpectationException', 'Current page is "/login?return_url=/user", but "/login" expected.' ); } - public function testAddressNotEqualsWithQueryString() + public function testAddressEqualsIgnoresTheQueryStringByDefault() + { + $this->session + ->expects($this->once()) + ->method('getCurrentUrl') + ->will($this->returnValue('http://example.com/login?return_url=/user')) + ; + + $this->assertCorrectAssertion(function () { + $this->assert->addressEquals('/login'); + }); + } + + public function testAddressEqualsKeepsTheQueryStringInFrontOfTheFragment() { - $assert = new WebAssert($this->session, true); + $this->session + ->expects($this->once()) + ->method('getCurrentUrl') + ->will($this->returnValue('http://example.com/script.php/sub/url?param=true#webapp/nav')) + ; + + $this->assertCorrectAssertion(function () { + $this->assert->addressEquals('/sub/url?param=true#webapp/nav', true); + }); + } + public function testAddressNotEqualsWithQueryString() + { $this->session ->expects($this->exactly(2)) ->method('getCurrentUrl') ->will($this->returnValue('http://example.com/login?return_url=/user')) ; - $this->assertCorrectAssertion(function () use ($assert) { - $assert->addressNotEquals('/login?return_url=/admin'); + $this->assertCorrectAssertion(function () { + $this->assert->addressNotEquals('/login?return_url=/admin', true); }); $this->assertWrongAssertion( - function () use ($assert) { - $assert->addressNotEquals('/login?return_url=/user'); + function () { + $this->assert->addressNotEquals('/login?return_url=/user', true); }, 'Behat\\Mink\\Exception\\ExpectationException', 'Current page is "/login?return_url=/user", but should not be.' ); } + public function testAddressMatchesWithQueryString() + { + $this->session + ->expects($this->exactly(2)) + ->method('getCurrentUrl') + ->will($this->returnValue('http://example.com/login?return_url=/user')) + ; + + $this->assertCorrectAssertion(function () { + $this->assert->addressMatches('/return_url/', true); + }); + $this->assertWrongAssertion( + function () { + $this->assert->addressMatches('/return_url/'); + }, + 'Behat\\Mink\\Exception\\ExpectationException', + 'Current page "/login" does not match the regex "/return_url/".' + ); + } + public function testAddressEqualsEmptyPath() { $this->session