diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
index 8072319b5eaf7..e947361e7fb48 100644
--- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
+++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
@@ -7,6 +7,9 @@ CHANGELOG
* Add native return type to `Translator` and to `Application::reset()`
* Deprecate the integration of Doctrine annotations, either uninstall the `doctrine/annotations` package or disable the integration by setting `framework.annotations` to `false`
* Enable `json_decode_detailed_errors` context for Serializer by default if `kernel.debug` is true and the `seld/jsonlint` package is installed
+ * Add `DomCrawlerAssertionsTrait::assertAnySelectorTextContains(string $selector, string $text)`
+ * Add `DomCrawlerAssertionsTrait::assertAnySelectorTextSame(string $selector, string $text)`
+ * Add `DomCrawlerAssertionsTrait::assertAnySelectorTextNotContains(string $selector, string $text)`
6.3
---
diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/DomCrawlerAssertionsTrait.php b/src/Symfony/Bundle/FrameworkBundle/Test/DomCrawlerAssertionsTrait.php
index 02e3ad848b1ae..a167094614097 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Test/DomCrawlerAssertionsTrait.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Test/DomCrawlerAssertionsTrait.php
@@ -47,6 +47,14 @@ public static function assertSelectorTextContains(string $selector, string $text
), $message);
}
+ public static function assertAnySelectorTextContains(string $selector, string $text, string $message = ''): void
+ {
+ self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints(
+ new DomCrawlerConstraint\CrawlerSelectorExists($selector),
+ new DomCrawlerConstraint\CrawlerAnySelectorTextContains($selector, $text)
+ ), $message);
+ }
+
public static function assertSelectorTextSame(string $selector, string $text, string $message = ''): void
{
self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints(
@@ -55,6 +63,14 @@ public static function assertSelectorTextSame(string $selector, string $text, st
), $message);
}
+ public static function assertAnySelectorTextSame(string $selector, string $text, string $message = ''): void
+ {
+ self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints(
+ new DomCrawlerConstraint\CrawlerSelectorExists($selector),
+ new DomCrawlerConstraint\CrawlerAnySelectorTextSame($selector, $text)
+ ), $message);
+ }
+
public static function assertSelectorTextNotContains(string $selector, string $text, string $message = ''): void
{
self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints(
@@ -63,6 +79,14 @@ public static function assertSelectorTextNotContains(string $selector, string $t
), $message);
}
+ public static function assertAnySelectorTextNotContains(string $selector, string $text, string $message = ''): void
+ {
+ self::assertThat(self::getCrawler(), LogicalAnd::fromConstraints(
+ new DomCrawlerConstraint\CrawlerSelectorExists($selector),
+ new LogicalNot(new DomCrawlerConstraint\CrawlerAnySelectorTextContains($selector, $text))
+ ), $message);
+ }
+
public static function assertPageTitleSame(string $expectedTitle, string $message = ''): void
{
self::assertSelectorTextSame('title', $expectedTitle, $message);
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Test/WebTestCaseTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Test/WebTestCaseTest.php
index 802f676070519..fbaa7a02c277d 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/Test/WebTestCaseTest.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Test/WebTestCaseTest.php
@@ -208,6 +208,30 @@ public function testAssertSelectorTextNotContains()
$this->getCrawlerTester(new Crawler('
Foo'))->assertSelectorTextNotContains('body > h1', 'Foo');
}
+ public function testAssertAnySelectorTextContains()
+ {
+ $this->getCrawlerTester(new Crawler('- Bar
- Foo Baz'))->assertAnySelectorTextContains('ul li', 'Foo');
+ $this->expectException(AssertionFailedError::class);
+ $this->expectExceptionMessage('matches selector "ul li" and the text of any node matching selector "ul li" contains "Foo".');
+ $this->getCrawlerTester(new Crawler('
- Bar
- Baz'))->assertAnySelectorTextContains('ul li', 'Foo');
+ }
+
+ public function testAssertAnySelectorTextSame()
+ {
+ $this->getCrawlerTester(new Crawler('
- Bar
- Foo'))->assertAnySelectorTextSame('ul li', 'Foo');
+ $this->expectException(AssertionFailedError::class);
+ $this->expectExceptionMessage('matches selector "ul li" and has at least a node matching selector "ul li" with content "Foo".');
+ $this->getCrawlerTester(new Crawler('
- Bar
- Baz'))->assertAnySelectorTextSame('ul li', 'Foo');
+ }
+
+ public function testAssertAnySelectorTextNotContains()
+ {
+ $this->getCrawlerTester(new Crawler('
- Bar
- Baz'))->assertAnySelectorTextNotContains('ul li', 'Foo');
+ $this->expectException(AssertionFailedError::class);
+ $this->expectExceptionMessage('matches selector "ul li" and the text of any node matching selector "ul li" does not contain "Foo".');
+ $this->getCrawlerTester(new Crawler('
- Bar
- Foo'))->assertAnySelectorTextNotContains('ul li', 'Foo');
+ }
+
public function testAssertPageTitleSame()
{
$this->getCrawlerTester(new Crawler('Foo'))->assertPageTitleSame('Foo');
diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json
index 12f2858a41d4a..a4525d9724a4d 100644
--- a/src/Symfony/Bundle/FrameworkBundle/composer.json
+++ b/src/Symfony/Bundle/FrameworkBundle/composer.json
@@ -42,7 +42,7 @@
"symfony/console": "^5.4.9|^6.0.9|^7.0",
"symfony/clock": "^6.2|^7.0",
"symfony/css-selector": "^5.4|^6.0|^7.0",
- "symfony/dom-crawler": "^6.3|^7.0",
+ "symfony/dom-crawler": "^6.4|^7.0",
"symfony/dotenv": "^5.4|^6.0|^7.0",
"symfony/polyfill-intl-icu": "~1.0",
"symfony/form": "^5.4|^6.0|^7.0",
diff --git a/src/Symfony/Component/DomCrawler/CHANGELOG.md b/src/Symfony/Component/DomCrawler/CHANGELOG.md
index be1c0ba143f92..a46a6120e7d6d 100644
--- a/src/Symfony/Component/DomCrawler/CHANGELOG.md
+++ b/src/Symfony/Component/DomCrawler/CHANGELOG.md
@@ -1,6 +1,12 @@
CHANGELOG
=========
+6.4
+---
+
+* Add `CrawlerAnySelectorTextContains` test constraint
+* Add `CrawlerAnySelectorTextSame` test constraint
+
6.3
---
diff --git a/src/Symfony/Component/DomCrawler/Test/Constraint/CrawlerAnySelectorTextContains.php b/src/Symfony/Component/DomCrawler/Test/Constraint/CrawlerAnySelectorTextContains.php
new file mode 100644
index 0000000000000..f209499315087
--- /dev/null
+++ b/src/Symfony/Component/DomCrawler/Test/Constraint/CrawlerAnySelectorTextContains.php
@@ -0,0 +1,69 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\DomCrawler\Test\Constraint;
+
+use PHPUnit\Framework\Constraint\Constraint;
+use Symfony\Component\DomCrawler\Crawler;
+
+final class CrawlerAnySelectorTextContains extends Constraint
+{
+ private string $selector;
+ private string $expectedText;
+ private bool $hasNode = false;
+
+ public function __construct(string $selector, string $expectedText)
+ {
+ $this->selector = $selector;
+ $this->expectedText = $expectedText;
+ }
+
+ public function toString(): string
+ {
+ if ($this->hasNode) {
+ return sprintf('the text of any node matching selector "%s" contains "%s"', $this->selector, $this->expectedText);
+ }
+
+ return sprintf('the Crawler has a node matching selector "%s"', $this->selector);
+ }
+
+ protected function matches($other): bool
+ {
+ if (!$other instanceof Crawler) {
+ throw new \InvalidArgumentException(sprintf('"%s" constraint expected an argument of type "%s", got "%s".', self::class, Crawler::class, get_debug_type($other)));
+ }
+
+ $other = $other->filter($this->selector);
+ if (!\count($other)) {
+ $this->hasNode = false;
+
+ return false;
+ }
+
+ $this->hasNode = true;
+
+ $nodes = $other->each(fn (Crawler $node) => $node->text(null, true));
+ $matches = array_filter($nodes, function (string $node): bool {
+ return str_contains($node, $this->expectedText);
+ });
+
+ return 0 < \count($matches);
+ }
+
+ protected function failureDescription($other): string
+ {
+ if (!$other instanceof Crawler) {
+ throw new \InvalidArgumentException(sprintf('"%s" constraint expected an argument of type "%s", got "%s".', self::class, Crawler::class, get_debug_type($other)));
+ }
+
+ return $this->toString();
+ }
+}
diff --git a/src/Symfony/Component/DomCrawler/Test/Constraint/CrawlerAnySelectorTextSame.php b/src/Symfony/Component/DomCrawler/Test/Constraint/CrawlerAnySelectorTextSame.php
new file mode 100644
index 0000000000000..f4c8320b2a372
--- /dev/null
+++ b/src/Symfony/Component/DomCrawler/Test/Constraint/CrawlerAnySelectorTextSame.php
@@ -0,0 +1,57 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\DomCrawler\Test\Constraint;
+
+use PHPUnit\Framework\Constraint\Constraint;
+use Symfony\Component\DomCrawler\Crawler;
+
+final class CrawlerAnySelectorTextSame extends Constraint
+{
+ private string $selector;
+ private string $expectedText;
+
+ public function __construct(string $selector, string $expectedText)
+ {
+ $this->selector = $selector;
+ $this->expectedText = $expectedText;
+ }
+
+ public function toString(): string
+ {
+ return sprintf('has at least a node matching selector "%s" with content "%s"', $this->selector, $this->expectedText);
+ }
+
+ protected function matches($other): bool
+ {
+ if (!$other instanceof Crawler) {
+ throw new \InvalidArgumentException(sprintf('"%s" constraint expected an argument of type "%s", got "%s".', self::class, Crawler::class, get_debug_type($other)));
+ }
+
+ $other = $other->filter($this->selector);
+ if (!\count($other)) {
+ return false;
+ }
+
+ $nodes = $other->each(fn (Crawler $node) => trim($node->text(null, true)));
+
+ return \in_array($this->expectedText, $nodes, true);
+ }
+
+ protected function failureDescription($other): string
+ {
+ if (!$other instanceof Crawler) {
+ throw new \InvalidArgumentException(sprintf('"%s" constraint expected an argument of type "%s", got "%s".', self::class, Crawler::class, get_debug_type($other)));
+ }
+
+ return 'the Crawler '.$this->toString();
+ }
+}
diff --git a/src/Symfony/Component/DomCrawler/Tests/Test/Constraint/CrawlerAnySelectorTextContainsTest.php b/src/Symfony/Component/DomCrawler/Tests/Test/Constraint/CrawlerAnySelectorTextContainsTest.php
new file mode 100644
index 0000000000000..d3c4d8ac78e0a
--- /dev/null
+++ b/src/Symfony/Component/DomCrawler/Tests/Test/Constraint/CrawlerAnySelectorTextContainsTest.php
@@ -0,0 +1,49 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\DomCrawler\Tests\Test\Constraint;
+
+use PHPUnit\Framework\ExpectationFailedException;
+use PHPUnit\Framework\TestCase;
+use PHPUnit\Framework\TestFailure;
+use Symfony\Component\DomCrawler\Crawler;
+use Symfony\Component\DomCrawler\Test\Constraint\CrawlerAnySelectorTextContains;
+
+class CrawlerAnySelectorTextContainsTest extends TestCase
+{
+ public function testConstraint()
+ {
+ $constraint = new CrawlerAnySelectorTextContains('ul li', 'Foo');
+
+ self::assertTrue($constraint->evaluate(new Crawler('
- Foo
'), '', true));
+ self::assertTrue($constraint->evaluate(new Crawler('- Bar
- Foo'), '', true));
+ self::assertTrue($constraint->evaluate(new Crawler('
- Bar
- Foo Bar Baz'), '', true));
+ self::assertFalse($constraint->evaluate(new Crawler('
- Bar
- Baz'), '', true));
+
+ try {
+ $constraint->evaluate(new Crawler('
- Bar
- Baz'));
+
+ self::fail();
+ } catch (ExpectationFailedException $e) {
+ self::assertEquals("Failed asserting that the text of any node matching selector \"ul li\" contains \"Foo\".\n", TestFailure::exceptionToString($e));
+ }
+
+ try {
+ $constraint->evaluate(new Crawler('Foobar'));
+
+ self::fail();
+ } catch (ExpectationFailedException $e) {
+ self::assertEquals("Failed asserting that the Crawler has a node matching selector \"ul li\".\n", TestFailure::exceptionToString($e));
+
+ return;
+ }
+ }
+}
diff --git a/src/Symfony/Component/DomCrawler/Tests/Test/Constraint/CrawlerAnySelectorTextSameTest.php b/src/Symfony/Component/DomCrawler/Tests/Test/Constraint/CrawlerAnySelectorTextSameTest.php
new file mode 100644
index 0000000000000..265d55c162fb6
--- /dev/null
+++ b/src/Symfony/Component/DomCrawler/Tests/Test/Constraint/CrawlerAnySelectorTextSameTest.php
@@ -0,0 +1,39 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\DomCrawler\Tests\Test\Constraint;
+
+use PHPUnit\Framework\ExpectationFailedException;
+use PHPUnit\Framework\TestCase;
+use PHPUnit\Framework\TestFailure;
+use Symfony\Component\DomCrawler\Crawler;
+use Symfony\Component\DomCrawler\Test\Constraint\CrawlerAnySelectorTextSame;
+
+final class CrawlerAnySelectorTextSameTest extends TestCase
+{
+ public function testConstraint()
+ {
+ $constraint = new CrawlerAnySelectorTextSame('ul li', 'Foo');
+
+ self::assertTrue($constraint->evaluate(new Crawler('
- Foo
'), '', true));
+ self::assertTrue($constraint->evaluate(new Crawler('- Bar
- Foo'), '', true));
+ self::assertFalse($constraint->evaluate(new Crawler('
- Bar
- Foo Bar Baz'), '', true));
+ self::assertFalse($constraint->evaluate(new Crawler('
- Bar
- Baz'), '', true));
+
+ try {
+ $constraint->evaluate(new Crawler('
- Bar
- Baz'));
+
+ self::fail();
+ } catch (ExpectationFailedException $e) {
+ self::assertEquals("Failed asserting that the Crawler has at least a node matching selector \"ul li\" with content \"Foo\".\n", TestFailure::exceptionToString($e));
+ }
+ }
+}