diff --git a/src/Concerns/AssertsMany.php b/src/Concerns/AssertsMany.php new file mode 100644 index 0000000..9e436da --- /dev/null +++ b/src/Concerns/AssertsMany.php @@ -0,0 +1,22 @@ +call($this); + } catch (AssertionFailedError $assertion) { + throw new AssertionFailedError(message: $message ?? $assertion->getMessage(), previous: $assertion); + } + + return $this; + } +} diff --git a/src/Contracts/PromotableAssertableElement.php b/src/Contracts/PromotableAssertableElement.php new file mode 100644 index 0000000..1a3f10a --- /dev/null +++ b/src/Contracts/PromotableAssertableElement.php @@ -0,0 +1,14 @@ + new self(HTMLDocument::createFromFile($path, $options, $overrideEncoding))); + return self::convertErrorsToExceptions(fn () => new self(HTMLDocument::createFromFile($path, $options, $overrideEncoding))); } /** Create an assertable document from a string. */ public static function createFromString(string $source, int $options = 0, ?string $overrideEncoding = null): self { - return self::promoteErrorsToExceptions(fn () => new self(HTMLDocument::createFromString($source, $options, $overrideEncoding))); + return self::convertErrorsToExceptions(fn () => new self(HTMLDocument::createFromString($source, $options, $overrideEncoding))); } /** Return the assertable element matching the given selector. */ @@ -76,7 +76,7 @@ public function querySelector(string $selector): AssertableElement )); } - return new AssertableElement($element); + return new AssertableElement($element)->promote(); } /** Return assertable elements matching the given selector. */ @@ -95,7 +95,7 @@ public function getElementById(string $id): AssertableElement )); } - return new AssertableElement($element); + return new AssertableElement($element)->promote(); } /** Return assertable elements matching the given tag. */ @@ -104,14 +104,14 @@ public function getElementsByTagName(string $tag): AssertableElementsList return new AssertableElementsList($this->document->getElementsByTagName($tag)); } - /** Promote any PHP errors that occur in the given callback to custom exceptions. */ - private static function promoteErrorsToExceptions(callable $callback): mixed + /** Convert any PHP errors that occur in the given callback to custom exceptions. */ + private static function convertErrorsToExceptions(callable $callback): mixed { try { set_error_handler(function (int $severity, string $message, string $file, int $line): never { throw new UnableToCreateAssertableDocument( 'Unable to create assertable HTML document.', - previous: new ErrorException($message, $severity, $severity, $file, $line), + previous: new ErrorException($message, 0, $severity, $file, $line), ); }); diff --git a/src/Dom/AssertableElement.php b/src/Dom/AssertableElement.php index 06cd5fc..e54b42d 100644 --- a/src/Dom/AssertableElement.php +++ b/src/Dom/AssertableElement.php @@ -57,6 +57,12 @@ private function getElement(): HTMLElement|Element return $this->element; } + /** Promote this assertable element to an element-specific equivalent assertable element, if possible. */ + public function promote(): static + { + return new AssertableElementPromoter($this->getElement())->promote() ?? $this; + } + /** Get the assertable element HTML. */ public function getHtml(): string { diff --git a/src/Dom/AssertableElementPromoter.php b/src/Dom/AssertableElementPromoter.php new file mode 100644 index 0000000..df22c5f --- /dev/null +++ b/src/Dom/AssertableElementPromoter.php @@ -0,0 +1,33 @@ + $customElement::shouldPromote($this->element), + ); + + return $match ? new $match($this->element) : null; + } +} diff --git a/src/Dom/AssertableElementsList.php b/src/Dom/AssertableElementsList.php index 5488e64..aab1499 100644 --- a/src/Dom/AssertableElementsList.php +++ b/src/Dom/AssertableElementsList.php @@ -33,7 +33,7 @@ public function __construct(NodeList|HTMLCollection $nodes) { $this->elements = array_values( array_map( - fn (HTMLElement|Element $element): AssertableElement => new AssertableElement($element), + fn (HTMLElement|Element $element): AssertableElement => new AssertableElement($element)->promote(), $nodes instanceof NodeList ? iterator_to_array($nodes) : $this->htmlCollectionToArray($nodes), diff --git a/src/Dom/Elements/AssertableForm.php b/src/Dom/Elements/AssertableForm.php new file mode 100644 index 0000000..982f5fc --- /dev/null +++ b/src/Dom/Elements/AssertableForm.php @@ -0,0 +1,133 @@ +tagName === 'FORM'; + } + + /* + |-------------------------------------------------------------------------- + | Assert Method + |-------------------------------------------------------------------------- + */ + + /** Assert the form has the given method attribute. */ + public function assertMethod(string $method, ?string $message = null): static + { + $method = trim(mb_strtolower($method)); + + $this->assertAttribute( + 'method', + fn (?string $value): bool => trim(mb_strtolower((string) $value)) === $method, + $message ?? sprintf("The form method doesn't equal [%s].", $method), + ); + + return $this; + } + + /** Assert the form has the GET method attribute. */ + public function assertMethodGet(?string $message = null): static + { + $this->assertMethod('get', $message); + + return $this; + } + + /** Assert the form has the POST method attribute. */ + public function assertMethodPost(?string $message = null): static + { + $this->assertMethod('post', $message); + + return $this; + } + + /** Assert the form has the DIALOG method attribute. */ + public function assertMethodDialog(?string $message = null): static + { + $this->assertMethod('dialog', $message); + + return $this; + } + + /* + |-------------------------------------------------------------------------- + | Assert Hidden Method + |-------------------------------------------------------------------------- + */ + + /** Assert the form has the given hidden input method. */ + public function assertHiddenInputMethod(string $selector, string $method, ?string $message = null): static + { + $this->assertMany(function () use ($selector, $method): void { + $method = trim(mb_strtolower($method)); + + $this->querySelector($selector) + ->assertMatchesSelector('input[type="hidden"]') + ->assertAttribute('value', fn (?string $value): bool => trim(mb_strtolower((string) $value)) === $method); + }, $message ?? sprintf("The form hidden input method doesn't equal [%s].", $method)); + + return $this; + } + + /** Assert the form has the PUT hidden input method. */ + public function assertMethodPut(?string $message = null): static + { + $this->assertHiddenInputMethod('input[type="hidden"][name="_method"]', 'put', $message); + + return $this; + } + + /** Assert the form has the PATCH hidden input method. */ + public function assertMethodPatch(?string $message = null): static + { + $this->assertHiddenInputMethod('input[type="hidden"][name="_method"]', 'patch', $message); + + return $this; + } + + /** Assert the form has the DELETE hidden input method. */ + public function assertMethodDelete(?string $message = null): static + { + $this->assertHiddenInputMethod('input[type="hidden"][name="_method"]', 'delete', $message); + + return $this; + } + + /* + |-------------------------------------------------------------------------- + | Assert Upload + |-------------------------------------------------------------------------- + */ + + /** Assert the form accepts uploads (has correct enctype and at least one file input. */ + public function assertAcceptsUpload(?string $message = null): static + { + $this->assertMany(function (): void { + $this->assertAttribute('enctype', fn (?string $value): bool => trim(mb_strtolower((string) $value)) === 'multipart/form-data') + ->assertElementsCountGreaterThanOrEqual('input[type="file"]', 1); + }, $message ?? "The form doesn't accept uploads."); + + return $this; + } +} diff --git a/tests/Integration/AssertableFormTest.php b/tests/Integration/AssertableFormTest.php new file mode 100644 index 0000000..0c48ca6 --- /dev/null +++ b/tests/Integration/AssertableFormTest.php @@ -0,0 +1,44 @@ +', LIBXML_HTML_NOIMPLIED) + ->querySelector('form') + ->assertMethodGet(); + + AssertableDocument::createFromString('
', LIBXML_HTML_NOIMPLIED) + ->querySelector('form') + ->assertMethodPost(); + + AssertableDocument::createFromString('', LIBXML_HTML_NOIMPLIED) + ->querySelector('form') + ->assertMethodDialog(); + + AssertableDocument::createFromString('', LIBXML_HTML_NOIMPLIED) + ->querySelector('form') + ->assertMethodPut(); + + AssertableDocument::createFromString('', LIBXML_HTML_NOIMPLIED) + ->querySelector('form') + ->assertMethodPatch(); + + AssertableDocument::createFromString('', LIBXML_HTML_NOIMPLIED) + ->querySelector('form') + ->assertMethodDelete(); + + // Form Uploads + AssertableDocument::createFromString('', LIBXML_HTML_NOIMPLIED) + ->querySelector('form') + ->assertAcceptsUpload(); + } +} diff --git a/tests/Unit/Concerns/Asserts/AssertsManyTest.php b/tests/Unit/Concerns/Asserts/AssertsManyTest.php new file mode 100644 index 0000000..f4845de --- /dev/null +++ b/tests/Unit/Concerns/Asserts/AssertsManyTest.php @@ -0,0 +1,34 @@ +getAssertsMany(); + $object->assertMany(function () { + PHPUnit::assertSame('Foo', 'Bar', 'Foo is not Bar'); + }, 'The test assertion failed'); + } catch (AssertionFailedError $exception) { + $this->assertSame('The test assertion failed', $exception->getMessage()); + $this->assertSame('Foo is not Bar' . "\n" . 'Failed asserting that two strings are identical.', $exception->getPrevious()->getMessage()); + } + } + + private function getAssertsMany(): object + { + return new class + { + use AssertsMany; + }; + } +} diff --git a/tests/Unit/Dom/AssertableElementPromoterTest.php b/tests/Unit/Dom/AssertableElementPromoterTest.php new file mode 100644 index 0000000..5ff943d --- /dev/null +++ b/tests/Unit/Dom/AssertableElementPromoterTest.php @@ -0,0 +1,45 @@ +getElement($html))->promote(); + + if (is_string($class)) { + $this->assertInstanceOf($class, $promoted); + } else { + $this->assertNull($promoted); + } + } + + public static function promote_data_provider(): iterable + { + yield 'form' => ['', AssertableForm::class]; + yield 'null' => ['