Blog header image

Rebuilding my invoicing application in Laravel, Part 6 - Clients Section

Posted on Dec 6th, 2017

## Goal Now I am ready to start working on the client frontend section. Here I will introduce Laravel Dusk as use it to built out the basic functionality for my Vue components. In this "clients" section, I will need to provide a way to display a list of clients. ## Templating Up to now, I haven't done any frontend updates. So before doing browser tests, I'll need to get this ready. So before I go further, I will update some views and add assets such as CSS and JS. First, I'll remove some of Laravel's default scaffolding and rename the "app" layout to "master". ```console rm app/Http/Controllers/HomeController.php rm resources/views/home.blade.php rm resources/views/welcome.blade.php mv resources/views/layouts/app.blade.php resources/views/layouts/master.blade.php ``` Next, I'll update the master layout. `resources/views/layouts/master.blade.php` ```html Invoicing
``` ## JS/CSS My CSS/JS scaffolding framework of choice is [UIkit]( I'll use NPM to install it. ```console npm install uikit ``` UIkit provides a decent "base" theme to extend from that I will use to mock up most of the frontend. I'll use the source LESS file to make any "theme" changes. First, I need to create a LESS file. ```console mkdir resources/assets/less && touch resources/assets/less/app.less ``` I'll then import in the base theme. `resources/assets/less/app.less` ```less @import "../../../node_modules/uikit/src/less/uikit.theme.less"; ``` Next up is loading the JS. I'll removed bootstrap and install uikit. `resources/assets/js/app.js` ```js try { window.$ = window.jQuery = require('jquery'); } catch (e) {} import UIkit from 'uikit'; import Icons from 'uikit/dist/js/uikit-icons'; // loads the Icon plugin UIkit.use(Icons); window.UIkit = require('uikit'); ``` `package.json` ``` "devDependencies": { "axios": "^0.16.2", "cross-env": "^5.0.1", "laravel-mix": "^1.0", "less": "^3.0.0-alpha.3", "less-loader": "^4.0.5", "lodash": "^4.17.4", "vue": "^2.1.10", "yarn": "*" }, "dependencies": { "jquery": "^3.2.1", "uikit": "^3.0.0-beta.30" } ``` Note: I had to add yarn to get the correct less compiling packages. ```console npm install ``` All that is left is to update the webpack.mix.js file and compile the assets. ```js mix.js('resources/assets/js/app.js', 'public/js') .less('resources/assets/less/app.less', 'public/css'); ``` ```console npm run watch ``` Note: Vue is setup by default in the app.js file. ## Tests and Development Ok, let's just create and setup clients page using Dusk. ```console php artisan dusk:page ClientsPage ``` `tests/Browser/Pages/ClientsPage.php` ```php class ClientsPage extends BasePage { /** * Get the URL for the page. * * @return string */ public function url() { return '/clients'; } /** * Assert that the browser is on the page. * * @param Browser $browser * @return void */ public function assert(Browser $browser) { $browser->assertPathIs($this->url()); } /** * Get the element shortcuts for the page. * * @return array */ public function elements() { return [ // ]; } } ``` The first test I want, is one that will check for a Vue component that has a list of clients. ```console php artisan dusk:make ClientsPageTest ``` `tests/Browser/ClientsPageTest.php` ```php class ClientsPageTest extends DuskTestCase { use DatabaseMigrations; /** * @test */ public function clients_vue_component_has_a_list_of_clients() { $clients = factory(Client::class, 3)->create(); $browser->loginAs($this->createUser()) ->visit(new ClientsPage) ->assertVue('clients', $clients->toArray(), '@clients-component'); } } ``` Note that I created a `$this->createUser()` helper in the parent class. ```console php artisan dusk --filter ClientsPageTest Facebook\WebDriver\Exception\UnknownServerException: unknown error: Cannot read property '__vue__' of null ``` Looks like it wasn't able to find the "clients" Vue component. It is at this point where I am tempted to fire up a browser and see what is going on. But it really helps save time to trust the tools and figure out why the test fails. In this case, I know there is no `/clients` uri. Maybe I should have created a basic feature test to check page loads? Why not? If I have a "PageLoad" test, I could at least have this to run as a quick way to fix any simple errors. ```console php artisan make:test PageLoadTest ``` `tests/Feature/PageLoadTest.php` ```php class PageLoadTest extends TestCase { use RefreshDatabase; /** * @test */ public function clients_page_loads() { $this->call('GET', 'clients')->assertRedirect('login'); $this->actingAsNewUser()->call('GET', 'clients')->assertStatus(200); } } ``` Notice I'm also making sure guests cannot view it. Of course, this will show a 404 error. So I'll create a simple view route and return a "clients" view. This view will extend the master layout. `routes/web.php` ```php Route::get('clients', 'clients.index')->middleware('auth'); ``` ```console mkdir resources/views/clients && touch resources/views/clients/index.blade.php ``` `resources/view/clients/index.blade.php` ```blade @extends('layouts.master') @section('content') @endsection ``` This test passes. However, we still need to build the "clients" Vue component to get the browser test working. Laravel has a default Vue component called "Example.vue". Let's just rename and use it for our "clients" component. ```console mv resources/assets/js/components/Example.vue resources/assets/js/components/Clients.vue ``` Then update the reference in the JS. `resources/assets/js/app.js` ```js Vue.component('clients', require('./components/Clients.vue')); ``` I'll update the component and start really simple. `resources/assets/js/components/Clients.vue` ```js ``` Then add it to the view. `resources/views/clients/index.blade.php` ```html @section('content') @endsection ``` This test also fails but at least the page loads, the component renders and the test can see the "clients" data variable. So there is two ways I could load the "clients" data. Now I'll load data into the component. Here is what that will look like. `resources/assets/js/components/Clients.vue` ```js export default { data: function() { return { clients: null } }, created: function() { axios.get('api/clients') .then(response => { this.clients =; }) } } ``` Here I am using [Axios]( a promise base HTTP browser that works great with these Vue components. This still fails, but I would bet it is because it takes just a tiny bit before the clients variable is fully loaded. I'll wait until a list of the clients show up in the table. `tests/Feature/PageLoadTest.php` ```php $this->browse(function (Browser $browser) use ($clients) { $browser->loginAs($this->createUser()) ->visit(new ClientsPage) ->waitFor('td.title') ->assertVue('clients', $clients->toArray(), '@clients-component'); }); ``` ```console php artisan dusk --filter ClientsPageTest PHPUnit 6.3.1 by Sebastian Bergmann and contributors. . 1 / 1 (100%) Time: 1.5 seconds, Memory: 16.00MB OK (1 test, 2 assertions) ``` Yes! It passes. All this was all done without opening a browser! So what about displaying our list. I'll start with just showing the client title. Nice and simple. I know the clients component is successfully loading a list of all clients. So, I'll go ahead and use that data to populate a table in the Vue component. `resources/assets/js/components/Clients.vue` ```html ``` Here I am using the `v-for` Vue directive to bind the clients to this list. This will render the list of clients into the table as well as update whenever the `clients` variable is updated.