Skip to content

Commit c7d6231

Browse files
[DomCrawler] Add DomCrawler to leverage PHP 8.4's HTML5-compliant DOM parser
1 parent b223c40 commit c7d6231

29 files changed

+690
-245
lines changed

UPGRADE-7.4.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ Console
1818

1919
* Deprecate `Symfony\Component\Console\Application::add()` in favor of `addCommand()`
2020

21+
BrowserKit
22+
----------
23+
24+
* Leverage the native HTML5 parser when using PHP 8.4+
25+
2126
DependencyInjection
2227
-------------------
2328

@@ -29,6 +34,16 @@ DoctrineBridge
2934

3035
* Deprecate `UniqueEntity::getRequiredOptions()` and `UniqueEntity::getDefaultOption()`
3136

37+
DomCrawler
38+
----------
39+
40+
* [BC BREAK] Type widening on public and protected methods/properties to support both
41+
`Dom\*` and `DOM*` native classes for:
42+
* properties `FormField::$document`, `$xpath`, `$node` and method `getLabel()`
43+
* methods `Form::getFormNode()` and `addField()`
44+
* property `AbstractUriElement::$node`, and methods `getNode()` and `setNode()`
45+
* methods `Crawler::add()`, `addDocument()`, `addNodeList()`, `addNode()`, `getNode()` and `sibling()`
46+
3247
FrameworkBundle
3348
---------------
3449

