diff --git a/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php b/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php index 1c08baa1..a8f60e15 100644 --- a/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/BrowserAssertionsTrait.php @@ -10,6 +10,8 @@ use PHPUnit\Framework\Constraint\LogicalNot; use Symfony\Component\BrowserKit\Test\Constraint\BrowserCookieValueSame; use Symfony\Component\BrowserKit\Test\Constraint\BrowserHasCookie; +use Symfony\Component\BrowserKit\Test\Constraint\BrowserHistoryIsOnFirstPage; +use Symfony\Component\BrowserKit\Test\Constraint\BrowserHistoryIsOnLastPage; use Symfony\Component\HttpFoundation\Test\Constraint\RequestAttributeValueSame; use Symfony\Component\HttpFoundation\Test\Constraint\ResponseCookieValueSame; use Symfony\Component\HttpFoundation\Test\Constraint\ResponseFormatSame; @@ -71,6 +73,78 @@ public function assertBrowserNotHasCookie(string $name, string $path = '/', ?str $this->assertThatForClient(new LogicalNot(new BrowserHasCookie($name, $path, $domain)), $message); } + /** + * Asserts that the browser history is currently on the first page. + * + * ```php + * assertBrowserHistoryIsOnFirstPage(); + * ``` + */ + public function assertBrowserHistoryIsOnFirstPage(string $message = ''): void + { + $this->assertTrue( + class_exists(BrowserHistoryIsOnFirstPage::class), + 'The assertBrowserHistoryIsOnFirstPage method requires symfony/browser-kit >= 7.4.' + ); + + $this->assertThatForClient(new BrowserHistoryIsOnFirstPage(), $message); + } + + /** + * Asserts that the browser history is not currently on the first page. + * + * ```php + * assertBrowserHistoryIsNotOnFirstPage(); + * ``` + */ + public function assertBrowserHistoryIsNotOnFirstPage(string $message = ''): void + { + $this->assertTrue( + class_exists(BrowserHistoryIsOnFirstPage::class), + 'The assertBrowserHistoryIsNotOnFirstPage method requires symfony/browser-kit >= 7.4.' + ); + + $this->assertThatForClient(new LogicalNot(new BrowserHistoryIsOnFirstPage()), $message); + } + + /** + * Asserts that the browser history is currently on the last page. + * + * ```php + * assertBrowserHistoryIsOnLastPage(); + * ``` + */ + public function assertBrowserHistoryIsOnLastPage(string $message = ''): void + { + $this->assertTrue( + class_exists(BrowserHistoryIsOnLastPage::class), + 'The assertBrowserHistoryIsOnLastPage method requires symfony/browser-kit >= 7.4.' + ); + + $this->assertThatForClient(new BrowserHistoryIsOnLastPage(), $message); + } + + /** + * Asserts that the browser history is not currently on the last page. + * + * ```php + * assertBrowserHistoryIsNotOnLastPage(); + * ``` + */ + public function assertBrowserHistoryIsNotOnLastPage(string $message = ''): void + { + $this->assertTrue( + class_exists(BrowserHistoryIsOnLastPage::class), + 'The assertBrowserHistoryIsNotOnLastPage method requires symfony/browser-kit >= 7.4.' + ); + + $this->assertThatForClient(new LogicalNot(new BrowserHistoryIsOnLastPage()), $message); + } + /** * Asserts that the specified request attribute matches the expected value. * diff --git a/src/Codeception/Module/Symfony/DomCrawlerAssertionsTrait.php b/src/Codeception/Module/Symfony/DomCrawlerAssertionsTrait.php index 2c4fa177..c7fefbe1 100644 --- a/src/Codeception/Module/Symfony/DomCrawlerAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/DomCrawlerAssertionsTrait.php @@ -5,12 +5,17 @@ namespace Codeception\Module\Symfony; use PHPUnit\Framework\Constraint\Constraint; +use PHPUnit\Framework\Constraint\LogicalAnd; use PHPUnit\Framework\Constraint\LogicalNot; +use Symfony\Component\DomCrawler\Test\Constraint\CrawlerAnySelectorTextContains; +use Symfony\Component\DomCrawler\Test\Constraint\CrawlerAnySelectorTextSame; use Symfony\Component\DomCrawler\Test\Constraint\CrawlerSelectorAttributeValueSame; +use Symfony\Component\DomCrawler\Test\Constraint\CrawlerSelectorCount; use Symfony\Component\DomCrawler\Test\Constraint\CrawlerSelectorExists; use Symfony\Component\DomCrawler\Test\Constraint\CrawlerSelectorTextContains; use Symfony\Component\DomCrawler\Test\Constraint\CrawlerSelectorTextSame; +use function class_exists; use function sprintf; trait DomCrawlerAssertionsTrait @@ -119,6 +124,20 @@ public function assertSelectorNotExists(string $selector, string $message = ''): $this->assertThatCrawler(new LogicalNot(new CrawlerSelectorExists($selector)), $message); } + /** + * Asserts that the given selector matches the expected number of elements. + * + * ```php + * assertSelectorCount(3, '.item'); + * ``` + */ + public function assertSelectorCount(int $expectedCount, string $selector, string $message = ''): void + { + $this->assertDomCrawlerConstraintAvailable(CrawlerSelectorCount::class, __FUNCTION__); + $this->assertThatCrawler(new CrawlerSelectorCount($expectedCount, $selector), $message); + } + /** * Asserts that the first element matching the given selector contains the expected text. * @@ -132,6 +151,48 @@ public function assertSelectorTextContains(string $selector, string $text, strin $this->assertThatCrawler(new CrawlerSelectorTextContains($selector, $text), $message); } + /** + * Asserts that at least one element matching the given selector contains the expected text. + * + * ```php + * assertAnySelectorTextContains('.item', 'Available'); + * ``` + */ + public function assertAnySelectorTextContains(string $selector, string $text, string $message = ''): void + { + $this->assertDomCrawlerConstraintAvailable(CrawlerAnySelectorTextContains::class, __FUNCTION__); + + $this->assertThatCrawler( + LogicalAnd::fromConstraints( + new CrawlerSelectorExists($selector), + new CrawlerAnySelectorTextContains($selector, $text) + ), + $message + ); + } + + /** + * Asserts that at least one element matching the given selector has exactly the expected text. + * + * ```php + * assertAnySelectorTextSame('.item', 'In stock'); + * ``` + */ + public function assertAnySelectorTextSame(string $selector, string $text, string $message = ''): void + { + $this->assertDomCrawlerConstraintAvailable(CrawlerAnySelectorTextSame::class, __FUNCTION__); + + $this->assertThatCrawler( + LogicalAnd::fromConstraints( + new CrawlerSelectorExists($selector), + new CrawlerAnySelectorTextSame($selector, $text) + ), + $message + ); + } + /** * Asserts that the first element matching the given selector does not contain the expected text. * @@ -145,6 +206,27 @@ public function assertSelectorTextNotContains(string $selector, string $text, st $this->assertThatCrawler(new LogicalNot(new CrawlerSelectorTextContains($selector, $text)), $message); } + /** + * Asserts that no element matching the given selector contains the expected text. + * + * ```php + * assertAnySelectorTextNotContains('.item', 'Error'); + * ``` + */ + public function assertAnySelectorTextNotContains(string $selector, string $text, string $message = ''): void + { + $this->assertDomCrawlerConstraintAvailable(CrawlerAnySelectorTextContains::class, __FUNCTION__); + + $this->assertThatCrawler( + LogicalAnd::fromConstraints( + new CrawlerSelectorExists($selector), + new LogicalNot(new CrawlerAnySelectorTextContains($selector, $text)) + ), + $message + ); + } + /** * Asserts that the text of the first element matching the given selector equals the expected text. * @@ -184,4 +266,16 @@ private function assertInputValue(string $fieldName, string $expectedValue, bool } $this->assertThatCrawler($constraint, $context); } + + private function assertDomCrawlerConstraintAvailable(string $constraintClass, string $function): void + { + $this->assertTrue( + class_exists($constraintClass), + sprintf( + "The '%s' assertion is not available with your installed symfony/dom-crawler version. Missing constraint: %s", + $function, + $constraintClass + ) + ); + } } diff --git a/src/Codeception/Module/Symfony/MailerAssertionsTrait.php b/src/Codeception/Module/Symfony/MailerAssertionsTrait.php index bcfccfd7..54f829c2 100644 --- a/src/Codeception/Module/Symfony/MailerAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/MailerAssertionsTrait.php @@ -145,6 +145,21 @@ public function seeEmailIsSent(int $expectedCount = 1): void $this->assertThat($this->getMessageMailerEvents(), new MailerConstraint\EmailCount($expectedCount)); } + /** + * Returns all mailer events for the given transport. + * + * ```php + * getMailerEvents(); + * ``` + * + * @return MessageEvent[] + */ + public function getMailerEvents(?string $transport = null): array + { + return $this->getMessageMailerEvents()->getEvents($transport); + } + /** * Returns the mailer event at the specified index. * @@ -155,7 +170,35 @@ public function seeEmailIsSent(int $expectedCount = 1): void */ public function getMailerEvent(int $index = 0, ?string $transport = null): ?MessageEvent { - return $this->getMessageMailerEvents()->getEvents($transport)[$index] ?? null; + return $this->getMailerEvents($transport)[$index] ?? null; + } + + /** + * Returns all sent mailer messages for the given transport. + * + * ```php + * getMailerMessages(); + * ``` + * + * @return RawMessage[] + */ + public function getMailerMessages(?string $transport = null): array + { + return $this->getMessageMailerEvents()->getMessages($transport); + } + + /** + * Returns the mailer message at the specified index. + * + * ```php + * getMailerMessage(); + * ``` + */ + public function getMailerMessage(int $index = 0, ?string $transport = null): ?RawMessage + { + return $this->getMailerMessages($transport)[$index] ?? null; } protected function grabLastSentRawMessage(): ?RawMessage diff --git a/src/Codeception/Module/Symfony/MimeAssertionsTrait.php b/src/Codeception/Module/Symfony/MimeAssertionsTrait.php index fbda1fe8..a6fbc2c2 100644 --- a/src/Codeception/Module/Symfony/MimeAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/MimeAssertionsTrait.php @@ -11,6 +11,7 @@ use Symfony\Component\Mime\RawMessage; use Symfony\Component\Mime\Test\Constraint as MimeConstraint; +use function class_exists; use function sprintf; trait MimeAssertionsTrait @@ -30,6 +31,20 @@ public function assertEmailAddressContains(string $headerName, string $expectedV $this->assertThat($this->getMessageOrFail($email, __FUNCTION__), new MimeConstraint\EmailAddressContains($headerName, $expectedValue)); } + /** + * Verify that an email does not contain the given address in the specified header. + * If no Message is specified, the last sent message is used instead. + * + * ```php + * assertEmailAddressNotContains('To', 'john_doe@example.com'); + * ``` + */ + public function assertEmailAddressNotContains(string $headerName, string $expectedValue, ?Message $email = null): void + { + $this->assertThat($this->getMessageOrFail($email, __FUNCTION__), new LogicalNot(new MimeConstraint\EmailAddressContains($headerName, $expectedValue))); + } + /** * Verify that an email has the specified number `$count` of attachments. * If no Email is specified, the last sent email is used instead. @@ -158,6 +173,36 @@ public function assertEmailTextBodyNotContains(string $text, ?Email $email = nul $this->assertThat($this->getMessageOrFail($email, __FUNCTION__), new LogicalNot(new MimeConstraint\EmailTextBodyContains($text))); } + /** + * Verify that an email subject contains `$expectedValue`. + * If no Email is specified, the last sent email is used instead. + * + * ```php + * assertEmailSubjectContains('Account created successfully'); + * ``` + */ + public function assertEmailSubjectContains(string $expectedValue, ?Email $email = null): void + { + $this->assertMimeConstraintAvailable(MimeConstraint\EmailSubjectContains::class, __FUNCTION__); + $this->assertThat($this->getMessageOrFail($email, __FUNCTION__), new MimeConstraint\EmailSubjectContains($expectedValue)); + } + + /** + * Verify that an email subject does not contain `$expectedValue`. + * If no Email is specified, the last sent email is used instead. + * + * ```php + * assertEmailSubjectNotContains('Password reset'); + * ``` + */ + public function assertEmailSubjectNotContains(string $expectedValue, ?Email $email = null): void + { + $this->assertMimeConstraintAvailable(MimeConstraint\EmailSubjectContains::class, __FUNCTION__); + $this->assertThat($this->getMessageOrFail($email, __FUNCTION__), new LogicalNot(new MimeConstraint\EmailSubjectContains($expectedValue))); + } + /** * Resolves a Message for assertion or fails the test. * @@ -174,4 +219,16 @@ private function getMessageOrFail(?Message $message, string $caller): Message return $message; } + + private function assertMimeConstraintAvailable(string $constraintClass, string $function): void + { + $this->assertTrue( + class_exists($constraintClass), + sprintf( + "The '%s' assertion is not available with your installed symfony/mime version. Missing constraint: %s", + $function, + $constraintClass + ) + ); + } } diff --git a/tests/BrowserAssertionsTest.php b/tests/BrowserAssertionsTest.php index c52ccfe9..505198fb 100644 --- a/tests/BrowserAssertionsTest.php +++ b/tests/BrowserAssertionsTest.php @@ -6,8 +6,12 @@ use Codeception\Module\Symfony\BrowserAssertionsTrait; use Symfony\Component\BrowserKit\Cookie; +use Symfony\Component\BrowserKit\Test\Constraint\BrowserHistoryIsOnFirstPage; +use Symfony\Component\BrowserKit\Test\Constraint\BrowserHistoryIsOnLastPage; use Tests\Support\CodeceptTestCase; +use function class_exists; + final class BrowserAssertionsTest extends CodeceptTestCase { use BrowserAssertionsTrait; @@ -35,6 +39,50 @@ public function testAssertBrowserNotHasCookie(): void $this->assertBrowserNotHasCookie('browser_cookie'); } + public function testAssertBrowserHistoryIsOnFirstPage(): void + { + if (!class_exists(BrowserHistoryIsOnFirstPage::class)) { + $this->markTestSkipped('Browser history assertions require symfony/browser-kit with BrowserHistoryIsOnFirstPage support.'); + } + + $this->client->request('GET', '/'); + $this->assertBrowserHistoryIsOnFirstPage(); + } + + public function testAssertBrowserHistoryIsNotOnFirstPage(): void + { + if (!class_exists(BrowserHistoryIsOnFirstPage::class)) { + $this->markTestSkipped('Browser history assertions require symfony/browser-kit with BrowserHistoryIsOnFirstPage support.'); + } + + $this->client->request('GET', '/'); + $this->client->request('GET', '/login'); + $this->assertBrowserHistoryIsNotOnFirstPage(); + } + + public function testAssertBrowserHistoryIsOnLastPage(): void + { + if (!class_exists(BrowserHistoryIsOnLastPage::class)) { + $this->markTestSkipped('Browser history assertions require symfony/browser-kit with BrowserHistoryIsOnLastPage support.'); + } + + $this->client->request('GET', '/'); + $this->client->request('GET', '/login'); + $this->assertBrowserHistoryIsOnLastPage(); + } + + public function testAssertBrowserHistoryIsNotOnLastPage(): void + { + if (!class_exists(BrowserHistoryIsOnLastPage::class)) { + $this->markTestSkipped('Browser history assertions require symfony/browser-kit with BrowserHistoryIsOnLastPage support.'); + } + + $this->client->request('GET', '/'); + $this->client->request('GET', '/login'); + $this->client->back(); + $this->assertBrowserHistoryIsNotOnLastPage(); + } + public function testAssertRequestAttributeValueSame(): void { $this->client->request('GET', '/request_attr'); diff --git a/tests/DomCrawlerAssertionsTest.php b/tests/DomCrawlerAssertionsTest.php index 5c22efe8..ff2f78c3 100644 --- a/tests/DomCrawlerAssertionsTest.php +++ b/tests/DomCrawlerAssertionsTest.php @@ -5,8 +5,13 @@ namespace Tests; use Codeception\Module\Symfony\DomCrawlerAssertionsTrait; +use Symfony\Component\DomCrawler\Test\Constraint\CrawlerAnySelectorTextContains; +use Symfony\Component\DomCrawler\Test\Constraint\CrawlerAnySelectorTextSame; +use Symfony\Component\DomCrawler\Test\Constraint\CrawlerSelectorCount; use Tests\Support\CodeceptTestCase; +use function class_exists; + final class DomCrawlerAssertionsTest extends CodeceptTestCase { use DomCrawlerAssertionsTrait; @@ -57,16 +62,55 @@ public function testAssertSelectorNotExists(): void $this->assertSelectorNotExists('.non-existent-class', 'This selector should not exist.'); } + public function testAssertSelectorCount(): void + { + if (!class_exists(CrawlerSelectorCount::class)) { + $this->markTestSkipped('assertSelectorCount requires CrawlerSelectorCount support in symfony/dom-crawler.'); + } + + $this->assertSelectorCount(2, 'input', 'Expected exactly 2 inputs on the test page.'); + } + public function testAssertSelectorTextContains(): void { $this->assertSelectorTextContains('h1', 'Test', 'The

tag should contain "Test".'); } + public function testAssertAnySelectorTextContains(): void + { + if (!class_exists(CrawlerAnySelectorTextContains::class)) { + $this->markTestSkipped('assertAnySelectorTextContains requires CrawlerAnySelectorTextContains support in symfony/dom-crawler.'); + } + + $this->client->request('GET', '/register'); + $this->assertAnySelectorTextContains('label', 'Password', 'One label should contain the password text.'); + } + + public function testAssertAnySelectorTextSame(): void + { + if (!class_exists(CrawlerAnySelectorTextSame::class)) { + $this->markTestSkipped('assertAnySelectorTextSame requires CrawlerAnySelectorTextSame support in symfony/dom-crawler.'); + } + + $this->client->request('GET', '/register'); + $this->assertAnySelectorTextSame('label', 'Email Address', 'One label should match the email text.'); + } + public function testAssertSelectorTextNotContains(): void { $this->assertSelectorTextNotContains('h1', 'Error', 'The

tag should not contain "Error".'); } + public function testAssertAnySelectorTextNotContains(): void + { + if (!class_exists(CrawlerAnySelectorTextContains::class)) { + $this->markTestSkipped('assertAnySelectorTextNotContains requires CrawlerAnySelectorTextContains support in symfony/dom-crawler.'); + } + + $this->client->request('GET', '/register'); + $this->assertAnySelectorTextNotContains('label', 'forbidden_text', 'No label should contain the forbidden text.'); + } + public function testAssertSelectorTextSame(): void { $this->assertSelectorTextSame('h1', 'Test Page', 'The text in the

tag should be exactly "Test Page".'); diff --git a/tests/MailerAssertionsTest.php b/tests/MailerAssertionsTest.php index ed9c61e4..8398fb02 100644 --- a/tests/MailerAssertionsTest.php +++ b/tests/MailerAssertionsTest.php @@ -65,6 +65,26 @@ public function testGetMailerEvent(): void $this->assertInstanceOf(MessageEvent::class, $this->getMailerEvent()); } + public function testGetMailerEvents(): void + { + $this->client->request('GET', '/send-email'); + $this->assertCount(1, $this->getMailerEvents()); + } + + public function testGetMailerMessages(): void + { + $this->client->request('GET', '/send-email'); + $messages = $this->getMailerMessages(); + $this->assertCount(1, $messages); + $this->assertInstanceOf(Email::class, $messages[0]); + } + + public function testGetMailerMessage(): void + { + $this->client->request('GET', '/send-email'); + $this->assertInstanceOf(Email::class, $this->getMailerMessage()); + } + public function testGrabLastSentEmailReturnsEmailInstance(): void { $this->client->request('GET', '/send-email'); @@ -113,6 +133,7 @@ public function testEdgeCases(): void $this->client->request('GET', '/send-email'); $this->assertNull($this->getMailerEvent(999)); + $this->assertNull($this->getMailerMessage(999)); } private function createQueuedEvent(): MessageEvent diff --git a/tests/MimeAssertionsTest.php b/tests/MimeAssertionsTest.php index f73d1295..53d0c080 100644 --- a/tests/MimeAssertionsTest.php +++ b/tests/MimeAssertionsTest.php @@ -11,8 +11,12 @@ use Symfony\Component\Mime\Header\Headers; use Symfony\Component\Mime\Message; use Symfony\Component\Mime\Part\TextPart; +use Symfony\Component\Mime\Test\Constraint\EmailAddressContains; +use Symfony\Component\Mime\Test\Constraint\EmailSubjectContains; use Tests\Support\CodeceptTestCase; +use function class_exists; + final class MimeAssertionsTest extends CodeceptTestCase { use MailerAssertionsTrait; @@ -30,6 +34,15 @@ public function testAssertEmailAddressContains(): void $this->assertEmailAddressContains('To', 'jane_doe@example.com'); } + public function testAssertEmailAddressNotContains(): void + { + if (!class_exists(EmailAddressContains::class)) { + $this->markTestSkipped('assertEmailAddressNotContains requires EmailAddressContains support in symfony/mime.'); + } + + $this->assertEmailAddressNotContains('To', 'john_doe@example.com'); + } + public function testAssertEmailAttachmentCount(): void { $this->assertEmailAttachmentCount(1); @@ -75,12 +88,45 @@ public function testAssertEmailTextBodyNotContains(): void $this->assertEmailTextBodyNotContains('My secret text body'); } + public function testAssertEmailSubjectContains(): void + { + if (!class_exists(EmailSubjectContains::class)) { + $this->markTestSkipped('assertEmailSubjectContains requires EmailSubjectContains support in symfony/mime.'); + } + + $this->assertEmailSubjectContains('Account created successfully'); + } + + public function testAssertEmailSubjectNotContains(): void + { + if (!class_exists(EmailSubjectContains::class)) { + $this->markTestSkipped('assertEmailSubjectNotContains requires EmailSubjectContains support in symfony/mime.'); + } + + $this->assertEmailSubjectNotContains('Password reset'); + } + public function testAssertionsWorkWithProvidedEmail(): void { - $email = (new Email())->from('custom@example.com')->to('custom@example.com')->text('Custom body text'); + if (!class_exists(EmailAddressContains::class)) { + $this->markTestSkipped('assertEmailAddressNotContains requires EmailAddressContains support in symfony/mime.'); + } + + if (!class_exists(EmailSubjectContains::class)) { + $this->markTestSkipped('assertEmailSubjectContains/assertEmailSubjectNotContains require EmailSubjectContains support in symfony/mime.'); + } + + $email = (new Email()) + ->from('custom@example.com') + ->to('custom@example.com') + ->subject('Custom subject') + ->text('Custom body text'); $this->assertEmailAddressContains('To', 'custom@example.com', $email); + $this->assertEmailAddressNotContains('To', 'other@example.com', $email); $this->assertEmailTextBodyContains('Custom body text', $email); + $this->assertEmailSubjectContains('Custom subject', $email); + $this->assertEmailSubjectNotContains('Other subject', $email); $this->assertEmailNotHasHeader('Cc', $email); }