diff --git a/README.md b/README.md index 3230d13..bccff2f 100644 --- a/README.md +++ b/README.md @@ -337,7 +337,7 @@ The assertion uses the `\Sinnbeck\DomAssertions\Asserts\AssertDatalist` class. $this->get('/some-route') ->assertFormExists('#form1', function (AssertForm $form) { $form->findDatalist('#skills', function (AssertDatalist $list) { - $list ->containsOptions( + $list->containsOptions( [ 'value' => 'PHP', ], @@ -361,6 +361,17 @@ Livewire::test(UserForm::class) }); ``` +### Usage with Blade views +You can also use this package to test blade views. +```php +$this->view('navigation') + ->assertElementExists('nav > ul', function(AssertElement $ul) { + $ul->contains('li', [ + 'class' => 'active', + ]); + }); +``` + ## Overview of methods | Base methods | Description | |------------------------------------------------|--------------------------------------------------------------------------------------| diff --git a/src/DomAssertionsServiceProvider.php b/src/DomAssertionsServiceProvider.php index cc0cf1b..ab60905 100644 --- a/src/DomAssertionsServiceProvider.php +++ b/src/DomAssertionsServiceProvider.php @@ -4,6 +4,7 @@ use Illuminate\Support\ServiceProvider; use Illuminate\Testing\TestResponse; +use Illuminate\Testing\TestView; class DomAssertionsServiceProvider extends ServiceProvider { @@ -11,6 +12,7 @@ public function boot() { if ($this->app->runningUnitTests()) { TestResponse::mixin(new TestResponseMacros()); + TestView::mixin(new TestViewMacros()); } } } diff --git a/src/TestViewMacros.php b/src/TestViewMacros.php new file mode 100644 index 0000000..0ca6ed1 --- /dev/null +++ b/src/TestViewMacros.php @@ -0,0 +1,125 @@ +getMessage()); + } + + Assert::assertEquals( + 'html', + $parser->getDocType(), + 'Not a html5 doctype!' + ); + + return $this; + }; + } + + public function assertElementExists(): Closure + { + return function ($selector = 'body', $callback = null): TestView { + /** @var TestView $this */ + Assert::assertNotEmpty( + (string) $this, + 'The view is empty!' + ); + + try { + $parser = DomParser::new((string) $this); + } catch (DOMException $exception) { + Assert::fail($exception->getMessage()); + } + + if ($selector instanceof Closure) { + $callback = $selector; + $selector = 'body'; + } + + if (is_string($selector)) { + $element = $parser->query($selector); + } else { + Assert::fail('Invalid selector!'); + } + + Assert::assertNotNull($element, sprintf('No element found with selector: %s', $selector)); + + if ($callback) { + $callback(new AssertElement((string) $this, $element)); + } + + return $this; + }; + } + + public function assertFormExists(): Closure + { + return function ($selector = 'form', $callback = null): TestView { + /** @var TestView $this */ + Assert::assertNotEmpty( + (string) $this, + 'The view is empty!' + ); + + try { + $parser = DomParser::new((string) $this); + } catch (DOMException $exception) { + Assert::fail($exception->getMessage()); + } + + if ($selector instanceof Closure) { + $callback = $selector; + $selector = 'form'; + } + + if (is_string($selector)) { + $form = $parser->query($selector); + } else { + Assert::fail('Invalid selector!'); + } + + Assert::assertNotNull( + $form, + sprintf('No form was found with selector "%s"', $selector) + ); + Assert::assertEquals( + 'form', + $form->nodeName, + 'Element is not of type form!'); + + if ($callback) { + $callback(new AssertForm((string) $this, $form)); + } + + return $this; + }; + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 3f22a48..11729f1 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,10 +2,13 @@ namespace Tests; +use Illuminate\Foundation\Testing\Concerns\InteractsWithViews; use Illuminate\Support\Facades\Route; class TestCase extends \Orchestra\Testbench\TestCase { + use InteractsWithViews; + protected function getPackageProviders($app) { return [ diff --git a/tests/ViewTest.php b/tests/ViewTest.php new file mode 100644 index 0000000..2ea00a6 --- /dev/null +++ b/tests/ViewTest.php @@ -0,0 +1,384 @@ +view('empty') + ->assertElementExists(); +})->throws( + AssertionFailedError::class, + 'The view is empty!' +); + +it('can handle an empty body', function () { + $this->view('empty-body') + ->assertElementExists(); +})->throws( + AssertionFailedError::class, + 'No element found with selector: body' +); + +it('can parse broken html', function () { + $this->view('broken') + ->assertElementExists(); +}); + +it('can find the an element', function () { + $this->view('nesting') + ->assertElementExists(); +}); + +it('can find the body', function () { + $this->view('nesting') + ->assertElementExists('body', function (AssertElement $assert) { + $assert->is('body'); + }); +}); + +it('can check for html5', function () { + $this->view('nesting') + ->assertHtml5(); +}); + +it('can fail checking for html5', function () { + $this->view('livewire') + ->assertHtml5(); +})->throws( + AssertionFailedError::class, + 'Not a html5 doctype!' +); + +it('can find the head', function () { + $this->view('nesting') + ->assertElementExists('head', function (AssertElement $assert) { + $assert->is('head'); + }); +}); + +it('can find a meta tag', function () { + $this->view('nesting') + ->assertElementExists('head', function (AssertElement $assert) { + $assert->contains('meta', [ + 'charset' => 'UTF-8', + ]); + }); +}); + +it('can find a title', function () { + $this->view('nesting') + ->assertElementExists('head', function (AssertElement $assert) { + $assert->find('title', function (AssertElement $element) { + $element->has('text', 'Nesting'); + }); + }); +}); + +it('can fail finding a class', function () { + $this->view('nesting') + ->assertElementExists('body', function (AssertElement $assert) { + $assert->find('#nav', function (AssertElement $element) { + $element->doesntHave('class'); + }); + }); +}); + +it('can fail finding a href with exact match', function () { + $this->view('nesting') + ->assertElementExists('body', function (AssertElement $assert) { + $assert->find('#nav a', function (AssertElement $element) { + $element->has('href') + ->doesntHave('href', '/bar'); + }); + }); +}); + +it('can fail when finding a id that isnt expected', function () { + $this->view('nesting') + ->assertElementExists('body', function (AssertElement $assert) { + $assert->find('#nav', function (AssertElement $element) { + $element->doesntHave('id'); + }); + }); +})->throws( + AssertionFailedError::class, + 'Found an attribute "id"' +); + +it('can fail when finding a href with matching value that isnt expected', function () { + $this->view('nesting') + ->assertElementExists('body', function (AssertElement $assert) { + $assert->find('#nav a', function (AssertElement $element) { + $element->doesntHave('href', '/foo'); + }); + }); +})->throws( + AssertionFailedError::class, + 'Found an attribute "href" with value "/foo"' +); + +it('can find an element by selector', function () { + $this->view('nesting') + ->assertElementExists('#nav'); +}); + +it('can fail finding anything', function () { + $this->view('nesting') + ->assertElementExists('div > nav'); +})->throws( + AssertionFailedError::class, + 'No element found with selector: div > nav' +); + +it('can check the element has the correct type', function () { + $this->view('nesting') + ->assertElementExists('#nav', function (AssertElement $element) { + $element->is('nav'); + }); +}); + +it('can fail matching element type', function () { + $this->view('nesting') + ->assertElementExists('#nav', function (AssertElement $element) { + $element->is('div'); + }); +})->throws( + AssertionFailedError::class, + 'Element is not of type "div"' +); + +it('can fail with wrong type of selector', function () { + $this->view('form') + ->assertElementExists(['div']); +})->throws(AssertionFailedError::class, 'Invalid selector!'); + +it('can find a nested element', function () { + $this->view('nesting') + ->assertElementExists(function (AssertElement $element) { + $element->containsDiv(); + }); +}); + +it('can find a nested element with content', function () { + $this->view('nesting') + ->assertElementExists(function (AssertElement $element) { + $element->contains('div', [ + 'class' => 'foobar', + ]); + }); +}); + +it('can match text content', function () { + $this->view('nesting') + ->assertElementExists('span.bar', function (AssertElement $element) { + $element->has('text', 'Foo'); + }); +}); + +it('can match text content with duplicate spaces and vertical whitespace', function () { + $this->view('nesting') + ->assertElementExists('p.foo.bar', function (AssertElement $element) { + $element->has('text', 'Foo Bar'); + }); +}); + +it('can match text content containing a string', function () { + $this->view('nesting') + ->assertElementExists('p.foo.bar', function (AssertElement $element) { + $element->containsText('Bar'); + }); +}); + +it('can match text content containing a string ignoring case', function () { + $this->view('nesting') + ->assertElementExists('p.foo.bar', function (AssertElement $element) { + $element->containsText('bar', true); + }); +}); + +it('can match text content not containing a string', function () { + $this->view('nesting') + ->assertElementExists('p.foo.bar', function (AssertElement $element) { + $element->doesntContainText('bar'); + }); +}); + +it('can match a class no matter the order', function () { + $this->view('nesting') + ->assertElementExists(function (AssertElement $element) { + $element->contains('span', [ + 'class' => 'foo bar', + ]); + $element->find('span', function (AssertElement $span) { + $span->has('class', 'foo bar'); + }); + }); +}); + +it('can match a partial class', function () { + $this->view('nesting') + ->assertElementExists(function (AssertElement $element) { + $element->contains('span', [ + 'class' => 'foo bar', + ]); + $element->find('span', function (AssertElement $span) { + $span->has('class', 'bar'); + }); + }); +}); + +it('can find multiple identical items', function () { + $this->view('nesting') + ->assertElementExists(function (AssertElement $element) { + $element->contains('div', [], 4); + }); +}); + +it('can find multiple identical items simplified', function () { + $this->view('nesting') + ->assertElementExists(function (AssertElement $element) { + $element->contains('div', 4); + }); +}); + +it('can find multiple identical items with content', function () { + $this->view('nesting') + ->assertElementExists(function (AssertElement $element) { + $element->contains('ul > li', [ + 'x-data' => 'foobar', + ], 2); + }); +}); + +it('can find multiple identical items with content ensuring no wrong matches', function () { + $this->view('nesting') + ->assertElementExists(function (AssertElement $element) { + $element->contains('div', [ + 'x-data' => 'foobar', + ], 1); + }); +}); + +it('can fail finding a nested element with content', function () { + $this->view('nesting') + ->assertElementExists(function (AssertElement $element) { + $element->contains('div', [ + 'class' => 'foo', + ]); + }); +})->throws(AssertionFailedError::class, 'Could not find a matching "div" with data:'); + +it('can find a nested element with content functional', function () { + $this->view('nesting') + ->assertElementExists(function (AssertElement $element) { + $element->findDiv(function (AssertElement $element) { + $element->is('div'); + }); + }); +}); + +it('can find a nested element multiple levels', function () { + $this->view('nesting') + ->assertElementExists(function (AssertElement $element) { + $element->findDiv(function (AssertElement $element) { + $element->is('div'); + $element->find('div', function (AssertElement $element) { + $element->is('div'); + $element->findDiv(function (AssertElement $element) { + $element->is('div'); + }); + }); + }); + }); +}); + +it('can find a nested element multiple levels by query', function () { + $this->view('nesting') + ->assertElementExists(function (AssertElement $element) { + $element->findDiv(function (AssertElement $element) { + $element->is('div'); + $element->find('.deep', function (AssertElement $element) { + $element->is('div'); + $element->findSpan(function (AssertElement $element) { + $element->is('span'); + }); + }); + }); + }); +}); + +it('can find a nested element multiple levels by query and attributes', function () { + $this->view('nesting') + ->assertElementExists(function (AssertElement $element) { + $element->findDiv(function (AssertElement $element) { + $element->is('div'); + $element->contains('.deep', [ + 'class' => 'deep', + ]); + }); + }); +}); + +it('can find a nested element and ensure doesnt contain', function () { + $this->view('nesting') + ->assertElementExists(function (AssertElement $element) { + $element->findDiv(function (AssertElement $element) { + $element->is('div'); + $element->doesntContain('nav'); + }); + }); +}); + +it('can fail finding an contained element', function () { + $this->view('nesting') + ->assertElementExists(function (AssertElement $element) { + $element->findDiv(function (AssertElement $element) { + $element->doesntContain('div'); + }); + }); +})->throws( + AssertionFailedError::class, + 'Found a matching element of type "div' +); + +it('can fail finding an contained element with query', function () { + $this->view('nesting') + ->assertElementExists(function (AssertElement $element) { + $element->findDiv(function (AssertElement $element) { + $element->doesntContain('div.foobar'); + }); + }); +})->throws( + AssertionFailedError::class, + 'Found a matching element of type "div' +); + +it('can match on livewire attributes', function () { + $this->view('livewire') + ->assertElementExists('[wire\:model="foo"]', function (AssertElement $element) { + $element->is('input'); + }); +}); + +it('can match has on livewire attributes', function () { + $this->view('livewire') + ->assertElementExists('input', function (AssertElement $element) { + $element->has('wire:model', 'foo'); + }); +}); + +it('can match on livewire with contains', function () { + $this->view('livewire') + ->assertElementExists(function (AssertElement $element) { + $element->contains('input[wire\:model="foo"]'); + }); +}); + +it('can match on livewire contains as attribute', function () { + $this->view('livewire') + ->assertElementExists(function (AssertElement $element) { + $element->contains('input', [ + 'wire:model' => 'foo', + ]); + }); +});