src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ApiAttributesTest.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use PHPUnit\Framework\Attributes\DataProvider;
1515
use Symfony\Component\DomCrawler\Crawler;
16+
use Symfony\Component\DomCrawler\DomCrawler;
1617
use Symfony\Component\HttpFoundation\JsonResponse;
1718
use Symfony\Component\HttpFoundation\Request;
1819
use Symfony\Component\HttpFoundation\Response;
@@ -426,7 +427,7 @@ public static function mapRequestPayloadProvider(): iterable
426427
</request>
427428
XML,
428429
'responseAssertion' => static function (string $response) {
429-
$crawler = new Crawler($response);
430+
$crawler = \PHP_VERSION_ID >= 80400 && class_exists(DomCrawler::class) ? new DomCrawler($response) : new Crawler($response);
430431

431432
self::assertSame('https://symfony.com/errors/validation', $crawler->filterXPath('response/type')->text());
432433
self::assertSame('Validation Failed', $crawler->filterXPath('response/title')->text());
@@ -642,7 +643,7 @@ public static function mapRequestPayloadProvider(): iterable
642643
</request>
643644
XML,
644645
'responseAssertion' => static function (string $response) {
645-
$crawler = new Crawler($response);
646+
$crawler = \PHP_VERSION_ID >= 80400 && class_exists(DomCrawler::class) ? new DomCrawler($response) : new Crawler($response);
646647

647648
self::assertSame('https://symfony.com/errors/validation', $crawler->filterXPath('response/type')->text());
648649
self::assertSame('Validation Failed', $crawler->filterXPath('response/title')->text());
@@ -860,7 +861,7 @@ public static function mapRequestPayloadProvider(): iterable
860861
</request>
861862
XML,
862863
'responseAssertion' => static function (string $response) {
863-
$crawler = new Crawler($response);
864+
$crawler = \PHP_VERSION_ID >= 80400 && class_exists(DomCrawler::class) ? new DomCrawler($response) : new Crawler($response);
864865

865866
self::assertSame('https://symfony.com/errors/validation', $crawler->filterXPath('response/type')->text());
866867
self::assertSame('Validation Failed', $crawler->filterXPath('response/title')->text());

src/Symfony/Bundle/FrameworkBundle/Tests/Test/WebTestCaseTest.php

Lines changed: 65 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Symfony\Component\BrowserKit\CookieJar;
2424
use Symfony\Component\BrowserKit\History;
2525
use Symfony\Component\DomCrawler\Crawler;
26+
use Symfony\Component\DomCrawler\DomCrawler;
2627
use Symfony\Component\HttpFoundation\Cookie as HttpFoundationCookie;
2728
use Symfony\Component\HttpFoundation\Request;
2829
use Symfony\Component\HttpFoundation\Response;
@@ -231,126 +232,156 @@ public function testAssertBrowserHistoryIsNotOnLastPage()
231232

232233
public function testAssertSelectorExists()
233234
{
234-
$this->getCrawlerTester(new Crawler('<html><body><h1>'))->assertSelectorExists('body > h1');
235+
$crawler = \PHP_VERSION_ID >= 80400 && class_exists(DomCrawler::class) ? DomCrawler::class : Crawler::class;
236+
237+
$this->getCrawlerTester(new $crawler('<html><body><h1>'))->assertSelectorExists('body > h1');
235238
$this->expectException(AssertionFailedError::class);
236239
$this->expectExceptionMessage('matches selector "body > h1".');
237-
$this->getCrawlerTester(new Crawler('<html><head><title>Foo'))->assertSelectorExists('body > h1');
240+
$this->getCrawlerTester(new $crawler('<html><head><title>Foo'))->assertSelectorExists('body > h1');
238241
}
239242

240243
public function testAssertSelectorNotExists()
241244
{
242-
$this->getCrawlerTester(new Crawler('<html><head><title>Foo'))->assertSelectorNotExists('body > h1');
245+
$crawler = \PHP_VERSION_ID >= 80400 && class_exists(DomCrawler::class) ? DomCrawler::class : Crawler::class;
246+
247+
$this->getCrawlerTester(new $crawler('<html><head><title>Foo'))->assertSelectorNotExists('body > h1');
243248
$this->expectException(AssertionFailedError::class);
244249
$this->expectExceptionMessage('does not match selector "body > h1".');
245-
$this->getCrawlerTester(new Crawler('<html><body><h1>'))->assertSelectorNotExists('body > h1');
250+
$this->getCrawlerTester(new $crawler('<html><body><h1>'))->assertSelectorNotExists('body > h1');
246251
}
247252

248253
public function testAssertSelectorCount()
249254
{
250-
$this->getCrawlerTester(new Crawler('<html><body><p>Hello</p></body></html>'))->assertSelectorCount(1, 'p');
251-
$this->getCrawlerTester(new Crawler('<html><body><p>Hello</p><p>Foo</p></body></html>'))->assertSelectorCount(2, 'p');
252-
$this->getCrawlerTester(new Crawler('<html><body><h1>This is not a paragraph.</h1></body></html>'))->assertSelectorCount(0, 'p');
255+
$crawler = \PHP_VERSION_ID >= 80400 && class_exists(DomCrawler::class) ? DomCrawler::class : Crawler::class;
256+
257+
$this->getCrawlerTester(new $crawler('<html><body><p>Hello</p></body></html>'))->assertSelectorCount(1, 'p');
258+
$this->getCrawlerTester(new $crawler('<html><body><p>Hello</p><p>Foo</p></body></html>'))->assertSelectorCount(2, 'p');
259+
$this->getCrawlerTester(new $crawler('<html><body><h1>This is not a paragraph.</h1></body></html>'))->assertSelectorCount(0, 'p');
253260
$this->expectException(AssertionFailedError::class);
254261
$this->expectExceptionMessage('Failed asserting that the Crawler selector "p" was expected to be found 0 time(s) but was found 1 time(s).');
255-
$this->getCrawlerTester(new Crawler('<html><body><p>Hello</p></body></html>'))->assertSelectorCount(0, 'p');
262+
$this->getCrawlerTester(new $crawler('<html><body><p>Hello</p></body></html>'))->assertSelectorCount(0, 'p');
256263
}
257264

258265
public function testAssertSelectorTextNotContains()
259266
{
260-
$this->getCrawlerTester(new Crawler('<html><body><h1>Foo'))->assertSelectorTextNotContains('body > h1', 'Bar');
267+
$crawler = \PHP_VERSION_ID >= 80400 && class_exists(DomCrawler::class) ? DomCrawler::class : Crawler::class;
268+
269+
$this->getCrawlerTester(new $crawler('<html><body><h1>Foo'))->assertSelectorTextNotContains('body > h1', 'Bar');
261270
$this->expectException(AssertionFailedError::class);
262271
$this->expectExceptionMessage('matches selector "body > h1" and the text "Foo" of the node matching selector "body > h1" does not contain "Foo".');
263-
$this->getCrawlerTester(new Crawler('<html><body><h1>Foo'))->assertSelectorTextNotContains('body > h1', 'Foo');
272+
$this->getCrawlerTester(new $crawler('<html><body><h1>Foo'))->assertSelectorTextNotContains('body > h1', 'Foo');
264273
}
265274

266275
public function testAssertAnySelectorTextContains()
267276
{
268-
$this->getCrawlerTester(new Crawler('<ul><li>Bar</li><li>Foo Baz'))->assertAnySelectorTextContains('ul li', 'Foo');
277+
$crawler = \PHP_VERSION_ID >= 80400 && class_exists(DomCrawler::class) ? DomCrawler::class : Crawler::class;
278+
279+
$this->getCrawlerTester(new $crawler('<ul><li>Bar</li><li>Foo Baz'))->assertAnySelectorTextContains('ul li', 'Foo');
269280
$this->expectException(AssertionFailedError::class);
270281
$this->expectExceptionMessage('matches selector "ul li" and the text of any node matching selector "ul li" contains "Foo".');
271-
$this->getCrawlerTester(new Crawler('<ul><li>Bar</li><li>Baz'))->assertAnySelectorTextContains('ul li', 'Foo');
282+
$this->getCrawlerTester(new $crawler('<ul><li>Bar</li><li>Baz'))->assertAnySelectorTextContains('ul li', 'Foo');
272283
}
273284

274285
public function testAssertAnySelectorTextSame()
275286
{
276-
$this->getCrawlerTester(new Crawler('<ul><li>Bar</li><li>Foo'))->assertAnySelectorTextSame('ul li', 'Foo');
287+
$crawler = \PHP_VERSION_ID >= 80400 && class_exists(DomCrawler::class) ? DomCrawler::class : Crawler::class;
288+
289+
$this->getCrawlerTester(new $crawler('<ul><li>Bar</li><li>Foo'))->assertAnySelectorTextSame('ul li', 'Foo');
277290
$this->expectException(AssertionFailedError::class);
278291
$this->expectExceptionMessage('matches selector "ul li" and has at least a node matching selector "ul li" with content "Foo".');
279-
$this->getCrawlerTester(new Crawler('<ul><li>Bar</li><li>Baz'))->assertAnySelectorTextSame('ul li', 'Foo');
292+
$this->getCrawlerTester(new $crawler('<ul><li>Bar</li><li>Baz'))->assertAnySelectorTextSame('ul li', 'Foo');
280293
}
281294

282295
public function testAssertAnySelectorTextNotContains()
283296
{
284-
$this->getCrawlerTester(new Crawler('<ul><li>Bar</li><li>Baz'))->assertAnySelectorTextNotContains('ul li', 'Foo');
297+
$crawler = \PHP_VERSION_ID >= 80400 && class_exists(DomCrawler::class) ? DomCrawler::class : Crawler::class;
298+
299+
$this->getCrawlerTester(new $crawler('<ul><li>Bar</li><li>Baz'))->assertAnySelectorTextNotContains('ul li', 'Foo');
285300
$this->expectException(AssertionFailedError::class);
286301
$this->expectExceptionMessage('matches selector "ul li" and the text of any node matching selector "ul li" does not contain "Foo".');
287-
$this->getCrawlerTester(new Crawler('<ul><li>Bar</li><li>Foo'))->assertAnySelectorTextNotContains('ul li', 'Foo');
302+
$this->getCrawlerTester(new $crawler('<ul><li>Bar</li><li>Foo'))->assertAnySelectorTextNotContains('ul li', 'Foo');
288303
}
289304

290305
public function testAssertPageTitleSame()
291306
{
292-
$this->getCrawlerTester(new Crawler('<html><head><title>Foo'))->assertPageTitleSame('Foo');
307+
$crawler = \PHP_VERSION_ID >= 80400 && class_exists(DomCrawler::class) ? DomCrawler::class : Crawler::class;
308+
309+
$this->getCrawlerTester(new $crawler('<html><head><title>Foo'))->assertPageTitleSame('Foo');
293310
$this->expectException(AssertionFailedError::class);
294311
$this->expectExceptionMessage('matches selector "title" and has a node matching selector "title" with content "Bar".');
295-
$this->getCrawlerTester(new Crawler('<html><head><title>Foo'))->assertPageTitleSame('Bar');
312+
$this->getCrawlerTester(new $crawler('<html><head><title>Foo'))->assertPageTitleSame('Bar');
296313
}
297314

298315
public function testAssertPageTitleContains()
299316
{
300-
$this->getCrawlerTester(new Crawler('<html><head><title>Foobar'))->assertPageTitleContains('Foo');
317+
$crawler = \PHP_VERSION_ID >= 80400 && class_exists(DomCrawler::class) ? DomCrawler::class : Crawler::class;
318+
319+
$this->getCrawlerTester(new $crawler('<html><head><title>Foobar'))->assertPageTitleContains('Foo');
301320
$this->expectException(AssertionFailedError::class);
302321
$this->expectExceptionMessage('matches selector "title" and the text "Foo" of the node matching selector "title" contains "Bar".');
303-
$this->getCrawlerTester(new Crawler('<html><head><title>Foo'))->assertPageTitleContains('Bar');
322+
$this->getCrawlerTester(new $crawler('<html><head><title>Foo'))->assertPageTitleContains('Bar');
304323
}
305324

306325
public function testAssertInputValueSame()
307326
{
308-
$this->getCrawlerTester(new Crawler('<html><body><form><input type="text" name="username" value="Fabien">'))->assertInputValueSame('username', 'Fabien');
327+
$crawler = \PHP_VERSION_ID >= 80400 && class_exists(DomCrawler::class) ? DomCrawler::class : Crawler::class;
328+
329+
$this->getCrawlerTester(new $crawler('<html><body><form><input type="text" name="username" value="Fabien">'))->assertInputValueSame('username', 'Fabien');
309330
$this->expectException(AssertionFailedError::class);
310331
$this->expectExceptionMessage('matches selector "input[name="password"]" and has a node matching selector "input[name="password"]" with attribute "value" of value "pa$$".');
311-
$this->getCrawlerTester(new Crawler('<html><head><title>Foo'))->assertInputValueSame('password', 'pa$$');
332+
$this->getCrawlerTester(new $crawler('<html><head><title>Foo'))->assertInputValueSame('password', 'pa$$');
312333
}
313334

314335
public function testAssertInputValueNotSame()
315336
{
316-
$this->getCrawlerTester(new Crawler('<html><body><input type="text" name="username" value="Helene">'))->assertInputValueNotSame('username', 'Fabien');
337+
$crawler = \PHP_VERSION_ID >= 80400 && class_exists(DomCrawler::class) ? DomCrawler::class : Crawler::class;
338+
339+
$this->getCrawlerTester(new $crawler('<html><body><input type="text" name="username" value="Helene">'))->assertInputValueNotSame('username', 'Fabien');
317340
$this->expectException(AssertionFailedError::class);
318341
$this->expectExceptionMessage('matches selector "input[name="password"]" and does not have a node matching selector "input[name="password"]" with attribute "value" of value "pa$$".');
319-
$this->getCrawlerTester(new Crawler('<html><body><form><input type="text" name="password" value="pa$$">'))->assertInputValueNotSame('password', 'pa$$');
342+
$this->getCrawlerTester(new $crawler('<html><body><form><input type="text" name="password" value="pa$$">'))->assertInputValueNotSame('password', 'pa$$');
320343
}
321344

322345
public function testAssertCheckboxChecked()
323346
{
324-
$this->getCrawlerTester(new Crawler('<html><body><form><input type="checkbox" name="rememberMe" checked>'))->assertCheckboxChecked('rememberMe');
325-
$this->getCrawlerTester(new Crawler('<!DOCTYPE html><body><form><input type="checkbox" name="rememberMe" checked>'))->assertCheckboxChecked('rememberMe');
347+
$crawler = \PHP_VERSION_ID >= 80400 && class_exists(DomCrawler::class) ? DomCrawler::class : Crawler::class;
348+
349+
$this->getCrawlerTester(new $crawler('<html><body><form><input type="checkbox" name="rememberMe" checked>'))->assertCheckboxChecked('rememberMe');
350+
$this->getCrawlerTester(new $crawler('<!DOCTYPE html><body><form><input type="checkbox" name="rememberMe" checked>'))->assertCheckboxChecked('rememberMe');
326351
$this->expectException(AssertionFailedError::class);
327352
$this->expectExceptionMessage('matches selector "input[name="rememberMe"]:checked".');
328-
$this->getCrawlerTester(new Crawler('<html><body><form><input type="checkbox" name="rememberMe">'))->assertCheckboxChecked('rememberMe');
353+
$this->getCrawlerTester(new $crawler('<html><body><form><input type="checkbox" name="rememberMe">'))->assertCheckboxChecked('rememberMe');
329354
}
330355

331356
public function testAssertCheckboxNotChecked()
332357
{
333-
$this->getCrawlerTester(new Crawler('<html><body><form><input type="checkbox" name="rememberMe">'))->assertCheckboxNotChecked('rememberMe');
334-
$this->getCrawlerTester(new Crawler('<!DOCTYPE html><body><form><input type="checkbox" name="rememberMe">'))->assertCheckboxNotChecked('rememberMe');
358+
$crawler = \PHP_VERSION_ID >= 80400 && class_exists(DomCrawler::class) ? DomCrawler::class : Crawler::class;
359+
360+
$this->getCrawlerTester(new $crawler('<html><body><form><input type="checkbox" name="rememberMe">'))->assertCheckboxNotChecked('rememberMe');
361+
$this->getCrawlerTester(new $crawler('<!DOCTYPE html><body><form><input type="checkbox" name="rememberMe">'))->assertCheckboxNotChecked('rememberMe');
335362
$this->expectException(AssertionFailedError::class);
336363
$this->expectExceptionMessage('does not match selector "input[name="rememberMe"]:checked".');
337-
$this->getCrawlerTester(new Crawler('<html><body><form><input type="checkbox" name="rememberMe" checked>'))->assertCheckboxNotChecked('rememberMe');
364+
$this->getCrawlerTester(new $crawler('<html><body><form><input type="checkbox" name="rememberMe" checked>'))->assertCheckboxNotChecked('rememberMe');
338365
}
339366

340367
public function testAssertFormValue()
341368
{
342-
$this->getCrawlerTester(new Crawler('<html><body><form id="form"><input type="text" name="username" value="Fabien">', 'http://localhost'))->assertFormValue('#form', 'username', 'Fabien');
369+
$crawler = \PHP_VERSION_ID >= 80400 && class_exists(DomCrawler::class) ? DomCrawler::class : Crawler::class;
370+
371+
$this->getCrawlerTester(new $crawler('<html><body><form id="form"><input type="text" name="username" value="Fabien">', 'http://localhost'))->assertFormValue('#form', 'username', 'Fabien');
343372
$this->expectException(AssertionFailedError::class);
344373
$this->expectExceptionMessage('Failed asserting that two strings are identical.');
345-
$this->getCrawlerTester(new Crawler('<html><body><form id="form"><input type="text" name="username" value="Fabien">', 'http://localhost'))->assertFormValue('#form', 'username', 'Jane');
374+
$this->getCrawlerTester(new $crawler('<html><body><form id="form"><input type="text" name="username" value="Fabien">', 'http://localhost'))->assertFormValue('#form', 'username', 'Jane');
346375
}
347376

348377
public function testAssertNoFormValue()
349378
{
350-
$this->getCrawlerTester(new Crawler('<html><body><form id="form"><input type="checkbox" name="rememberMe">', 'http://localhost'))->assertNoFormValue('#form', 'rememberMe');
379+
$crawler = \PHP_VERSION_ID >= 80400 && class_exists(DomCrawler::class) ? DomCrawler::class : Crawler::class;
380+
381+
$this->getCrawlerTester(new $crawler('<html><body><form id="form"><input type="checkbox" name="rememberMe">', 'http://localhost'))->assertNoFormValue('#form', 'rememberMe');
351382
$this->expectException(AssertionFailedError::class);
352383
$this->expectExceptionMessage('Field "rememberMe" has a value in form "#form".');
353-
$this->getCrawlerTester(new Crawler('<html><body><form id="form"><input type="checkbox" name="rememberMe" checked>', 'http://localhost'))->assertNoFormValue('#form', 'rememberMe');
384+
$this->getCrawlerTester(new $crawler('<html><body><form id="form"><input type="checkbox" name="rememberMe" checked>', 'http://localhost'))->assertNoFormValue('#form', 'rememberMe');
354385
}
355386

356387
public function testAssertRequestAttributeValueSame()

src/Symfony/Component/BrowserKit/AbstractBrowser.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Symfony\Component\BrowserKit\Exception\LogicException;
1717
use Symfony\Component\BrowserKit\Exception\RuntimeException;
1818
use Symfony\Component\DomCrawler\Crawler;
19+
use Symfony\Component\DomCrawler\DomCrawler;
1920
use Symfony\Component\DomCrawler\Form;
2021
use Symfony\Component\DomCrawler\Link;
2122
use Symfony\Component\Process\PhpProcess;
@@ -510,7 +511,11 @@ protected function createCrawlerFromContent(string $uri, string $content, string
510511
return null;
511512
}
512513

513-
$crawler = new Crawler(null, $uri, null, $this->useHtml5Parser);
514+
if (\PHP_VERSION_ID >= 80400 && $this->useHtml5Parser && class_exists(DomCrawler::class)) {
515+
$crawler = new DomCrawler(null, $uri);
516+
} else {
517+
$crawler = new Crawler(null, $uri, null, $this->useHtml5Parser);
518+
}
514519
$crawler->addContent($content, $type);
515520

516521
return $crawler;

src/Symfony/Component/BrowserKit/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* Add `isFirstPage()` and `isLastPage()` methods to the History class for checking navigation boundaries
88
* Add PHPUnit constraints: `BrowserHistoryIsOnFirstPage` and `BrowserHistoryIsOnLastPage`
9+
* Leverage the native HTML5 parser when using PHP 8.4+
910

1011
6.4
1112
---

0 commit comments

Comments
 (0)