Tray2.se Logo

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 @endforeach
14</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 tag
2 
3Tests: 1 passed
4Assertions: 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 element
2 
3Tests: 1 passed
4Assertions: 1

You can use any valid CSS selector, like

1get('/records')
2 ->assertOk()
3 ->assertElementExists('#main') //Id selector
4 ->assertElementExists('.some-class') //Class selector
5 ->assertElementExists('p') //Tag selector
6 ->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 div
2 
3Tests: 1 passed
4Assertions: 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->title
12 ]);
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_year
12 ]);
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_name
14 ])
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->name
13 ])
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->name
32 ])
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->isbn
12 ]);
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->name
13 ])
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->part
12 ]);
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->name
28 ])
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_name
18 ])
19 ->containsInput([
20 'name' => 'author[]',
21 'list' => 'authors',
22 'value' => $this->book->authors[1]->last_name . ', ' . $this->book->authors[1]->first_name
23 ])
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

© 2022 Tray2.se. All rights reserved.

This site uses Torchlight for syntax highlighting.