Skip to content

Commit 4d45acd

Browse files
committed
@
Restore InnerBrowser navigation state for Codeception runs (#231) The 3.9.0 audit decoupled amOnRoute/amOnAction, seePageIsAvailable, seePageRedirectsTo and submitSymfonyForm from codeception/lib-innerbrowser so they can also run from plain PHPUnit test cases. They now drive the BrowserKit client directly (request()/submit()) and discard the returned Crawler. Under Codeception that broke downstream DOM assertions: only InnerBrowser::_loadPage() writes the Crawler back into the cached $crawler (and resets $baseUrl/$forms), so see(), click() and the form helpers ended up operating on a null or stale Crawler after those methods. Add InnerBrowserCompatibilityTrait, which keeps the decoupling but bridges the two runners: when the module is an InnerBrowser (Codeception) it delegates navigation/submission to amOnPage()/submitForm() so the cached state stays in sync; otherwise (PHPUnit) it keeps driving the client directly. The four methods now go through loadPage()/submitNamedForm(). A matching functional test still needs to land in Codeception/symfony-module-tests per CONTRIBUTING.md. @
1 parent bed4de2 commit 4d45acd

4 files changed

Lines changed: 200 additions & 16 deletions

File tree

src/Codeception/Module/Symfony/BrowserAssertionsTrait.php

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@
2222
use Symfony\Component\HttpFoundation\Test\Constraint\ResponseStatusCodeSame;
2323

2424
use function class_exists;
25-
use function count;
26-
use function sprintf;
2725

2826
trait BrowserAssertionsTrait
2927
{
28+
use InnerBrowserCompatibilityTrait;
29+
3030
/**
3131
* Asserts that the given cookie in the test client is set to the expected value.
3232
*
@@ -318,7 +318,7 @@ protected function doRebootClientKernel(): void {}
318318
public function seePageIsAvailable(?string $url = null): void
319319
{
320320
if ($url !== null) {
321-
$this->getClient()->request('GET', $url);
321+
$this->loadPage($url);
322322
$this->assertStringContainsString($url, $this->getClient()->getRequest()->getRequestUri());
323323
}
324324

@@ -337,7 +337,7 @@ public function seePageRedirectsTo(string $page, string $redirectsTo): void
337337
{
338338
$client = $this->getClient();
339339
$client->followRedirects(false);
340-
$client->request('GET', $page);
340+
$this->loadPage($page);
341341

342342
$this->assertThatForResponse(new ResponseIsRedirected(), 'The response is not a redirection.');
343343

@@ -364,17 +364,7 @@ public function seePageRedirectsTo(string $page, string $redirectsTo): void
364364
*/
365365
public function submitSymfonyForm(string $name, array $fields): void
366366
{
367-
$selector = sprintf('form[name=%s]', $name);
368-
369-
$params = [];
370-
foreach ($fields as $key => $value) {
371-
$params[$name . $key] = $value;
372-
}
373-
374-
$node = $this->getClient()->getCrawler()->filter($selector);
375-
$this->assertGreaterThan(0, count($node), sprintf('Form "%s" not found.', $selector));
376-
$form = $node->form();
377-
$this->getClient()->submit($form, $params);
367+
$this->submitNamedForm($name, $fields);
378368
}
379369

380370
protected function assertThatForClient(Constraint $constraint, string $message = ''): void
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Codeception\Module\Symfony;
6+
7+
use Codeception\Lib\InnerBrowser;
8+
use Symfony\Component\BrowserKit\AbstractBrowser;
9+
10+
use function sprintf;
11+
12+
/**
13+
* Bridges navigation and form submission between the two runners the module
14+
* supports.
15+
*
16+
* Most "see..." assertions read the response or the profiler, so they work the
17+
* same whether the methods are driven by Codeception or by plain PHPUnit.
18+
* Navigation is the exception: under Codeception the module *is* an
19+
* {@see InnerBrowser}, and only its `amOnPage()` / `submitForm()` path writes
20+
* the returned Crawler back into the cached `$crawler` (and resets `$baseUrl`
21+
* and `$forms`). Issuing the request straight through the BrowserKit client
22+
* skips that bookkeeping, leaving DOM helpers such as `see()`, `click()` or the
23+
* form steps operating on a null/stale Crawler.
24+
*
25+
* So when running inside Codeception we delegate to the InnerBrowser steps to
26+
* keep the cached state in sync; otherwise (PHPUnit, where the trait is used by
27+
* a TestCase that has no Crawler to maintain) we drive the client directly.
28+
*
29+
* @see https://github.com/Codeception/module-symfony/issues/231
30+
*
31+
* @internal
32+
*/
33+
trait InnerBrowserCompatibilityTrait
34+
{
35+
abstract protected function getClient(): AbstractBrowser;
36+
37+
/**
38+
* Opens a URL with a GET request, keeping the InnerBrowser Crawler in sync
39+
* when running under Codeception.
40+
*/
41+
protected function loadPage(string $url): void
42+
{
43+
$client = $this->getClient();
44+
45+
if ($this instanceof InnerBrowser) {
46+
$this->amOnPage($url);
47+
48+
return;
49+
}
50+
51+
$client->request('GET', $url);
52+
}
53+
54+
/**
55+
* Submits the form identified by its `name` attribute, prefixing every field
56+
* key with that name (so `'[email]'` targets `login_form[email]`).
57+
*
58+
* Under Codeception it goes through `submitForm()` so the Crawler is
59+
* refreshed with the resulting page; otherwise it submits the form object
60+
* straight through the BrowserKit client.
61+
*
62+
* @param array<string, mixed> $fields
63+
*/
64+
protected function submitNamedForm(string $name, array $fields): void
65+
{
66+
$client = $this->getClient();
67+
$selector = sprintf('form[name=%s]', $name);
68+
69+
$params = [];
70+
foreach ($fields as $key => $value) {
71+
$params[$name . $key] = $value;
72+
}
73+
74+
if ($this instanceof InnerBrowser) {
75+
$this->submitForm($selector, $params, sprintf('%s_submit', $name));
76+
77+
return;
78+
}
79+
80+
$node = $client->getCrawler()->filter($selector);
81+
$this->assertGreaterThan(0, $node->count(), sprintf('Form "%s" not found.', $selector));
82+
$form = $node->form();
83+
$client->submit($form, $params);
84+
}
85+
}

src/Codeception/Module/Symfony/RouterAssertionsTrait.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
trait RouterAssertionsTrait
1717
{
18+
use InnerBrowserCompatibilityTrait;
19+
1820
/**
1921
* Opens web page by action name
2022
*
@@ -157,7 +159,7 @@ private function assertRouteExists(string $routeName): void
157159
/** @param array<string, mixed> $params */
158160
private function openRoute(string $routeName, array $params = []): void
159161
{
160-
$this->getClient()->request('GET', $this->grabRouterService()->generate($routeName, $params));
162+
$this->loadPage($this->grabRouterService()->generate($routeName, $params));
161163
}
162164

163165
protected function grabRouterService(): RouterInterface
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests;
6+
7+
use Codeception\Lib\InnerBrowser;
8+
use Codeception\Module\Symfony\InnerBrowserCompatibilityTrait;
9+
use PHPUnit\Framework\TestCase;
10+
use Symfony\Component\BrowserKit\AbstractBrowser;
11+
use Symfony\Component\BrowserKit\Response;
12+
13+
/**
14+
* Guards the regression reported in issue #231: when the module runs under
15+
* Codeception it must drive navigation/submission through the InnerBrowser
16+
* steps (`amOnPage()` / `submitForm()`) so the cached Crawler is refreshed,
17+
* instead of issuing the request straight through the BrowserKit client.
18+
*
19+
* @see https://github.com/Codeception/module-symfony/issues/231
20+
*/
21+
final class InnerBrowserCompatibilityTest extends TestCase
22+
{
23+
public function testLoadPageDelegatesToAmOnPageUnderCodeception(): void
24+
{
25+
$browser = new FakeInnerBrowser();
26+
27+
$browser->callLoadPage('/dashboard');
28+
29+
$this->assertSame(['/dashboard'], $browser->amOnPageCalls);
30+
$this->assertSame([], $browser->submitFormCalls);
31+
}
32+
33+
public function testSubmitNamedFormDelegatesToSubmitFormUnderCodeception(): void
34+
{
35+
$browser = new FakeInnerBrowser();
36+
37+
$browser->callSubmitNamedForm('login_form', [
38+
'[email]' => 'john_doe@example.com',
39+
'[password]' => 'secretForest',
40+
]);
41+
42+
$this->assertSame([[
43+
'form[name=login_form]',
44+
[
45+
'login_form[email]' => 'john_doe@example.com',
46+
'login_form[password]' => 'secretForest',
47+
],
48+
'login_form_submit',
49+
]], $browser->submitFormCalls);
50+
$this->assertSame([], $browser->amOnPageCalls);
51+
}
52+
}
53+
54+
/**
55+
* Minimal InnerBrowser stand-in: makes `$this instanceof InnerBrowser` true and
56+
* records the steps the compatibility trait delegates to, without booting a
57+
* Codeception module.
58+
*/
59+
final class FakeInnerBrowser extends InnerBrowser
60+
{
61+
use InnerBrowserCompatibilityTrait;
62+
63+
/** @var list<string> */
64+
public array $amOnPageCalls = [];
65+
66+
/** @var list<array{0: mixed, 1: array<string, mixed>, 2: string|null}> */
67+
public array $submitFormCalls = [];
68+
69+
public function __construct()
70+
{
71+
// Intentionally bypass the Codeception\Module constructor: this stub only
72+
// exercises the trait's runner-detection branch, not the real module.
73+
}
74+
75+
public function amOnPage(string $page): void
76+
{
77+
$this->amOnPageCalls[] = $page;
78+
}
79+
80+
public function submitForm($selector, array $params, ?string $button = null): void
81+
{
82+
$this->submitFormCalls[] = [$selector, $params, $button];
83+
}
84+
85+
public function callLoadPage(string $url): void
86+
{
87+
$this->loadPage($url);
88+
}
89+
90+
/** @param array<string, mixed> $fields */
91+
public function callSubmitNamedForm(string $name, array $fields): void
92+
{
93+
$this->submitNamedForm($name, $fields);
94+
}
95+
96+
protected function getClient(): AbstractBrowser
97+
{
98+
// Captured by the trait before the runner branch, but unused on the
99+
// Codeception delegation path.
100+
return new class extends AbstractBrowser {
101+
protected function doRequest(object $request): object
102+
{
103+
return new Response('');
104+
}
105+
};
106+
}
107+
}

0 commit comments

Comments
 (0)