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".
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
<!DOCTYPE html>
<html lang="{{ app()->getLocale() }}">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>Invoicing</title>
<!-- Styles -->
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
<div id="app">
@yield('content')
</div>
<!-- Scripts -->
<script src="{{ asset('js/app.js') }}"></script>
</body>
</html>
JS/CSS
My CSS/JS scaffolding framework of choice is UIkit. I'll use NPM to install it.
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.
mkdir resources/assets/less && touch resources/assets/less/app.less
I'll then import in the base theme.
resources/assets/less/app.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
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.
npm install
All that is left is to update the webpack.mix.js file and compile the assets.
mix.js('resources/assets/js/app.js', 'public/js')
.less('resources/assets/less/app.less', 'public/css');
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.
php artisan dusk:page ClientsPage
tests/Browser/Pages/ClientsPage.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.
php artisan dusk:make ClientsPageTest
tests/Browser/ClientsPageTest.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.
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.
php artisan make:test PageLoadTest
tests/Feature/PageLoadTest.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
Route::get('clients', 'clients.index')->middleware('auth');
mkdir resources/views/clients && touch resources/views/clients/index.blade.php
resources/view/clients/index.blade.php
@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.
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
Vue.component('clients', require('./components/Clients.vue'));
I'll update the component and start really simple.
resources/assets/js/components/Clients.vue
<template>
<div>
<table>
<thead>
<tr>
<th>Title</th>
</tr>
</thead>
<tbody>
<tr>
<td class="title"></td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
export default {
data: function() {
return {
clients: null
}
}
}
</script>
Then add it to the view.
resources/views/clients/index.blade.php
@section('content')
<clients dusk="clients-component"></clients>
@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
export default {
data: function() {
return {
clients: null
}
},
created: function() {
axios.get('api/clients')
.then(response => {
this.clients = response.data;
})
}
}
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
$this->browse(function (Browser $browser) use ($clients) {
$browser->loginAs($this->createUser())
->visit(new ClientsPage)
->waitFor('td.title')
->assertVue('clients', $clients->toArray(), '@clients-component');
});
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
<template>
<div>
<table>
<thead>
<tr>
<th>Title</th>
</tr>
</thead>
<tbody>
<tr v-for="client in clients">
<td v-text="client.title"></td>
</tr>
</tbody>
</table>
</div>
</template>
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.