From a4cf7df5466239ce5f84fafad725669803c5cdde Mon Sep 17 00:00:00 2001 From: Aaron Gustavo Nieves Date: Fri, 26 Jun 2026 17:01:51 -0500 Subject: [PATCH 1/2] Fix moveBack() to move the history pointer instead of appending `moveBack()` replayed the previous request through `_loadPage()`, which called `clientRequest()` with the default `$changeHistory = true`. That appended a new entry to the BrowserKit history and discarded the forward entries, so going back behaved like a fresh navigation rather than a real Back button: the history pointer ended up on the last page again and a second `moveBack()` no longer walked further back. Thread a `$changeHistory` flag through `_loadPage()` (default `true`, so it is backward compatible) and pass `false` from `moveBack()`. The replayed request no longer mutates the history, matching Symfony's native `AbstractBrowser::back()` semantics. This also makes the framework history assertions (`assertBrowserHistoryIsNotOnLastPage()`) observable through the public `moveBack()` step. --- src/Codeception/Lib/InnerBrowser.php | 8 +++++--- tests/unit/Codeception/Module/FrameworksTest.php | 11 +++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Codeception/Lib/InnerBrowser.php b/src/Codeception/Lib/InnerBrowser.php index 22dd243..f7a855a 100644 --- a/src/Codeception/Lib/InnerBrowser.php +++ b/src/Codeception/Lib/InnerBrowser.php @@ -280,9 +280,10 @@ public function _loadPage( array $parameters = [], array $files = [], array $server = [], - ?string $content = null + ?string $content = null, + bool $changeHistory = true ): void { - $this->crawler = $this->clientRequest($method, $uri, $parameters, $files, $server, $content); + $this->crawler = $this->clientRequest($method, $uri, $parameters, $files, $server, $content, $changeHistory); $this->baseUrl = $this->retrieveBaseUrl(); $this->forms = []; } @@ -2006,7 +2007,8 @@ public function moveBack(int $numberOfSteps = 1): void $request->getParameters(), $request->getFiles(), $request->getServer(), - $request->getContent() + $request->getContent(), + false ); } diff --git a/tests/unit/Codeception/Module/FrameworksTest.php b/tests/unit/Codeception/Module/FrameworksTest.php index a1e0ec1..d83ec8d 100644 --- a/tests/unit/Codeception/Module/FrameworksTest.php +++ b/tests/unit/Codeception/Module/FrameworksTest.php @@ -68,6 +68,17 @@ public function testMoveBackTwoSteps() $this->module->seeCurrentUrlEquals('/iframe'); } + public function testMoveBackPreservesForwardHistory() + { + $this->module->amOnPage('/iframe'); + $this->module->amOnPage('/info'); + $this->module->amOnPage('/'); + $this->module->moveBack(); + $this->module->seeCurrentUrlEquals('/info'); + $this->module->moveBack(); + $this->module->seeCurrentUrlEquals('/iframe'); + } + public function testMoveBackThrowsExceptionIfNumberOfStepsIsInvalid() { $this->module->amOnPage('/iframe'); From da2befb476659113be19ed96db2c1ba68d2a9828 Mon Sep 17 00:00:00 2001 From: Aaron Gustavo Nieves Date: Fri, 26 Jun 2026 17:02:43 -0500 Subject: [PATCH 2/2] Add moveForward() to navigate forward in history Mirror of moveBack(): walks the BrowserKit history pointer forward N steps and replays the request with changeHistory = false, matching Symfony's native AbstractBrowser::forward(). Symfony exposes both back() and forward() with identical semantics, so InnerBrowser now offers the symmetric pair. This is purely additive (new public method) and is only usable now that moveBack() preserves the forward history entries instead of discarding them. Tests: testMoveForwardOneStep, testMoveForwardTwoSteps and testMoveForwardThrowsExceptionIfNumberOfStepsIsInvalid. The forward tests also assert that moveBack() kept the forward stack intact. --- src/Codeception/Lib/InnerBrowser.php | 37 ++++++++++++++++ .../Codeception/Module/FrameworksTest.php | 44 +++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/src/Codeception/Lib/InnerBrowser.php b/src/Codeception/Lib/InnerBrowser.php index f7a855a..bd45f14 100644 --- a/src/Codeception/Lib/InnerBrowser.php +++ b/src/Codeception/Lib/InnerBrowser.php @@ -2012,6 +2012,43 @@ public function moveBack(int $numberOfSteps = 1): void ); } + /** + * Moves forward in history. + * + * @param int $numberOfSteps (default value 1) + */ + public function moveForward(int $numberOfSteps = 1): void + { + $request = null; + if (!is_int($numberOfSteps) || $numberOfSteps < 1) { + throw new InvalidArgumentException('numberOfSteps must be positive integer'); + } + + try { + $history = $this->getRunningClient()->getHistory(); + for ($i = $numberOfSteps; $i > 0; --$i) { + $request = $history->forward(); + } + } catch (LogicException $exception) { + throw new InvalidArgumentException( + sprintf( + 'numberOfSteps is set to %d, but there are only %d forward steps in the history', + $numberOfSteps, + $numberOfSteps - $i + ), $exception->getCode(), $exception); + } + + $this->_loadPage( + $request->getMethod(), + $request->getUri(), + $request->getParameters(), + $request->getFiles(), + $request->getServer(), + $request->getContent(), + false + ); + } + protected function debugCookieJar(): void { $cookies = $this->client->getCookieJar()->all(); diff --git a/tests/unit/Codeception/Module/FrameworksTest.php b/tests/unit/Codeception/Module/FrameworksTest.php index d83ec8d..ac708f9 100644 --- a/tests/unit/Codeception/Module/FrameworksTest.php +++ b/tests/unit/Codeception/Module/FrameworksTest.php @@ -100,6 +100,50 @@ public function testMoveBackThrowsExceptionIfNumberOfStepsIsInvalid() } } + public function testMoveForwardOneStep() + { + $this->module->amOnPage('/iframe'); + $this->module->amOnPage('/info'); + $this->module->amOnPage('/'); + $this->module->moveBack(2); + $this->module->seeCurrentUrlEquals('/iframe'); + $this->module->moveForward(); + $this->module->seeCurrentUrlEquals('/info'); + $this->module->moveForward(); + $this->module->seeCurrentUrlEquals('/'); + } + + public function testMoveForwardTwoSteps() + { + $this->module->amOnPage('/iframe'); + $this->module->amOnPage('/info'); + $this->module->amOnPage('/'); + $this->module->moveBack(2); + $this->module->seeCurrentUrlEquals('/iframe'); + $this->module->moveForward(2); + $this->module->seeCurrentUrlEquals('/'); + } + + public function testMoveForwardThrowsExceptionIfNumberOfStepsIsInvalid() + { + $this->module->amOnPage('/iframe'); + $this->module->amOnPage('/'); + $this->module->moveBack(); + $this->module->seeCurrentUrlEquals('/iframe'); + + $invalidValues = [0, -5, 1.5, 'a', 3]; + foreach ($invalidValues as $invalidValue) { + try { + $this->module->moveForward($invalidValue); + $this->fail('Expected to get exception here'); + } catch (InvalidArgumentException $exception) { + codecept_debug('Exception: ' . $exception->getMessage()); + } catch (TypeError $error) { + codecept_debug('Error: ' . $error->getMessage()); + } + } + } + public function testCreateSnapshotOnFail() { $container = Stub::make(ModuleContainer::class);