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

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.

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.

2023 Phil Mareu - Coder, Traveler & Disc Thrower