Testing your DOM in Laravel.
Testing your views, yes I said, testing your views. This is something that is both boring and quite tricky to get right. Sure you can write a test that checks that a certain text is shown, or not shown according to what you want to show in your view. The reason for it being tricky to test your views is that the view is something that you as a backend developer doesn't really have that much control over, unless of course if you are a one-man team doing the full stack.
Let's look at an example. You will have to excuse that I don't use PHPUnit in this post, but I'm trying to learn how to use PEST, and so far I really like it. I was a bit of a sceptic about using closures, but I have grown quite fond of PEST. However, this isn't a post about PEST, but rather about testing your views, or rather the DOM of your views.
Let's say that we want to test if the view contains a table. We could write the test like this.
1it('has a table', function () {2 get(route('records'))3 ->assertOk()4 ->assertSee([5 '<table>',6 '</table>'7 ], false);8});
Then in our view we would have code similar to this.
1<table> 2 <tr> 3 <th>Artist:</th> 4 <th>Record Title:</th> 5 <th>Release Year:</th> 6 </tr> 7 @foreach($records as $record) 8 <tr> 9 <td>{{ $record->artist }}</td>10 <td>{{ $record->title }}</td>11 <td>{{ $record->pelease_year }}</td>12 </tr>13 @endforeach14</table>
This is all well and good, but what if our frontend persons adds classes to our table?
1<table class="some_class_making_it_pretty">
Our test would no longer pass. Some might argue that you shouldn't really test for the structure of the html,
but sometimes you need to. Maybe not for simple things like divs
, paragraphs
, spans
or any of the multitude of
html elements that you could use, but rather things like the image we are displaying has the accessibility alt
property set. The thing we really should test is our forms, but that can be really tricky to do, sure you can just
check that there is an input
, a name
property that equals a column in our database, but ...
1it('has a input named some_database_column', function () {2 get(route('records.create'))3 ->assertOk()4 ->assertSee([5 'input',6 'name="some_database_column"'7 ], false);8});
How would we know that we test the right input? We don't, sure we could do something like this.
1it('has a table', function () {2 get(route('records.create'))3 ->assertOk()4 ->assertSee([5 '<input name="some_database_column"'6 ], false);7});
That would make sure that we test the correct input, but we are back in the hands of the frontend person. What if they change the order of the properties, well then we are screwed.
What about input values? Same thing there, we can check for its existence, but we can't really be sure that the value isn't in the wrong form element. You could probably come up with some solution using regular expressions, and make that work for most cases, but I promise you that you will never get them all, there will always be edge cases.
So what can we do?
We could of course ignore the issue completely, we could be satisfied with the checks we can do,
I was chatting on Twitter with @rsinnbeck about the voes of testing forms, to make
sure they contain the right elements, the labels for those elements, the CSRF tokens, and so on.
A few days later he started developing a package to help with testing the DOM, and it has just been made public,
and published on Packigist
. The package is called laravel-dom-assertions
and at the time of writing is in
version 1.1.1.
Let's pull it in as a dev dependency.
composer require sinnbeck/laravel-dom-assertions --dev
How will this package help us? It helps us partly by giving us a pretty syntax, and providing us with a simple way to read the DOM, the elements and their attributes.
Testing the DOM.
Let's start with something simple, like checking if the page contains an element of a certain type.
A recommendation is to always use the ->assertOk()
before you do any other assertions, this is to avoid any false
positives.
1get('/records')2 ->assertOk()3 ->assertElementExists();
The assertElementExists()
without any parameters looks for the body
tag in your view,
so if we run the test above on our empty view, we will get the following result.
1The view is empty!2Failed asserting that a string is not empty.
So let's try to make it pass.
1<body></body>
1✓ it has a body tag2 3Tests: 1 passed4Assertions: 1
Testing if the page has a body
tag might not be the most useful test, but it is a good start.
It is not proper html since we are missing the doctype
, header
, title
and a few meta
tag,
but it makes the test pass. Hopefully it will also check for those things in a later version,
or at least have methods for it.
There is a small caveat for this, it will wrap the html in a body tag if one is missing, just like your browser would. I personally think this is wrong, and it should be strict with lazy developers who write improper html. I know that @rsinnbeck is still looking for a way to prevent the DOMDocument to do that, so if you know how to fix it please make a PR at the GitHub repository
The assertElementExists()
method takes a parameter that is the CSS selector of the element you want
to assert is there.
1get('/records')2 ->assertOk()3 ->assertElementExists('#main');
Running the test will tell us the following
1No element found with selector: #main
So let's update the view so that it has a #main
.
1<body>2 <div id="main">3 4 </div>5</body>
The test is once again passing.
1✓ it has a #main element2 3Tests: 1 passed4Assertions: 1
You can use any valid CSS selector, like
1get('/records')2 ->assertOk()3 ->assertElementExists('#main') //Id selector4 ->assertElementExists('.some-class') //Class selector5 ->assertElementExists('p') //Tag selector6 ->assertElementExists('div > p'); //Nested selector
Basically any CSS selector that you can imagine.
Let's make the test above pass, just to see that it works.
1<body>2 <div id="main" class="some-class">3 <p></p>4 </div>5</body>
Now just testing for the presence of an element with an id of main
is pretty pointless, so let's expand on it
to make sure that it is of the correct tag type. We want to make sure that the main
id is a div
, and this is
how we do it.
1get('/records')2 ->assertOk()3 ->assertElementExists('#main', function (AssertElement $element) {4 $element->is('div');5 });
Since we have a div with the id of main
already our test passes with flying colors.
1✓ it has a #main element that is a div2 3Tests: 1 passed4Assertions: 4
We can also test if the element has a certain attribute.
1get('/records')2 ->assertOk()3 ->assertElementExists('#main', function (AssertElement $element) {4 $element->is('div')5 ->has('class');6 });
Now checking that an element has a class attribute isn't very useful, but if you for example use a JavaScript framework
like AlpineJs, then it would be good to check for attributes related to that. This will look for an element with the
id of overview
, and the property x-data
with the value of {foo: 1}
1get('/records')2 ->assertOk()3 ->assertElementExists('#overview', function (AssertElement $element) {4 $element->has('x-data', '{foo: 1}');5 });
The html would then something look like this.
1<div id="overview" class="some-nifty-class" x-data="{foo: 1}"></div>
We could even check for which CSS classes that are attached to the element if we want. This would be a very bad
practice, but it is possible, however for each class you add to your element, the ->has('class', '<classes>'
would
need to have the same classes in both your test and view. The only time I think it's valid to test for a class on
an element is when you check the state of it. If it's active
, hidden
, or some other state that you need to check.
1get('/records')2 ->assertOk()3 ->assertElementExists('#overview', function (AssertElement $element) {4 $element->has('x-data', '{foo: 1}')5 ->has('class', 'some-nifty-class');6 });
We can chain an infinite number of attribute checks if we want to, or write them on their own line.
1get('/records')2 ->assertOk()3 ->assertElementExists('#overview', function (AssertElement $element) {4 $element->has('x-data', '{foo: 1}')5 ->has('x-something', '{something: 3}');6 });
We can also chain an infinite number of assertElement()
if we want to.
1get('/records')2 ->assertOk()3 ->assertElementExists('#overview', function (AssertElement $element) {4 $element->has('x-data', '{foo: 1}')5 ->has('x-something', '{something: 3}');6 })7 ->assertElementExists('#underview', function (AssertElement $element) {8 $element->has('x-data', '{foo: 2}')9 ->has('x-something', '{something: 4}'););
Let's move on shall we, and take a look at how to check that an element contains another element
AKA have a child-element. This test would look for an element with the id of overview
and then a div
inside that element.
1get('/records')2 ->assertOk()3 ->assertElementExists('#overview', function (AssertElement $element) {4 $element->contains('div');5 });
You can use any CSS selector in the contains
that you need.
Take this test straight from the docs for example.
1get('/records')2 ->assertOk()3 ->assertElementExists('#overview', function (AssertElement $element) {4 $element->contains('div:nth-of-type(3)');5 });
It will look for the third child div
of the "overview
" element.
What about the child element having a certain attribute?
@rsinnbeck has you covered there as well, just pass a second argument of type array
to the ->contains
method.
1get('/records')2 ->assertOk()3 ->assertElementExists('#overview', function (AssertElement $element) {4 $element->contains('li.list-item', [5 'x-data' => 'foobar'6 ]);7 });
We can also use the doesntContain()
method to make sure that a that doesn't have a certain child element. Same as for
contains
we can pass the attributes as a second parameter.
We can check for multiple child elements of the same type as so.
1get('/records')2 ->assertOk()3 ->assertElementExists('#some-list', function (AssertElement $element) {4 $element->contains('li', 3);5 });
The code above would look for this html
1<ul id="some-list">2 <li>Item 1</li>3 <li>Item 2</li>4 <li>Item 3</li>5</ul>
If you have more than three list items the assertion will fail your test.
If you want to target a specific child element you can use the find()
method like so.
1get('/records')2 ->assertOk()3 ->assertElementExists('#some-list', function (AssertElement $element) {4 $element->find('li.list-item');5 });
If more than one child element is found, it will use the first one, so you need to be specific when using the
find()
method.
You can make further assertions on the element you found if you want to, just pass a new closure as the second argument
to the find()
method.
1get('/records')2 ->assertOk()3 ->assertElementExists('#some-list', function (AssertElement $element) {4 $element->find('li.list-item', function (AssertElement $element) {5 $element->is('li');6 });7 });
This means that you can add how many levels as you like when asserting the DOM, but be aware that it becomes quite messy with too many levels in the same test.
Now on to the most interesting part of the Laravel-dom-assertion package, testing forms.
Testing forms.
When testing forms we could use the ->assertElementExists()
method, but for our convenience there is a more
descriptive helper method called ->assertFormExists()
. If you don't pass any parameters it will check for the
existence of any form in our DOM. To assert that a particular form exists, just pass the selector of choice to the
method.
1get('/records/create')2 ->assertOk()3 ->assertFormExists('#my-form');
We can then as a second argument to the assertFormExists()
method pass a closure that receives an instance of the
AssertForm
class like so.
1get('/records/create')2 ->assertOk()3 ->assertFormExists('#my-form', function (AssertForm $form) {4 5 });
Then we can assert lots of things about the form by using one of the following assertions in the AssertForm
instance.
- hasMethod()
- hasAction()
- hasCSRF()
- hasSpoofMethod()
- has()
- hasEnctype()
- containsInput()
- containsTextArea()
- contains()
- containsButton()
- doesntContain()
Let's take a look at each of these assertions one by one.
The hasMethod()
assertion checks to see if the form has the given method.
1get('/records/create')2 ->assertOk()3 ->assertFormExists('#my-form', function (AssertForm $form) {4 $form->hasMethod('POST');5 });
The hasMethod()
method normalizes the string passed into it, so both POST
and post
will match.
That means that you don't have to worry if you use upper or lower case in your view.
The hasAction()
method check that your form has the given action.
1get('/records/create')2 ->assertOk()3 ->assertFormExists('#my-form', function (AssertForm $form) {4 $form->hasMethod('POST')5 ->hasAction('/records/store');6 });
The hasCSRF()
method doesn't need any parameters, and it checks that you have the hidden token field that protects
you from cross site request forgeries.
1get('/records/create')2 ->assertOk()3 ->assertFormExists('#my-form', function (AssertForm $form) {4 $form->hasMethod('POST')5 ->hasAction('/records/store')6 ->hasCSRF();7 });
Since the support for all the methods are a bit sketchy at best, Laravel uses method spoofs for the form methods
other than the POST
method, and you can assert for the existence of the desired method spoof by using
hasSpoofMethod()
. The hasSpoofMethod()
takes one argument, which is a string containing the method you want to spoof.
So if we want to make sure that our update form has the PUT
method spoof we can then do this.
1get('/records/edit')2 ->assertOk()3 ->assertFormExists('#my-form', function (AssertForm $form) {4 $form->hasMethod('POST')5 ->hasAction('/records/update')6 ->hasCSRF()7 ->hasSpoofMethod('PUT');8 });
You can also do it like this, but I think that the readability of the test suffers.
1get('/records/edit')2 ->assertOk()3 ->assertFormExists('#my-form', function (AssertForm $form) {4 $form->hasMethod('PUT')5 ->hasAction('/records/update')6 ->hasCSRF();7 });
If you pass any other method than GET
or POST
to the hasMethod()
it will assume that you want to check
for the spoof instead. I'm not sure that I like that and I might just pass a PR for that at the GitHub repo.
Just like with the element assertions you can check for any kind of attributes using the has()
method.
If you want you can use Laravel's magic methods so instead of typing has('enctype', 'multipart/form-data')
,
you can use hasEnctype
where pass you pass the enctype
as a parameter like so hasEnctype('multipart/form-data')
.
1get('/records/create')2 ->assertOk()3 ->assertFormExists('#my-form', function (AssertForm $form) {4 $form->hasMethod('POST')5 ->hasAction('/records/store')6 ->hasCSRF()7 ->hasEnctype('multipart/form-data');8 });
Input fields and text areas are very easy to test for, just use the contains
method on the AssertForm
instance,
and to make it very readable you use those magic methods again like so.
1get('/records/create') 2 ->assertOk() 3 ->assertFormExists('#my-form', function (AssertForm $form) { 4 $form->hasMethod('POST') 5 ->hasAction('/records/store') 6 ->hasCSRF() 7 ->hasEnctype('multipart/form-data'); 8 ->containsInput([ 9 'name' => 'title',10 'value' => 'Some title'11 ])12 ->containsTextarea([13 'name' => 'comments',14 'value' => 'The text you want to assert'15 ]);16 });
You can use magic methods to test for other types of inputs, labels, and buttons, or you can use the longer form.
1$form->contains('label', [2 'for' => 'title'3]);4 5$form->containsLabel([6 'for' => 'title'7]);
You can also make sure that a form doesn't contain a certain element by using the doesntContain()
method.
1get('/records/create')2 ->assertOk()3 ->assertFormExists('#my-form', function (AssertForm $form) {4 $form->doesntContain('label', [5 'for' => 'password'6 ]);7 });
You can use the magic methods on the doesntContain
assertions as well, just like the contains
assertions.
Select has warranted its own assertion class, called AssertSelect
. The AssertSelect
API differs a bit from
the other assertion classes we have been using so far, as we need to use the findSelect()
method to find the
select we are looking for. If we have more than one select we can use the nth-of-type()
selector, and if we
only have one we don't need to pass any selector, just the closure that we need for making our assertions on the
select.
1get('/records/create')2 ->assertOk()3 ->assertFormExists('#my-form', function (AssertForm $form) {4 $form->findSelect('select:nth-of-type(2)', function (AssertSelect $select) {5 $select->has('name', 'country');6 });7 });
You can also assert that it contains options, by using the ->containOption()
or the ->containsOptions()
methods.
The first takes a single array of keys and values like this.
1$select->containsOption([2 'value' => 1,3 'text' => 'Sweden'4]);
You can also check for properties like x-data
if you want too.
The ->containsOptions()
method is for checking that you have a bunch of options in your select.
1$select->containsOptions(2 [3 'value' => 'dk',4 'text' => 'Denmark'5 ],6 [7 'value' => 'se',8 'text' => 'Sweden'9 ]);
You can check if a value is selected with the ->hasValue()
method, or check multiple values with the ->hasValues()
method.
1$select->hasValue('se');2 3$select->hasValues(['dk', 'se']);
The ->hasValue()
and ->hasValues
are syntactic sugar for.
1$select->containsOption([ 2 'selected' => 'selected', 3 'value' => 'dk' 4]); 5 6$select->containsOptions( 7[ 8 'selected' => 'selected', 9 'value' => 'dk'10],11[12 'selected' => 'selected',13 'value' => 'se'14]15);
Datalists are very similar to selects, the difference is that they are hidden, and that they are referenced in a property on the input text item. This is how you assert that a form has a datalist and that it contains the values you are looking for.
1get('/records/create')2 ->assertOk()3 ->assertFormExists('#my-form', function (AssertForm $form) {4 $form->findDatalist('#my-datalist', function (AssertDatalist $datalist) {5 $datalist->containsOption(['value' => 'Sweden']);6 });7 });
And for multiple values you use the ->containsOptions()
assertion.
1$form->findDatalist('#my-datalist', function (AssertDatalist $datalist) { 2 $datalist->containsOptions( 3 [ 4 'value' => 'Denmark' 5 ], 6 [ 7 'value' => 'Sweden' 8 ], 9 );10});
So now we have looked at how to do each step, let's try applying this to a real life example.
We will be looking at the books.edit
view of my pet project Mediabase, and try to write tests for it
using this package. Now I already have a test suite for this, so I will not TDD the form from scratch.
Let's do it piece by piece, starting with making sure that the view has a form, and since it only has one form we don't need to pass any additional parameters.
1it('has a form', function () {2 get(route('books.edit', $this->book))3 ->assertOk()4 ->assertFormExists();5});
We then check for the proper method on the form.
1it('has a form with a post method', function () {2 get(route('books.edit', $this->book))3 ->assertOk()4 ->assertFormExists(function(AssertForm $form) {5 $form->hasMethod('post');6 });7});
Then we look for the correct action.
1it('has a form with the correct action', function () {2 get(route('books.edit', $this->book))3 ->assertOk()4 ->assertFormExists(function(AssertForm $form) {5 $form->hasAction(route('books.update', $this->book));6 });7});
The proper spoof method for an update.
1it('has a spoof method of put', function () {2 get(route('books.edit', $this->book))3 ->assertOk()4 ->assertFormExists(function(AssertForm $form) {5 $form->hasSpoofMethod('put');6 });7});
The CSRF field.
1it('has a CSRF token field', function () {2 get(route('books.edit', $this->book))3 ->assertOk()4 ->assertFormExists(function (AssertForm $form){5 $form->hasCSRF();6 });7});
Now we are five tests into our form, and so far we have only tested that the form has the proper method, action, method spoof, and that it has the CSRF token. I believe we can make all these tests into a single test.
1it('has an update form with the necessary parts needed by laravel', function () { 2 get(route('books.edit', $this->book)) 3 ->assertOk() 4 ->assertFormExists(function(AssertForm $form) { 5 $form->hasMethod('post') 6 ->hasAction(route('books.update', $this->book)) 7 ->hasSpoofMethod('put') 8 ->hasCSRF(); 9 });10});
Now isn't that much better? Hey, shouldn't we test the form's enctype? Well since we don't send any files with the form there's no need for it.
We could continue chaining on the test above, but I think it's a good idea to test each input on its own.
Let's start with the title
field. We assert that we have a label for our title
input, and that we have the
name
, id
and value
of the input. The reason for asserting that the input has the correct id
is that the
for
attribute in the label needs a corresponding id
on the input.
1it('contains a title field', function () { 2 get(route('books.edit', $this->book)) 3 ->assertOk() 4 ->assertFormExists(function(AssertForm $form) { 5 $form->containsLabel([ 6 'for' => 'title' 7 ]) 8 ->containsInput([ 9 'name' => 'title',10 'id' => 'title',11 'value' => $this->book->title12 ]);13 });14});
We do the same thing for the published year field.
1it('contains a published_year field', function () { 2 get(route('books.edit', $this->book)) 3 ->assertOk() 4 ->assertFormExists(function(AssertForm $form) { 5 $form->containsLabel([ 6 'for' => 'published_year' 7 ]) 8 ->containsInput([ 9 'name' => 'published_year',10 'id' => 'published_year',11 'value' => $this->book->published_year12 ]);13 });14});
The author field is a bit special, and as you can see we are using the array syntax for the field name, this is so that we can pass multiple authors to each book.
1it('contains an author field', function () { 2 $this->book->authors()->attach(Author::factory()->create()); 3 get(route('books.edit', $this->book)) 4 ->assertOk() 5 ->assertFormExists(function(AssertForm $form) { 6 $form->containsLabel([ 7 'for' => 'author' 8 ]) 9 ->containsInput([10 'name' => 'author[]',11 'id' => 'author',12 'list' => 'authors',13 'value' => $this->book->authors[0]->last_name . ', ' . $this->book->authors[0]->first_name14 ])15 ->containsDatalist([16 'id' => 'authors'17 ]);18 });19});
There are two thing that we need to pay attention to here as well. The first is that we use the list
attribute,
and we give it the value of the id on the datalist. The second thing is that we check for a datalist element with
the id authors
. We do not check that the datalist has any options here, we will do that in a later test.
Next up is the format field, and that just like the authors field uses a datalist. The same goes for the genre field.
1it('contains a format field', function () { 2 get(route('books.edit', $this->book)) 3 ->assertOk() 4 ->assertFormExists(function(AssertForm $form) { 5 $form->containsLabel([ 6 'for' => 'format' 7 ]) 8 ->containsInput([ 9 'name' => 'format_name',10 'id' => 'format',11 'list' => 'formats',12 'value' => $this->book->format->name13 ])14 ->containsDatalist([15 'id' => 'formats'16 ]);17 });18});19 20it('contains a genre field', function () {21 get(route('books.edit', $this->book))22 ->assertOk()23 ->assertFormExists(function(AssertForm $form) {24 $form->containsLabel([25 'for' => 'genre'26 ])27 ->containsInput([28 'name' => 'genre_name',29 'id' => 'genre',30 'list' => 'genres',31 'value' => $this->book->genre->name32 ])33 ->containsDatalist([34 'id' => 'genres'35 ]);36 });37});
The following to test makes sure that we have an ISBN field and a blurb textarea.
1it('contains an isbn field', function () { 2 get(route('books.edit', $this->book)) 3 ->assertOk() 4 ->assertFormExists(function(AssertForm $form) { 5 $form->containsLabel([ 6 'for' => 'isbn' 7 ]) 8 ->containsInput([ 9 'name' => 'isbn',10 'id' => 'isbn',11 'value' => $this->book->isbn12 ]);13 });14});15 16it('contains a blurb text area', function () {17 get(route('books.edit', $this->book))18 ->assertOk()19 ->assertFormExists(function(AssertForm $form) {20 $form->containsLabel([21 'for' => 'blurb'22 ])23 ->containsTextarea([24 'name' => 'blurb',25 'id' => 'blurb',26 'value' => $this->book->blurb,27 ]);28 });29});
The thing to notice is that the content of the textarea needs to be handled with the value attribute.
A textarea doesn't really have a value
attribute, as you can see here
MDN.
It works with the value or the text
attribute since the package normalizes it behind the scenes.
There isn't much to say about the series field, it is another input that uses a datalist.
1it('contains a series field', function () { 2 get(route('books.edit', $this->book)) 3 ->assertOk() 4 ->assertFormExists(function(AssertForm $form) { 5 $form->containsLabel([ 6 'for' => 'series' 7 ]) 8 ->containsInput([ 9 'name' => 'series_name',10 'id' => 'series',11 'list' => 'series-list',12 'value' => $this->book->series->name13 ])14 ->containsDatalist([15 'id' => 'series-list'16 ]);17 });18});
We will not take any closer look on the part
and publisher
fields, since they are more of the same that we already
looked at.
1it('contains a part field', function () { 2 get(route('books.edit', $this->book)) 3 ->assertOk() 4 ->assertFormExists(function(AssertForm $form) { 5 $form->containsLabel([ 6 'for' => 'part' 7 ]) 8 ->containsInput([ 9 'name' => 'part',10 'id' => 'part',11 'value' => $this->book->part12 ]);13 });14});15 16it('contains a publishers field', function () {17 get(route('books.edit', $this->book))18 ->assertOk()19 ->assertFormExists(function(AssertForm $form) {20 $form->containsLabel([21 'for' => 'publisher'22 ])23 ->containsInput([24 'name' => 'publisher_name',25 'id' => 'publisher',26 'list' => 'publishers',27 'value' => $this->book->publisher->name28 ])29 ->containsDatalist([30 'id' => 'publishers'31 ]);32 });33});
The next test is a bit special, they both are and aren't a part of the form. They are meant to be used for adding and removing author fields. Maybe I shouldn't have put them inside the form tag, but that is what I did. I will later on add some JavaScript to handle the necessary DOM updates needed when the buttons are clicked.
1it('contains buttons for adding and removing author inputs', function () { 2 get(route('books.edit', $this->book)) 3 ->assertOk() 4 ->assertFormExists(function (AssertForm $form) { 5 $form->containsButton([ 6 'title' => 'Add Author', 7 ]) 8 ->containsButton([ 9 'title' => 'Remove Author',10 ]);11 });12});
I just check that they are buttons and that they have the correct titles. If you don't know what the global title attribute does, it's the little balloon tip that pops up when you are hovering over the element. You can read more about those here MDN.
For the submit button I just make sure that there is an input of the type submit.
1it('contains a submit button', function () {2 get(route('books.edit', $this->book))3 ->assertOk()4 ->assertFormExists(function (AssertForm $form) {5 $form->containsInput([6 'type' => 'submit',7 ]);8 });9});
Submit buttons are a bit strange since you can create them in two ways.
Like this
1<input type="submit" value="Submit">
Or like this
1<button type="submit">Submit</button>
That means that you need to write your assertions accordingly. For the first way you can use the ->containsInput()
like I did, and for the second you can use the ->containsButton()
assertion.
You remember that I had a test that checks that we have the author field, well this one checks and makes sure that
we have one for each author, and remember an id is only allowed once on a valid html page. That means that the second
author field should not contain any id="author"
attribute. To make sure that we only have one input with the id
of author
we add another ->containsInput()
but this time, as a second argument we pass a number, and that number
makes sure that we only have one input on that form with the id of author
.
1it('contains an author field for each author', function () { 2 $this->book->authors()->attach(Author::factory()->create()); 3 $this->book->authors()->attach(Author::factory()->create()); 4 get(route('books.edit', $this->book)) 5 ->assertOk() 6 ->assertFormExists(function(AssertForm $form) { 7 $form->containsLabel([ 8 'for' => 'author' 9 ])10 ->containsInput([11 'id' => 'author'12 ], 1)13 ->containsInput([14 'name' => 'author[]',15 'id' => 'author',16 'list' => 'authors',17 'value' => $this->book->authors[0]->last_name . ', ' . $this->book->authors[0]->first_name18 ])19 ->containsInput([20 'name' => 'author[]',21 'list' => 'authors',22 'value' => $this->book->authors[1]->last_name . ', ' . $this->book->authors[1]->first_name23 ])24 ->containsDatalist([25 'id' => 'authors'26 ]);27 });28});
Now it's time to assert those datalists, this was introduced in version 1.1.1, so make sure that you have updated your project dependencies before attempting this.
Like I said earlier, the datalist is basically a hidden select, so the syntax for testing against it is almost identical. The most important thing about datalists is that they require an id, that means that you can't use another selector to find it. There isn't really much more to say about datalist in the sense of asserting that they exist and that they contain the proper values. If you want to know more about datalists, and how you can use them, you can do that here Using a datalist instead of a dropdown in your forms
So here are the tests for the datalist for.
- Authors
- Formats
- Genres
- Series
- Publishers
1it('contains a list of authors', function () { 2 Author::factory() 3 ->count(2) 4 ->sequence( 5 [ 6 'first_name' => 'David', 7 'last_name' => 'Eddings' 8 ], 9 [ 10 'first_name' => 'Terry', 11 'last_name' => 'Goodkind' 12 ])->create(); 13 14 get(route('books.edit', $this->book)) 15 ->assertOk() 16 ->assertFormExists(function (AssertForm $form) { 17 $form->findDatalist('#authors', function (AssertDataList $datalist) { 18 $datalist->containsOptions( 19 ['value' => 'Eddings, David'], 20 ['value' => 'Goodkind, Terry'] 21 ); 22 }); 23 }); 24}); 25 26it('contains a list of formats', function () { 27 Format::factory() 28 ->count(2) 29 ->sequence( 30 [ 31 'name' => 'Pocket', 32 'media_type_id' => $this->mediaTypeId, 33 ], 34 [ 35 'name' => 'Hardcover', 36 'media_type_id' => $this->mediaTypeId, 37 ] 38 ) 39 ->create(); 40 41 get(route('books.create')) 42 ->assertOk() 43 ->assertFormExists(function (AssertForm $form) { 44 $form->findDatalist('#formats', function (AssertDataList $datalist) { 45 $datalist->containsOptions( 46 ['value' => 'Hardcover'], 47 ['value' => 'Pocket'] 48 ); 49 }); 50 }); 51}); 52 53it('contains a list of genres', function () { 54 Genre::factory() 55 ->count(2) 56 ->sequence( 57 [ 58 'name' => 'Fantasy', 59 'media_type_id' => $this->mediaTypeId, 60 ], 61 [ 62 'name' => 'Crime', 63 'media_type_id' => $this->mediaTypeId, 64 ] 65 ) 66 ->create(); 67 68 get(route('books.create')) 69 ->assertOk() 70 ->assertFormExists(function (AssertForm $form) { 71 $form->findDatalist('#genres', function (AssertDataList $datalist) { 72 $datalist->containsOptions( 73 ['value' => 'Crime'], 74 ['value' => 'Fantasy'] 75 ); 76 }); 77 }); 78}); 79 80it('contains a list of series', function () { 81 Series::factory() 82 ->count(2) 83 ->sequence( 84 ['name' => 'The Wheel Of Time'], 85 ['name' => 'The Sword Of Truth'] 86 ) 87 ->create(); 88 89 get(route('books.create')) 90 ->assertOk() 91 ->assertFormExists(function (AssertForm $form) { 92 $form->findDatalist('#series-list', function (AssertDataList $datalist) { 93 $datalist->containsOptions( 94 ['value' => 'The Sword Of Truth'], 95 ['value' => 'The Wheel Of Time'] 96 ); 97 }); 98 }); 99});100 101it('contains a list of publishers', function () {102 Publisher::factory()103 ->count(2)104 ->sequence(105 ['name' => 'TOR'],106 ['name' => 'Ace Books']107 )108 ->create();109 110 get(route('books.create'))111 ->assertOk()112 ->assertFormExists(function (AssertForm $form) {113 $form->findDatalist('#publishers', function (AssertDataList $datalist) {114 $datalist->containsOptions(115 ['value' => 'Ace Books'],116 ['value' => 'TOR']117 );118 });119 });120});
That is it for now, just a quick side note before I leave you to do whatever it is you do when not reading my posts. The package works out of the box with Laravel Livewire and since I don't know Livewire yet, I will just steal the example from the docs.
As livewire uses the TestResponse class from laravel, you can easily use this package with Livewire without any changes
1Livewire::test(UserForm::class)2 ->assertElementExists('form', function (AssertElement $form) {3 $form->find('#submit', function (AssertElement $assert) {4 $assert->is('button');5 $assert->has('text', 'Submit');6 })->contains('[wire\:model="name"]', 1);7 });
For more information about Laravel-dom-assertions make sure to visit the repo on GitHub, and don't forget to give it a star.
Are you still here reading?
Good, because there are a couple yet undocumented assertions that you can do, and as a bonus I will include them here.
Most of you probably know that you need to have a doctype to make sure your page doesn't get render in some strange compatibility mode by your browser. That is why it makes sense to assert that the document has the proper html5 doctype.
1it('has a html5 doctype ', function () {2 $this->get(route('books.index'))3 ->assertHtml5();4});
You can also assert that you have the proper tags in the document head
.
1->assertElementExists('head', function (AssertElement $assert) {2 $assert->is('head');3 });4});
1->assertElementExists('head', function (AssertElement $assert) {2 $assert->contains('meta', [3 'charset' => 'UTF-8',4 ]);5});
1->assertElementExists('head', function (AssertElement $assert) {2 $assert->find('title', function (AssertElement $element) {3 $element->has('text', 'Nesting');4 });5});
Hope you will have an easier time of testing those views now.
//Tray2