Showing posts with label Mocha. Show all posts
Showing posts with label Mocha. Show all posts

Tuesday 2 March 2021

Vue.js: using TDD to develop a data-entry form

G'day:

Yesterday (well: it'll be a coupla days ago by the time I publish this; it's "yesterday" as I type this bit though), I started to do work to replicate and document a coding exercise I undertook recently. That part of the exercise was "Docker: using TDD to initialise my app's DB with some data". So I've now got database tables ready to receive some data (and to provide some stub data where necessary too). Today I'm shifting back to the front end of things. It's all well and good having a place to put the data, but I need a way to get the data from the end user first. Today's exercise is to TDD the construction of that form. I've currently got very little idea of how I'm gonna approach this, because I started typing this paragraph before I'd really thought too much about it. So this will be… ah it'll probably be a mess, but hey.

Right so the overall requirement here is (this is copy and pasted from the previous article) to construct an event registration form (personal details, a selection of workshops to register for), save the details to the DB and echo back a success page. Simple stuff. Less so for me given I'm using tooling I'm still only learning (Vue.js, Symfony, Docker, Kahlan, Mocha, MariaDB). It might seem odd that I did the storage part of the exercise first, but the form I'm creating will need to use some data to populate one of the form fields, so I decided it would make more sense to do all the DB stuff in one article, up front. Possibly I should have only done enough of the DB side of things to service today's part of the exercise but too late now :-)

(Later update: as it happens I ended-up just stubbing the data in this exercise, so it's completely stand-alone from the previous MariaDB article).

The form needs to be something along these lines:

And when I say "along these lines", I mean "now that I've written it, that's actually pretty-much the mark-up I will use". This is all basic. The only point needing clarification is that the workshops will be sourced from the DB data I created yesterday.

We can distill a bunch of obvious test cases here:

  • it has a fullName input field;
  • it has a phoneNumber input field;
  • it has a emailAddress input field;
  • it has a password input field that does not display the password when typed;
  • it has a workshopsToAttend multiple select field;
  • the workshopsToAttend field sources its data from the back-end system (stubbed for this exercise);
  • all fields are required;
  • it has a button to submit the registration details.

We'll be implementing this as a Vue component, and we'll write some tests to make sure the component implements all this. Later, when we come to put the component in a web page, we'll write a test with expectations as to how the form actually behaves when used. For now we're just testing the component. As with the preceding article dealing with the baseline DB schema and data, I am currently still mulling over the benefits of this level of testing. From a TDD perspective we're at least going through the exercise of coming up with our cases first and then writing code to fulfil the case. It makes sense to automate confirming we have fulfilled the requirements of the case.


As you might recall, I only worked out how to test Vue components a few days ago ("Part 12: unit testing Vue.js components"). I'm madly rereading that article and looking at the code to remind myself how to do. Firstly, I'll insert an initial case: "it will be a Vue component called 'WorkshopRegistration'". To remind me how this stuff works, I'm just gonna write a test to mount the component. Small steps. All these cases will be implemented in frontend/test/unit/WorkshopRegistration.spec.js, unless otherwise specified. Right then… my baseline initial sanity-test is:

import WorkshopRegistrationForm from "../../src/workshopRegistration/components/WorkshopRegistrationForm";

import { shallowMount } from "@vue/test-utils";

import {expect} from "chai";

describe("Tests of WorkshopRegistrationForm component", () => {

    it("should be mountable", () => {
        expect(()=>shallowMount(WorkshopRegistrationForm)).to.not.throw();
    });

});

I'm simply checking that mounting the component doesn't error. Of course the entire test spec will fail at the moment as src/workshopRegistration/components/WorkshopRegistrationForm doesn't exist yet:

 ERROR  Failed to compile with 1 error

This relative module was not found:

* ../../src/workshopRegistration/components/WorkshopRegistrationForm in ./test/unit/WorkshopRegistration.spec.js

But this is fine. It's inelegant, but it's a failing test, and when we fulfil its requirement it will pass, and we're good. I'll create the minimum possible frontend/src/workshopRegistration/components/WorkshopRegistrationForm.vue now:

<template>
    <form></form>
</template>

<script>
export default {
  name: 'WorkshopRegistrationForm'
}
</script>

I needed to leap ahead and put the <form/> tags in there, otherwise I got another compile error:


   1:1  error  The template requires child element vue/valid-template-root

But I also duly (and slightly pedantically, I know) updated the tests too:

let mountComponent = () => shallowMount(WorkshopRegistrationForm);

it("should be mountable", () => {
    expect(mountComponent).to.not.throw();
});

it("should contain a form", () => {
    let component = mountComponent();
    let form = component.find("form");
    expect(form.exists()).to.be.true;
});

I extracted that mountComponent function because I'll need it in every test. Whilst engaging in this pedantry, I again reminded myself that some of these tests will be very transient and short-lived. Now that I have an operational component, as soon as I start testing for form fields, I can ditch these two tests as they are implicitly replaced by other tests that would not work if the component wasn't mountable, and the form the fields are in didn't exist. Remember that - whilst I am pretty experienced with TDD - I am not experienced in either of Vue components, Vue's test utilities, and not even that familiar with Mocha (hey, even my JavaScript is pretty bloody rusty and last used in any anger 6-7yrs ago, and it was all front-end stuff. So Node is new to me too). So all these small repetitive exercises are good for me to learn with, which is half the reason I'm doing them. One cannot expect to be an expert right at the beginning, so I'm being diligent with my learning. The tight-loop red/green/refactor also helps to to focus on the task at hand and reduces the chance that I'll disappear down any rabbitholes or other side tracks. At any given moment I am only either writing a test case, or then writing the code to make that test case pass. No "Other Stuff".


Next I'm going to check for a required field for the registrant's full name:

describe.only("Tests of WorkshopRegistrationForm component", () => {

    let component;

    before("Load test WorkshopRegistrationForm component", () => {
        component = shallowMount(WorkshopRegistrationForm);
    });

    it("should have a required text input for fullName, maxLength 100, and label 'Full name'", () => {
        let fullNameField = component.find("form>input[name='fullName']");

        expect(fullNameField.exists(), "fullName field must exist").to.be.true;
        expect(fullNameField.attributes("type"), "fullName field must have a type of text").to.equal("text");
        expect(fullNameField.attributes("maxlength"), "fullname field must have a maxlength of 100").to.equal("100");
        expect(fullNameField.attributes("required"), "fullName field must be required").to.exist;

        let inputId = fullNameField.attributes("id");
        expect(inputId, "id attribute must be present").to.exist;

        let label = component.find(`form>label[for='${inputId}']`);
        expect(label, "fullName field must have a label").to.exist;
        expect(label.text(), "fullName field's label must have value 'Full name'").to.equal("Full name:");
    });
});

You can see I've already ditched those first two tests, and anticipating every test case is going to need that component to be mounted, I've put that in the before handler. I've also rolled in a coupla other requirements of the field I didn't think of before: I need to enforce the length of the field, plus it needs a label. The label thing was partly an exercise in working out how to check it, I have to admit. This is a learning exercise, remember.

I tested every part of this along the way, and I'm not gonna bore you with that. Here's the last failed case of that lot:

1) Tests of WorkshopRegistrationForm component
   should have a required text input for fullName, maxLength 100, and label 'Full name':

  AssertionError: fullName field's label must have value 'Full name': expected 'WRONG LABEL:' to equal 'Full name:'
  + expected - actual

  -WRONG LABEL:
  +Full name:

The code being tested for that was:

<template>
    <form method="post" action="" class="sample">
        <label for="fullName" class="required">WRONG LABEL:</label>
        <input type="text" name="fullName" required="required" maxlength="100" id="fullName">
    </form>
</template>

I can already see that the tests for the other inputs are gonna be the same logic, just with the specific details extracted, so I'll go and refactor that now:


let inputFieldMetadata = [
    {fieldName : "fullName", type:"text", maxLength: 100, labelText: "Full name"},
    {fieldName : "phoneNumber", type:"text", maxLength: 50, labelText: "Phone number"},
    {fieldName : "emailAddress", type:"text", maxLength: 320, labelText: "Email address"},
    {fieldName : "password", type:"password", maxLength: 255, labelText: "Password"}
];

inputFieldMetadata.forEach((caseValues) => {
    let [fieldName, type, maxLength, labelText] = Object.values(caseValues);

    it(`should have a required ${type} input for ${fieldName}, maxLength ${maxLength}, and label '${labelText}'`, () => {
        let field = component.find(`form>input[name='${fieldName}']`);
        expect(field.exists(), `${fieldName} field must exist`).to.be.true;
        expect(field.attributes("type"), `${fieldName} field must have a type of ${type}`).to.equal(type);
        expect(field.attributes("maxlength"), `${fieldName} field must have a maxlength of ${maxLength}`).to.equal(maxLength.toString());
        expect(field.attributes("required"), `${fieldName} field must be required`).to.exist;

        let inputId = field.attributes("id");
        expect(inputId, "id attribute must be present").to.exist;

        let label = component.find(`form>label[for='${inputId}']`);
        expect(label, `${fieldName} field must have a label`).to.exist;
        expect(label.text(), `${fieldName} field's label must have value '${labelText}'`).to.equal(`${labelText}:`);
    });
});

Having done that refactor, it passes for fullName (so my refactoring was correct), but errors on the other - as yet not-implemented - fields. Perfect.

  Tests of WorkshopRegistrationForm component
    ✓ should have a required text input for fullName, maxLength 100, and label 'Full name'
    1) should have a required text input for phoneNumber, maxLength 50, and label 'Phone number'
    2) should have a required text input for emailAddress, maxLength 320, and label 'Email address'
    3) should have a required password input for password, maxLength 255, and label 'Password'


  1 passing (195ms)
  3 failing

  1) Tests of WorkshopRegistrationForm component
       should have a required text input for phoneNumber, maxLength 50, and label 'Phone number':

      phoneNumber field must exist
      + expected - actual

      -false
      +true
[… etc for the other missing fields too…]

Once I copy and paste the mark-up for the form into the template section of the component, the other tests now… still fail. But this is good, and it validates why I'm doing these tests!

  1) Tests of WorkshopRegistrationForm component
       should have a required text input for phoneNumber, maxLength 50, and label 'Phone number':
     AssertionError: phoneNumber field must have a maxlength of 50: expected undefined to equal '50'

My mark-up from above doesn't have the maxlength attribute. I just caught a bug.

OK enough TDD excitement. I fix the mark-up and now the tests pass:

  Tests of WorkshopRegistrationForm component
    ✓ should have a required text input for fullName, maxLength 100, and label 'Full name'
    ✓ should have a required text input for phoneNumber, maxLength 50, and label 'Phone number'
    ✓ should have a required text input for emailAddress, maxLength 320, and label 'Email address'
    ✓ should have a required password input for password, maxLength 255, and label 'Password'

Now I need to scratch my head about how to test the workshopsToAttend field. The implementation is going to need to read from the database. However my test ain't gonna do that. I'm gonna mock the DB connection out and return known-values for the test. I have - as yet - no idea how to do this. Excuse me whilst I google some stuff…

[… time passes …]


Actually I'm getting ahead of myself. The first case relating to this workshopsToAttend form control is much the same as the tests for the text/password inputs. And there should be a case for that before we start testing the <option> tags present within it. This is easy, but takes a slight refactor of the existing test:

inputFieldMetadata.forEach((caseValues) => {
    let [name, type, maxLength, labelText] = Object.values(caseValues);

    it(`should have a required ${type} input for ${name}, maxLength ${maxLength}, and label '${labelText}'`, () => {
        let field = component.find(`form>input[name='${name}']`);

        expect(field.exists(), `${name} field must exist`).to.be.true;
        expect(field.attributes("required"), `${name} field must be required`).to.exist;
        expect(field.attributes("type"), `${name} field must have a type of ${type}`).to.equal(type);
        expect(field.attributes("maxlength"), `${name} field must have a maxlength of ${maxLength}`).to.equal(maxLength.toString());

        testLabel(field, labelText);
    });
});

it("should have a required workshopsToAttend multiple-select box, with label 'Workshops to attend'", () => {
    let field = component.find(`form>select[name='workshopsToAttend[]']`);

    expect(field.exists(), "workshopsToAttend field must exist").to.be.true;
    expect(field.attributes("required"), "workshopsToAttend field must be required").to.exist;
    expect(field.attributes("multiple"), "workshopsToAttend field must be a multiple-select").to.exist;

    testLabel(field, "Workshops to attend");
});

let testLabel = (field, labelText) => {
    let name = field.attributes("name");
    let inputId = field.attributes("id");
    expect(inputId, "id attribute must be present").to.exist;

    let label = component.find(`form>label[for='${inputId}']`);
    expect(label, `${name} field must have a label`).to.exist;
    expect(label.text(), `${name} field's label must have value '${labelText}'`).to.equal(`${labelText}:`);
};

Much of the test for the <select> was the same as for the text fields: that it exists, that it's required and that it has a label. However the selector is different for the field itself, and that is also its type. Plus there's no maxLength check, but there is a multiple check. I thought about trying to increase the metadata in the array I looped over for each field's tests, and then optionally testing maxLength or multiple if it was present in the metadata. But the logic to vary how to get the field, how to get its type, making the optional attributes optional was making the test code a bit impenetrably "generic", which was a flag to me that I was over-complicating things. One can take DRY too far some times: it should not result in a loss of code clarity. One thing I could lift out, lock-stock, and it actually improves the code's readability is the logic around finding and testing the form field's label. So I've done that. This leaves two fairly concise and logic-free test cases, and another function which is also fairly easy to follow, and now has its own name.

Now I can add the mark-up for just the <select> part, and the tests pass (I'll stop showing passing / failing tests unless there's something noteworthy. You get the idea.

<label for="workshopsToAttend" class="required">Workshops to attend:</label>
<select name="workshopsToAttend[]" multiple="multiple" required="required" id="workshopsToAttend">
</select>

Note the weird way PHP requires form controls that can take multiple-values to be named: as if they're an array. This is appalling, but it's what one needs to do, otherwise one only gets the first of the multiple values exposed to PHP. Pathetic.

Next I had to do more reading to work out how to test and implement the options, the data for which will be sourced by an API call. This is more complicated that the earlier stuff, so I'll treat it step by step.

Firstly, it's not the job of a UI component to know about APIs, it's just its job to present (dynamic ~) mark-up. So I will be abstracting the logic to source the data into a WorkshopService, which I will pass to the component via dependency injection. Vue seems to make this easy by implementing a provide/inject mechanism where the top level Vue application can provide dependencies to its component stack, and any given component can have its dependencies injected into it. This is documented at "Provide / inject". And how to provide the dependencies to a component we're just mounting for testing is referenced in "Mounting Options › Provide" for the Vue 2.x version of Vue Test Utils, and you have to know what you're looking for to see how this has changed for the Vue 3.x version of Vue Test Utils: "Reusability & Composition". This boils down to the mounting of the component becomes this (all the final code for the test spec is at frontend/test/unit/WorkshopRegistration.spec.js):

component = shallowMount(
    WorkshopRegistrationForm,
    {
        global : {
            provide: {
                workshopService : workshopService
            }
        }
    }
);

The difference in the implementation of this between Vue Test Utils for Vue 2.x and 3.x is this intermediary global tier of the mount options. I only found this out thanks to Issue testing provider/inject with Vue 3 composition API #1698, which makes it way more explicit than the docs, and the only reference to this I was able to find via Google.

WorkshopService is currently just a stub that I can mock (frontend/src/workshopRegistration/services/WorkshopService.js):

class WorkshopService {
    getWorkshops() {
        return [];
    }
}

module.exports = WorkshopService;

And the version of it I pass into the component is mocked with Sinon to return a canned response:

let expectedOptions = [
    {value: 2, text:"Workshop 1"},
    {value: 3, text:"Workshop 2"},
    {value: 5, text:"Workshop 3"},
    {value: 7, text:"Workshop 4"}
];

before("Load test WorkshopRegistrationForm component", () => {
    let workshopService = new WorkshopService();
    sinon.stub(workshopService, "getWorkshops").returns(expectedOptions);
    component = shallowMount(
    	// etc, as per earlier code snippet

From there the actual test is pretty simple and predictable:

it("should list the workshop options fetched from the back-end", () => {
    let options = component.findAll(`form>select[name='workshopsToAttend[]']>option`);

    expect(options).to.have.length(expectedOptions.length);
    options.forEach((option, i) => {
        expect(option.attributes("value"), `option[${i}] value incorrect`).to.equal(expectedOptions[i].value.toString());
        expect(option.text(), `option[${i}] text incorrect`).to.equal(expectedOptions[i].text);
    });
});

I just check each of the options in the form against the values I expect (and have been returned by the call to workshopService.getWorkshops. Conveniently this also tests the mocking is working: this is the first time I've used Sinon, so this is beneficial for me.

Now that I have a failing test, I can go ahead and update the component template to render the options (frontend/src/workshopRegistration/components/WorkshopRegistrationForm.vue):

<select name="workshopsToAttend[]" multiple="multiple" required="required" id="workshopsToAttend">
    <option v-for="workshop in workshops" :value="workshop.value" :key="workshop.value">{{workshop.text}}</option>
</select>

And also the template initialisation code to call the WorkshopService to get the options (same file as above, in case that's not clear):

<script>
export default {
  name: "WorkshopRegistrationForm",
    inject: ["workshopService"],
    data() {
      return {workshops: []};
    },
    mounted() {
      this.workshops = this.workshopService.getWorkshops();
    }
}
</script>

Note that because I am using the provide / inject mechanism, the component doesn't need to know where it gets workshopService from, all it needs to know is that something will inject it (so far: just my test, but once this is actually implemented on a page, the Vue application will provide it.


OK so the form is now populated. We need to test that we have a button, and when we click it we submit the form. As per earlier I am typing this before I have any idea how to do this, so I need to read some more. I guess we can at least test the button is present in the form, to start with:

it("should have a button to submit the registration", () => {
    let button = component.find(`form>button`);

    expect(button.exists(), "submit button must exist").to.be.true;
    expect(button.text(), "submit button must be labelled 'register'").to.equal("Register");
});

With that, I can at least add the button tot he component template:

<button>Register</button>

Now I just need to work out what the button needs to do, and how to test for it. And… err… how to implement that.

I've decided this component is going to represent both states of the registration: the initial form, and the resultant confirmation info once the data has been submitted and processed. When the button is pressed it's going to hide the form, and show a transition whilst the registration details are being processed. Once the processing call comes back, it's going to hide the transition and show the results. So our first test case is that when the button is clicked to submit the form, it shows the transition instead of the form.


Writing some code before writing the tests, but still doing TDD

Before I start on this next round of development, I will admit I am spiking a lot of test code here. I don't mean "code that is tests", I mean "I'm writing and discarding a bunch of code to work out and test how Vue.js does stuff". For example I'm going to monkey around with showing / hiding the <div> elements with each of the stages of the process. However once I've nailed how Vue.js works, I will discard any source code I have, get my test case written, and then implement just the source code to make the test pass. I make a point of saying this because it is just a TDD hint that it's OK to write scratch code to check stuff before one writes the tests, but once one is clear that the scratch code is actually how things need to be, discard it (even cut it out and paste it somewhere else for now), write the test for the test case. Note: I don't mean write tests for for the code you just hide away in a scratch file, but for the test case! Those are two different things! Once you have your test case written, then implement the code to pass the test. It can be inspired by your scratch code, but you should make sure to focus on inplementing just enough code to make the case pass. As development - even of scratch code - is an iterative and layered endeavour, it's likely the final state of your scratch code is ahead of where the code for the test case might be, but make sure to - to belabour the point - only implement the code for the test. Not any code for the next test. Remember TDD is about test cases leading code design, and this tight red/green cycle helps stay focused on solving the issue one case at a time. It also helps weed out any rabbit-hole code you might have in your scratch code that ends up not actually being necessary to fulfil the test case requirements.

OK, two days of reading, experimenting, and generally fart-arsing around, I have achieved two things:

  • I have learned enough about Vue.js and Vue Test Utils (and Mocha, and Sinon… and even some CSS!) to do the work I need to do;
  • I have identified the remaining test cases I need to implement to round-out the testing of the component.

Here is the state of the work so far:

  Tests of WorkshopRegistrationForm component
     should have a required text input for fullName, maxLength 100, and label 'Full name'
     should have a required text input for phoneNumber, maxLength 50, and label 'Phone number'
     should have a required text input for emailAddress, maxLength 320, and label 'Email address'
     should have a required password input for password, maxLength 255, and label 'Password'
     should have a required workshopsToAttend multiple-select box, with label 'Workshops to attend'
     should list the workshop options fetched from the back-end
     should have a button to submit the registration
    - should leave the submit button disabled until the form is filled


  7 passing (48ms)
  1 pending

 MOCHA  Tests completed successfully

I've added the next test case there too. I'll implement that now:

it.only("should leave the submit button disabled until the form is filled", async () => {
    let button = component.find("form.workshopRegistration button");

    expect(button.attributes("disabled"), "button should be disabled").to.exist;

    let form = component.find("form.workshopRegistration")
    form.findAll("input").forEach((input)=>{
        input.setValue("TEST_INPUT_VALUE");
    });
    form.find("select").setValue(5);

    await flushPromises();

    expect(button.attributes("disabled"), "button should be enabled").to.not.exist;
});

Here I am doing this:

  • checking the button starts as being disabled when the form is empty;
  • sticking some values in the form;
  • checking the button is now enabled because the form is OK to submit.

Because the act of setting values is asynchronous, I need to wait for them to finish. I could await each one, but when reading about how to test this stuff I landed on this flush-promises library which does what it suggests: blocks until all pending promises are resolved. This just makes the test code simpler to follow.

Obviously this fails at the moment, because there's nothing controlling the readiness of the button. It's just this: <button>Register</button>:

  1) Tests of WorkshopRegistrationForm component
       should leave the submit button disabled until the form is filled:
     AssertionError: button should be disabled: expected undefined to exist
      at Context.<anonymous> (dist/js/webpack:/test/unit/WorkshopRegistration.spec.js:99:1)

Line 99 references the first expectation checking that the disabled attribute exists. OK so we need to write some logic in the template that will change a flag based on whether the form fields have values. I'll make this a computed property. These are basically magic and the Vue app keeps track of their values in near-enough real time:

computed : {
    isFormUnready: function () {
        return this.formValues.fullName.length === 0
            || this.formValues.phoneNumber.length === 0
            || this.formValues.workshopsToAttend.length === 0
            || this.formValues.emailAddress.length === 0
            || this.formValues.password.length === 0
    }
}

And then we can reference that in "logic" in the button:

<button :disabled="isFormUnready">Register</button>

And that's it. The test case now passes:

  Tests of WorkshopRegistrationForm component
     should leave the submit button disabled until the form is filled


  1 passing (48ms)

 MOCHA  Tests completed successfully

I'm pretty impressed with how easy that ended up being. I mean it took me ages to work out how to approach it, but the final result was simple and the code is pretty clear.

The next test is as follows:

it("should disable the form and indicate data is processing when the form is submitted", async () => {
    await submitPopulatedForm();

    let fieldset = component.find("form.workshopRegistration fieldset");
    expect(fieldset.attributes("disabled"), "fieldset should be disabled").to.exist;

    let button = component.find("form.workshopRegistration button");
    expect(button.text(), "Button should now indicate it's processing").to.equal("Processing…");
});

let submitPopulatedForm = async () => {
    await populateForm();
    await component.find("form.workshopRegistration button").trigger("click");
    await flushPromises();
};

let populateForm = async () => {
    let form = component.find("form.workshopRegistration")
    form.findAll("input").forEach((input)=>{
    	let name = input.attributes("name");
        input.setValue("TEST_INPUT_VALUE" + name);
    });
    form.find("select").setValue(5);

    await flushPromises();
};
  • Given the form-filling, processing and summary are all going to be done within the one page without reloads, we need to visually indicate processing is taking place, and also guard against the use tampering with the form once it's submitted by disabling it.
  • We have abstracted out a function to populate and submit the form. This is a bit of premature refactoring I guess, but I know all the rest of the cases are going to need to populate and submit the form, so I'll do it now. Note I've also refactored the previous test to use populateForm instead of doing this logic inline.
  • In a similar fashion to the previous test, we check that the fieldset has been disabled…;
  • and the button now will indicate that processing is taking place.

Note that I'm checking on the fieldset not the form, because the form itself can't be disabled: disablement is a function of individual form controls, or groups of them in a fieldset

The implementation of this is dead easy again (I've elided irrelevant code from this where indicated):

const REGISTRATION_STATE_FORM = "form";
const REGISTRATION_STATE_PROCESSING = "processing";

export default {
    // [...]
    data() {
        return {
            registrationState: REGISTRATION_STATE_FORM,
            // [...]
        };
    },
    created() {
        this.REGISTRATION_STATE_FORM = REGISTRATION_STATE_FORM;
        this.REGISTRATION_STATE_PROCESSING = REGISTRATION_STATE_PROCESSING;
    },
    // [...]
    methods : {
        processFormSubmission(event) {
            event.preventDefault();
            this.registrationState = REGISTRATION_STATE_PROCESSING;
        }
    },
    computed : {
        // [...]
        isFormDisabled: function() {
            return this.registrationState !== REGISTRATION_STATE_FORM;
        },
        submitButtonLabel: function() {
            return this.registrationState === REGISTRATION_STATE_FORM ? "Register" : "Processing&hellip;";
        }
    }
}
  • I've added some constants to represent the states I'll be using so I'm not reproducing strings all over the place.
  • I need to load them into the application's memory too so they're accessible in the template too (this is just the way it needs to be done in Vue).
  • I've set the starting state of the process to be form.
  • I've added an event handler for the form submission that simply kills the default form submission action, then sets the current state to be processing.
  • Similar to before, I have computed properties for the elements on the form that need to change:
    • disabling it
    • Changing the button text.

In the template, this is just a matter of acting on those two computed properties:

<template>
    <form method="post" action="" class="workshopRegistration">
        <fieldset :disabled="isFormDisabled">
            <!-- ... -->
            <button @click="processFormSubmission" :disabled="isFormUnready" v-html="submitButtonLabel"></button>
        </fieldset>
    </form>
</template>

Once again: the code to achieve the functionality is very simple. And this test case now passes too:

  Tests of WorkshopRegistrationForm component
     should disable the form and indicate data is processing when the form is submitted


  1 passing (45ms)

 MOCHA  Tests completed successfully

Now we need to make sure we save the data (or at least: ask the WorkshopService to take care of that):


it("should send the form values to WorkshopService.saveWorkshopRegistration when the form is submitted", async () => {
    sinon.spy(workshopService, "saveWorkshopRegistration");

    await submitPopulatedForm();

    expect(
        workshopService.saveWorkshopRegistration.calledOnceWith({
            fullName: TEST_INPUT_VALUE + "fullName",
            phoneNumber: TEST_INPUT_VALUE + "phoneNumber",
            workshopsToAttend: [TEST_SELECT_VALUE],
            emailAddress: TEST_INPUT_VALUE + "emailAddress",
            password: TEST_INPUT_VALUE + "password"
        }),
        "Incorrect values sent to WorkshopService.saveWorkshopRegistration"
    ).to.be.true;
});

This also requires adding a stub method to WorkshopService:

class WorkshopService {
    // ...

    saveWorkshopRegistration() {
    }
}

Here we:

  • Put a spy on that saveWorkshopRegistration method;
  • Submit the form;
  • Make sure saveWorkshopRegistration received the values from the form.
  • Oh I've also refactored the previously hard-coded test value strings into constants, because I now need to compare them to themselves, and I wanted to make sure they're always the same.

To implement the code for this test is as simple as adding the call to saveWorkshopRegistration to the form-submit-handler:

processFormSubmission(event) {
    event.preventDefault();
    this.registrationState = REGISTRATION_STATE_PROCESSING;
    this.workshopService.saveWorkshopRegistration(this.formValues);
}

And yeah sure, nothing gets saved at the moment because saveWorkshopRegistration is just stubbed. But it's not the job of this Vue component to do the saving. It's the job of WorkshopService to do that. And we'll get to that later. We're just testing the component logic works for now.

And this test case passes:

  Tests of WorkshopRegistrationForm component
     should send the form values to WorkshopService.saveWorkshopRegistration when the form is submitted


  1 passing (46ms)

 MOCHA  Tests completed successfully

Almost done. This next case just checks the static parts of the summary display is there. I'm doing this separate from testing the values because when I initially implemented it all in one, it was way too big for one test case, and there seemed to be a reasonable slice point in the code between the bit here, and the bit doing the values:

it("should display the registration summary 'template' after the registration has been submitted", async () => {
    await submitPopulatedForm();

    let summary = component.find("dl.workshopRegistration");
    expect(summary.exists(), "summary must exist").to.be.true;

    let expectedLabels = ["Registration Code", "Full name", "Phone number", "Email address", "Workshops"];
    let labels = summary.findAll("dt");

    expect(labels).to.have.length(expectedLabels.length);
    expectedLabels.forEach((label, i) => {
        expect(labels[i].text()).to.equal(`${label}:`);
    });
});

Here we are doing the following:

  • Submitting the form as per usual;
  • Verifying the summary element - a definition list (<dl>) - is there;
  • Checking that the expected labels are implemented as definition terms (<dt>).

The mark-up for the implementation is as follows:

<template>
    <form method="post" action="" class="workshopRegistration" v-if="registrationState !== REGISTRATION_STATE_SUMMARY">
        <!-- ... -->
    </form>

    <dl v-if="registrationState === REGISTRATION_STATE_SUMMARY" class="workshopRegistration">
        <dt>Registration Code:</dt>
        <dd></dd>

        <dt>Full name:</dt>
        <dd></dd>

        <dt>Phone number:</dt>
        <dd></dd>

        <dt>Email address:</dt>
        <dd></dd>

        <dt>Workshops:</dt>
        <dd></dd>
    </dl>
</template>

Just note the conditions on the form and dl to control which of the two is displayed. The condition on the <form> is !== REGISTRATION_STATE_SUMMARY as opposed to === REGISTRATION_STATE_FORM because we want the form to stay there in its disabled and processing state whilst the submission is being handled. The the component code I just add a new registration state of "summary" via a constant, and switch to that state after the data is saved:

// ...
const REGISTRATION_STATE_SUMMARY = "summary";

export default {
    // ...
    created() {
        // ...
        this.REGISTRATION_STATE_SUMMARY = REGISTRATION_STATE_SUMMARY;
    },
    methods : {
        processFormSubmission(event) {
            event.preventDefault();
            this.registrationState = REGISTRATION_STATE_PROCESSING;
            this.summaryValues = this.workshopService.saveWorkshopRegistration(this.formValues);
            this.registrationState = REGISTRATION_STATE_SUMMARY;
        }
    },
    // ...
}

This test now passes:

  Tests of WorkshopRegistrationForm component
     should display the registration summary 'template' after the registration has been submitted


  1 passing (53ms)

 MOCHA  Tests completed successfully

However we have a problem. Some of the other tests now break!

    1) should disable the form and indicate data is processing when the form is submitted
    2) should send the form values to WorkshopService.saveWorkshopRegistration when the form is submitted
    3) should display the registration summary 'template' after the registration has been submitted


  8 passing (319ms)
  3 failing

  1) Tests of WorkshopRegistrationForm component
       should disable the form and indicate data is processing when the form is submitted:
     Error: Cannot call attributes on an empty DOMWrapper.

  2) Tests of WorkshopRegistrationForm component
       should send the form values to WorkshopService.saveWorkshopRegistration when the form is submitted:
     Error: Cannot call findAll on an empty DOMWrapper.

  3) Tests of WorkshopRegistrationForm component
       should display the registration summary 'template' after the registration has been submitted:
     Error: Cannot call findAll on an empty DOMWrapper.



 MOCHA 3 failure(s)

Note that this includes the test I just created, and passes when run on its own. When running each of these tests separately, only the should disable the form and indicate data is processing when the form is submitted one was always failing now.

I twigged that if a test passed in isolation, but fairly when run along with other tests, there must be some shared code that is stateful, and ends up in the wrong state between tests. I determined that this was because I had a before handler, when I really needed to run that code beforeEach:

before("Load test WorkshopRegistrationForm component", () => {
    workshopService = new WorkshopService();
    sinon.stub(workshopService, "getWorkshops").returns(expectedOptions);

    component = shallowMount(
        // etc
    );
});

Basically I'm re-using the same component in all tests, and once a test changes the registrationState, the next test could start running with the component showing the summary mark-up, rather than the form it's expecting to see. This only started being a problem when I implemented the transition from one to the other in the previous test/dev cycle. This was easy to spot because of the really small increments I'm doing with my TDD. As well as changing the handler to be beforeEach I also had to await on the shallowMount call, as whilst before blocks until all its code has completed, beforeEach does not. So without the await, often the component was not ready before the test started to try to use it. To be clear, the change was this:

beforeEach("Load test WorkshopRegistrationForm component", async () => {
    workshopService = new WorkshopService();
    sinon.stub(workshopService, "getWorkshops").returns(expectedOptions);

    component = await shallowMount(
        // etc
    );
});

Now when I run the tests I only get the one failure:

    1) should disable the form and indicate data is processing when the form is submitted


  10 passing (239ms)
  ` failing

  1) Tests of WorkshopRegistrationForm component
       should disable the form and indicate data is processing when the form is submitted:
     Error: Cannot call attributes on an empty DOMWrapper.
 MOCHA 1 failure(s)

And the issue here is kind of the same. The code in question is this:

it("should disable the form and indicate data is processing when the form is submitted", async () => {
    await submitPopulatedForm();

    let fieldset = component.find("form.workshopRegistration fieldset");
    expect(fieldset.attributes("disabled"), "fieldset should be disabled").to.exist;

    let button = component.find("form.workshopRegistration button");
    expect(button.text(), "Button should now indicate it's processing").to.equal("Processing…");
});

The problem is that now that the registrationState changes to summary, by the time we're checking these things about the form, Vue has already hidden it, and shown the summary. On the actual UI, and once the data needs to be sent off to a remote service to save and return the summary data there'll likely be a short pause when we're in that processing state, but it'll never be long. And it's kinda instantaneous now. As a refresher, this is the code that runs when the form is submitted:

processFormSubmission(event) {
    event.preventDefault();
    this.registrationState = REGISTRATION_STATE_PROCESSING;
    this.summaryValues = this.workshopService.saveWorkshopRegistration(this.formValues);
    this.registrationState = REGISTRATION_STATE_SUMMARY;
}

There's not much time between those two transitions. Fortunately Vue lets me watch what happens to any of its data whenever I want, so I can just leverage this:

it("should disable the form and indicate data is processing when the form is submitted", async () => {
    let lastLabel;
    component.vm.$watch("submitButtonLabel", (newValue) => {
        lastLabel = newValue;
    });

    let lastFormState;
    component.vm.$watch("isFormDisabled", (newValue) => {
        lastFormState = newValue;
    });

    await submitPopulatedForm();

    expect(lastLabel).to.equal("Processing&hellip;");
    expect(lastFormState).to.be.true;
});

I track each change to submitButtonLabel and isFormDisabled, and their last state is how the form was before it was hidden completely.

Now all my tests are passing again. There one last case to do: testing the values on the summary page:

it("should display the summary values in the registration summary", async () => {
    const summaryValues = {
        registrationCode : "TEST_registrationCode",
        fullName : "TEST_fullName",
        phoneNumber : "TEST_phoneNumber",
        emailAddress : "TEST_emailAddress",
        workshopsToAttend : [{value: "TEST_workshopToAttend_VALUE", text:"TEST_workshopToAttend_TEXT"}]
    };
    sinon.stub(workshopService, "saveWorkshopRegistration").returns(summaryValues);

    await submitPopulatedForm();

    let summary = component.find("dl.workshopRegistration");
    expect(summary.exists(), "summary must exist").to.be.true;

    let expectedValues = Object.values(summaryValues);
    let values = summary.findAll("dd");
    expect(values).to.have.length(expectedValues.length);

    let expectedWorkshopValue = expectedValues.pop();
    let actualWorkshopValue = values.pop();

    let ddValue = actualWorkshopValue.find("ul>li");
    expect(ddValue.exists()).to.be.true;
    expect(ddValue.text()).to.equal(expectedWorkshopValue[0].text);

    expectedValues.forEach((expectedValue, i) => {
        expect(values[i].text()).to.equal(expectedValue);
    });
});

This looks more complicated than it is:

  • We mock saveWorkshopRegistration to return known values;
  • We verify we're on the summary page;
  • We check that there are the right number of values in the summary;
  • We take the workshopsToAttend values out, because they need to be tested differently from the rest: they're the values from the multi-select, so the mark-up is different to accommodate that;
  • We test that workshopsToAttend value is in its own <ul>;
  • We loop over the other values and make sure they're all what we expect.

This test is a wee bit fragile because it relies on workshopsToAttend to be the last key in the values object, and the last <dd> in the mark-up, but it'll do for now.

There's only mark-up to change for this one: adding the value output to the summary template:

<dl v-if="registrationState === REGISTRATION_STATE_SUMMARY" class="workshopRegistration">
    <dt>Registration Code:</dt>
    <dd>{{ summaryValues.registrationCode }}</dd>

    <dt>Full name:</dt>
    <dd>{{ summaryValues.fullName }}</dd>

    <dt>Phone number:</dt>
    <dd>{{ summaryValues.phoneNumber }}</dd>

    <dt>Email address:</dt>
    <dd>{{ summaryValues.emailAddress }}</dd>

    <dt>Workshops:</dt>
    <dd>
        <ul>
            <li v-for="workshop in summaryValues.workshopsToAttend" :key="workshop.value">{{workshop.text}}</li>
        </ul>
    </dd>
</dl>

(You can also now see why we needed to test the values for workshopsToAttend separately: the mark-up is more complicated).


Okey doke. In theory now, the thing should "work" now, as far as the component's behaviour goes. Obviously the WorkshopService doesn't do anything, so we'll only get placeholder values, but that's something at least. Let's… make one last tweak and have a look. We need to return some sample values from WorkshopService.saveWorkshopRegistration, because it's that data that populates the summary display. I'm just gonna "loopback" the values from the form, plus add a placeholder registration code (which will be a GUID, but we 'll just mask it for now):

saveWorkshopRegistration(details) {
    let allWorkshops = this.getWorkshops();
    let selectedWorkshops = allWorkshops.filter((workshop) => {
        return details.workshopsToAttend.indexOf(workshop.value) >= 0;
    });

    return {
        registrationCode : "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
        fullName : details.fullName,
        phoneNumber : details.phoneNumber,
        emailAddress : details.emailAddress,
        workshopsToAttend : selectedWorkshops
    };
}

That'll do. Oh and of course we need to actually stick the thing on a web page somewhere. I need to add a page to frontend/vue.config.js:

module.exports = {
    pages : {
        // ...
        workshopRegistration: {
            entry: "src/workshopRegistration/main.js",
            template: "public/workshopRegistration.html",
            filename: "workshopRegistration.html"
        }
    },
    // ...
}

And put that frontend/src/workshopRegistration/main.js file in place (seems to be the same for every page):

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

And lastly the parent App.vue template which has a <workshop-registration-form /> tag in its template block, as well as importing the WorkshopRegistrationForm component file, and also doing the dependency injection of WorkshopService:

<template>
    <workshop-registration-form></workshop-registration-form>
</template>

<script>
import WorkshopRegistrationForm from "./components/WorkshopRegistrationForm";
import WorkshopService from "../../src/workshopRegistration/services/WorkshopService";

export default {
    name: 'App',
    components: {
        WorkshopRegistrationForm
    },
    provide: {
        workshopService: new WorkshopService()
    }
}
</script>

Now we have a web page browseable at /workshopRegistration.html


Let's have a look:

To start with no data is populated, and the Register button is disabled. So far so good.

I've filled some data in, but the button is still disabled. Cool.

I've finished filling in the form and the Register button is now active. Nice one.

Now I'm gonna click Register and… [cringe]…

holy f*** it actually worked!

That is not feigned surprise. That is the first time I tried to do that… today anyhow. Yesterday with the scratch version of the code I messed around and got it working correctly, got the CSS all "working" somehow that looked half decent, but I hadn't checked the summary view today at all.

I was expecting to have to fix stuff up and make excuses and waste more of your time reading this, but I guess I'm done with this exercise.


The next exercise will be to implement the back-end that WorkshopService needs to talk to (see "Symfony & TDD: adding endpoints to provide data for front-end workshop / registration requirements"). This is a switch from Vue.js to PHP and Symfony. That should be interesting as my Symfony skills are pretty marginal at best. I'd say after this exercise I now know more about Vue.js than I do about Symfony, by way of comparison.

Sorry this one was so long, but it was kinda one atom of work, and whilst I could ahve cut over into a different article when I shifted from the form test cases to the summary test cases, there wasn't much to do for the summary as it turned out, so I just kept going. I do have to say I enjoyed this exercise, and I learned a lot about Vue, Vue Test Utils, Sinon and even some more about Mocha. Plus a chunk of just JS stuff I didn't know before. And it was a useful TDD exercise to put myself through as well, and I actually think it was worth it. Anyway, enough. I'm outa here.

Righto.

--
Adam

Friday 19 February 2021

Part 12: unit testing Vue.js components

G'day

OKOK, another article in that bloody series I've been doing. Same caveats as usual: go have a breeze through the earlier articles if you feel so inclined. That said this is reasonably stand-alone.

  1. Intro / Nginx
  2. PHP
  3. PHPUnit
  4. Tweaks I made to my Bash environment in my Docker containers
  5. MariaDB
  6. Installing Symfony
  7. Using Symfony
  8. Testing a simple web page built with Vue.js using Mocha, Chai and Puppeteer
  9. I mess up how I configure my Docker containers
  10. An article about moving files and changing configuration
  11. Setting up a Vue.js project and integrating some existing code into it
  12. Unit testing Vue.js components (this article)

This is really very frustrating. You might recall I ended my previous article with this:

Now I just need to work out how to implement a test of just the GreetingMessage.vue component discretely, as opposed to the way I'm doing it now: curling a page it's within and checking the page's content. […]

Excuse me whilst I do some reading.

[Adam runs npm install of many combinations of NPM libs]

[Adam downgrades his version of Vue.js and does the npm install crap some more]

OK screw that. Seems to me - on initial examination - that getting all the libs together to make stand-alone components testable is going to take me longer to work out than I have patience for. I'll do it later. […] Sigh.

I have returned to this situation, and have got everything working fine. Along the way I had an issue with webpack that I eventually worked around, but once I circled back to replicate the work and write this article, I could no longer replicate the issue. Even rolling back to the previous version of the application code and step-by-step repeating the steps to get to where the problem was. This is very frustrating. However other people have had similar issues in the past so I'm going to include the steps to solve the problem here, even if I have to fake the issue to get error messages, etc.

Right so I'm back with the Vue.js docs regarding testing: "Vue.JS > Testing > Component Testing". The docs are incredibly content-lite, and pretty much just fob you off onto other people to work out what to do. Not that cool, and kinda suggests Vue.js considers the notion of unit testing pretty superficially. I did glean I needed to install a coupla Node modules.

First, @testing-library/vue, for which I ran into a glitch immediately:

root@00e2ea0a3109:/usr/share/fullstackExercise# npm install --save-dev @testing-library/vue
npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR!
npm ERR! While resolving: full-stack-exercise@2.13.0
npm ERR! Found: vue@3.0.5
npm ERR! node_modules/vue
npm ERR!   vue@"^3.0.0" from the root project
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer vue@"^2.6.10" from @testing-library/vue@5.6.1
npm ERR! node_modules/@testing-library/vue
npm ERR!   dev @testing-library/vue@"*" from the root project
npm ERR!
npm ERR! Fix the upstream dependency conflict, or retry
npm ERR! this command with --force, or --legacy-peer-deps
npm ERR! to accept an incorrect (and potentially broken) dependency resolution.
npm ERR!
npm ERR! See /var/cache/npm/eresolve-report.txt for a full report.

npm ERR! A complete log of this run can be found in:
npm ERR!     /var/cache/npm/_logs/2021-02-19T11_29_15_901Z-debug.log
root@00e2ea0a3109:/usr/share/fullstackExercise#

The current version of @testing-library/vue doesn't work with the current version of Vue.js. Neato. Nice one Vue team. After some googling of "wtf?", I landed on an issue someone else had raised already: "Support for Vue 3 #176": I need to use the next branch (npm install --save-dev @testing-library/vue@next). This worked OK.

The other module I needed was @vue/cli-plugin-unit-mocha. That installed with no problem. This all gives me the ability to run vue-cli-service test:unit, which will run call up Mocha and run some tests. The guidance is to set this up in package.json, thus:

  "scripts": {
    "test": "mocha test/**/*Test.js",
    "serve": "vue-cli-service serve --watch",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint",
    "test:unit": "vue-cli-service test:unit test/unit/**/*.spec.js"
  },

Then one can jus run it as npm run test:unit.

I looked at the docs for how to test a component ("Unit Testing Vue Components" - for Vue v2, but there's no equivalent page in the v3 docs), and knocked together an initial first test which would just load the component and do nothing else: just to check everything was running (frontend/test/unit/GreetingMessage.spec.js (final version) on Github):

import GreetingMessage from "../../src/gdayWorld/components/GreetingMessage";

import { shallowMount } from "@vue/test-utils";

import {expect} from "chai";

describe("Tests of GreetingMessage component", () => {
    it("should successfully load the component", () => {
        let greetingMessage = shallowMount(GreetingMessage, {propsData: {message: "G'day world"}});

        expect(true).to.be.true;
    });
});

Here's where I got to the problem I now can't replicate. When I ran this, I got something like:

root@eba0490b453d:/usr/share/fullstackExercise# npm run test:unit

> full-stack-exercise@2.13.0 test:unit
> vue-cli-service test:unit test/unit/**/*.spec.js

 WEBPACK  Compiling...

  [=========================] 98% (after emitting)

 ERROR  Failed to compile with 1 error

error  in ./node_modules/mocha/lib/cli/cli.js

Module parse failed: Unexpected character '#' (1:0)
File was processed with these loaders:
* ./node_modules/cache-loader/dist/cjs.js
* ./node_modules/babel-loader/lib/index.js
* ./node_modules/eslint-loader/index.js
You may need an additional loader to handle the result of these loaders.
> #!/usr/bin/env node
| 'use strict';
|
[…etc…]

There's a JS file that has a shell-script shebang thing at the start of it, and the Babel transpiler doesn't like that. Fair enough, but I really don't understand why it's trying to transpile stuff in the node_modules directory, but at this point in time, I just thought "Hey-ho, it knows what it's doing so I'll take its word for it".

Googling about I found a bunch of other people having a similar issue with Webpack compilation, and the solution seemed to be to use a shebang-loader in the compilation process (see "How to keep my shebang in place using webpack?", "How to Configure Webpack with Shebang Loader to Ignore Hashbang…", "Webpack report an error about Unexpected character '#'"). All the guidance for this solution was oriented aroud sticking stuff in the webpack.config.js file, but of course Vue.js hides that away from you, and you need to do things in a special Vue.js way, but adding stuff with a special syntax to the vue.config.js file. The docs for this are at "Vue.js > Working with Webpack". The docs there showed how to do it using chainWebpack, but I never actually got this approach to actually solve the problem, so I mention this only because it's "something I tried".

From there I starting thinking, "no, seriously why is it trying to transpile stuff in the node_modules directory?" This does not seem right. I changed my googling tactic to try to find out what was going on there, and came across "Webpack not excluding node_modules", and that let me to update my vue.config.js file to actively exclude node_modules (copied from that answer):

var nodeExternals = require('webpack-node-externals');
...
module.exports = {
    ...
    target: 'node', // in order to ignore built-in modules like path, fs, etc. 
    externals: [nodeExternals()], // in order to ignore all modules in node_modules folder 
    ...
};

And that worked. Now when I ran the test I have made progress:

root@eba0490b453d:/usr/share/fullstackExercise# npm run test:unit

> full-stack-exercise@2.13.0 test:unit
> vue-cli-service test:unit test/unit/**/*.spec.js

 WEBPACK   Compiling...

  [=========================] 98% (after emitting)

 DONE   Compiled successfully in 1161ms

  [=========================] 100% (completed)

WEBPACK  Compiled successfully in 1161ms

MOCHA  Testing...



  Tests of GreetingMessage component
    ✓ should successfully load the component


  1 passing (17ms)

MOCHA  Tests completed successfully

root@eba0490b453d:/usr/share/fullstackExercise#

From there I rounded out the tests properly (frontend/test/unit/GreetingMessage.spec.js):

import GreetingMessage from "../../src/gdayWorld/components/GreetingMessage";

import { shallowMount } from "@vue/test-utils";

import {expect} from "chai";

describe("Tests of GreetingMessage component", () => {

    let greetingMessage;
    let expectedText = "TEST_MESSAGE";

    before("Load test GreetingMessage component", () => {
        greetingMessage = shallowMount(GreetingMessage, {propsData: {message: expectedText}});
    });

    it("should return the correct heading", () => {
        let heading = greetingMessage.find("h1");
        expect(heading.exists()).to.be.true;

        let headingText = heading.text();
        expect(headingText).to.equal(expectedText);
    });

    it("should return the correct content", () => {
        let contentParagraph = greetingMessage.find("h1+p");
        expect(contentParagraph.exists()).to.be.true;

        let contentParagraphText = contentParagraph.text();
        expect(contentParagraphText).to.equal(expectedText);
    });
});

Oh! Just a reminder of what the component is (frontend/src/gdayWorld/components/GreetingMessage.vue)! Very simple stuff, as the tests indicate:

<template>
    <h1>{{ message }}</h1>
    <p>{{ message }}</p>
</template>

<script>
export default {
  name: 'GreetingMessage',
  props : {
    message : {
      type: String,
      required: true
    }
  }
}
</script>

One thing I found was that every time I touched the test file, I was getting this compilation error:

> full-stack-exercise@2.13.0 test:unit
> vue-cli-service test:unit test/unit/**/*.spec.js

WEBPACK  Compiling...

  [=========================] 98% (after emitting)

ERROR  Failed to compile with 1 error

error  in ./test/unit/GreetingMessage.spec.js

Module Error (from ./node_modules/eslint-loader/index.js):

/usr/share/fullstackExercise/test/unit/GreetingMessage.spec.js
   7:1  error  'describe' is not defined  no-undef
  12:5  error  'before' is not defined    no-undef
  16:5  error  'it' is not defined        no-undef
  24:5  error  'it' is not defined        no-undef

error4 problems (4 errors, 0 warnings)

But if I ran it again, the problem went away. Somehow ESLint was getting confused by things; it only lints things when they've changed, and on the second - and subsequent - runs it doesn't run so the problem doesn't occur. More googling, and I found this: "JavaScript Standard Style does not recognize Mocha". The guidance here is to let the linter know I'm running Mocha, with the inference that there will be some global functions it can just assume are legit. This is just an entry in package.json:

  "eslintConfig": {
    "root": true,
    "env": {
      "node": true,
      "mocha": true
    },
    // etc

Having done that, everything works perfectly, and despite the fact that is a very very simple unit test… I'm quite pleased with myself that I got it running OK.

After sorting it all out, I reverted everything in source control back to how it was before I started the exercise, so as to replicate it and write it up here. This is when I was completely unable to reproduce that shebang issue at all. I cannot - for the life of me - work out why not. Weird. Anyway, I decided to not waste too much time trying to reproduce a bug I had solved, and just decided to mention it here as a possible "thing" that could happen, but otherwise move on with my life.

I have now got to where I wanted to get at the beginning of this series of articles, so I am gonna stop now. My next task with Vue.js testing is to work out how to mock things, because my next task will require me to make remote calls and/or hit a DB. And I don't want to be doing that when I'm running tests. But that was never an aim of this article series, so I'll revert to stand-alone articles now.

Righto.

--
Adam

Wednesday 20 January 2021

Polishing my Vue / Puppeteer / Mocha / Chai testing some more

G'day:
I should be writing the article to finish off that series about Docker / … / Vue.js etc, but whilst I was reading the docs and doing some online course (read: watching some videos) on Vue.js, I decided to TDD one of the exercises they were suggesting. The testing requirements were a bit tougher than I'd previously done with Mocha, and it took me longer to nail the testing of this than I would have liked. I figured it's worth working through it again, and jotting down some notes this time. Also I've read a bunch of blog articles instructing on Vue.js, and none of them say stuff like "implement your expectations as tests before you begin", so this is breaking that mould: if you have a dev task… always start by implementing tests for your expectations.

The exercise is part of the video Build a GitHub User Profile Component at vueschool.io. I've found their videos pretty handy, btw, and have been practising my testing whilst working through them in parallel to watching them.

The exercise is to take some flat example HTML and convert it into a Vue-implemented / data-driven solution. Here's the HTML:

<html>

<head>
  <link rel="stylesheet"
        href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css">
</head>

<body>

  <div id="app"
       class="ui container">
    <h1>GitHub Profiles</h1>
    <github-user-card username="hootlex"></github-user-card>

    <!-- Template for GitHub card -->
    <div class="ui card">
      <div class="image">
        <img src="https://semantic-ui.com/images/avatar2/large/kristy.png">
      </div>
      <div class="content">
        <a class="header">Kristy</a>
        <div class="meta">
          <span class="date">Joined in 2013</span>
        </div>
        <div class="description">
          Kristy is an art director living in New York.
        </div>
      </div>
      <div class="extra content">
        <a>
          <i class="user icon"></i>
          22 Friends
        </a>
      </div>
    </div>
  </div>

  <!-- Import Vue.js and axios -->
  <script src="https://unpkg.com/vue"></script>
  <script src="https://unpkg.com/axios/dist/axios.min.js"></script>

  <!-- Your JavaScript Code :) -->
  <script>
    new Vue({
      el: '#app'
    })
  </script>
</body>

</html>

And that displays this sort of thing:

IE: some details from a Github user profile. NB: no idea whether Kirsty is a real person - I hope not - but it's the exampe vueschool gave. The ultimate object of the exercise is to provide the Github user name to the code and it will display that user's deets.

One could see this as a refactoring exercise. Before we can refactor our code, we need to have some green tests to guard that we don't break anything while we refactor. In the real world we'd already have these, but I don't in this case so I'll need to knock some together now. The initial "green" state of the code is that it displays placeholder information at specific places in the DOM. This is all we will test for to start with.

Having reviewed the mark-up, there's seven things we need to test for, best described by the test definitions themselves:

root@ed4374d9ac6a:/usr/share/fullstackExercise# cat tests/functional/public/GithubProfilesTest.js
let chai = require("chai");
let fail = chai.assert.fail;

describe.only("For now, just identify the tests we need to make green", function () {
    it("should have the expected person's name", function () {
        fail("not implemented");
    });

    it("should have the expected person's github page URL", function () {
        fail("not implemented");
    });

    it("should have the expected person's avatar", function () {
        fail("not implemented");
    });

    it("should have the expected person's joining year", function () {
        fail("not implemented");
    });

    it("should have the expected person's description", function () {
        fail("not implemented");
    });

    it("should have the expected person's number of friends", function () {
        fail("not implemented");
    });

    it("should have the expected person's friends URL", function () {
        fail("not implemented");
    });
});
root@ed4374d9ac6a:/usr/share/fullstackExercise# npm test

> fullstackexercise@2.6.0 test
> mocha tests/**/*.js



  For now, just identify the tests we need to make green
    1) should have the expected person's name
    2) should have the expected person's github page URL
    3) should have the expected person's avatar
    4) should have the expected person's joining year
    5) should have the expected person's description
    6) should have the expected person's number of friends
    7) should have the expected person's friends URL


  0 passing (5ms)
  7 failing

  1) For now, just identify the tests we need to make green
       should have the expected person's name:
     AssertionError: not implemented
      at Context.<anonymous> (tests/functional/public/GithubProfilesTest.js:6:9)
      at processImmediate (node:internal/timers:463:21)

[etc. elided for brevity]


npm ERR! code 7
npm ERR! path /usr/share/fullstackExercise
npm ERR! command failed
npm ERR! command sh -c mocha tests/**/*.js

npm ERR! A complete log of this run can be found in:
npm ERR!     /root/.npm/_logs/2021-01-18T18_11_48_274Z-debug.log
root@ed4374d9ac6a:/usr/share/fullstackExercise#

The tests run, they fail, but I have a list of requirements to fulfil, even if I have not yet defined what it is to fulfil them. I'll start doing that now. For the next iteration I am going to do these things:

  • make the actual HTTP request with Puppeteer;
  • identify where in the DOM I need to check values;
  • for now, test against a bad value. The failure message will help is check if we're checking the DOM correctly;
let puppeteer = require("puppeteer");

let chai = require("chai");
let should = chai.should();

describe.only("Tests of githubProfiles page using placeholder data", function () {

    let browser;
    let page;

    before("Load the page", async function () {
        this.timeout(5000);

        browser = await puppeteer.launch( {args: ["--no-sandbox"]});
        page = await browser.newPage();

        await Promise.all([
            page.goto("http://webserver.backend/githubProfiles.html"),
            page.waitForNavigation()
        ]);
    });

    after("Close down the browser", async function () {
        await page.close();
        await browser.close();
    });

    it("should have the expected person's name", async function () {
        let name = await page.$eval("#app>.card>.content>.header", headerElement => headerElement.innerText)
        name.should.equal("INSERT EXPECTED NAME VALUE HERE");
    });

    it("should have the expected person's github page URL", async function () {
        let linkHref = await page.$eval("#app>.card>.content>a.header", headerElement => headerElement.href);
        linkHref.should.equal("INSERT EXPECTED LINKHREF VALUE HERE");
    });

    it("should have the expected person's avatar", async function () {
        let avatar = await page.$eval("#app>.card>.image>img", avatarElement => avatarElement.src);
        avatar.should.equal("INSERT EXPECTED AVATAR SRC VALUE HERE");
    });

    it("should have the expected person's joining year", async function () {
        let joiningMessage = await page.$eval("#app>.card>.content>.meta>.date", joiningElement => joiningElement.innerText);
        joiningMessage.should.equal("INSERT EXPECTED JOINING MESSAGE VALUE HERE");
    });

    it("should have the expected person's description", async function () {
        let description = await page.$eval("#app>.card>.content>.description", descriptionElement => descriptionElement.innerText);
        description.should.equal("INSERT EXPECTED DESCRIPTION VALUE HERE");
    });

    it("should have the expected person's number of friends", async function () {
        let friendsText = await page.$eval("#app>.card>.extra.content>a", extraContentAnchorElement => extraContentAnchorElement.innerText);
        friendsText.should.equal("INSERT EXPECTED FRIENDS TEXT VALUE HERE");
    });

    it("should have the expected person's friends URL", async function () {
        let linkHref = await page.$eval("#app>.card>.extra.content>a", extraContentAnchorElement => extraContentAnchorElement.href);
        linkHref.should.equal("INSERT EXPECTED FRIENDS LINK HREF VALUE HERE");
    });
});

And the run confirms (mostly) that we're inspecting the correct bit of the DOM. I'll just include the AssertionErrors from each test here, cos the rest of the output is largely the same:

AssertionError: expected 'Kristy' to equal 'INSERT EXPECTED NAME VALUE HERE'
AssertionError: expected '' to equal 'INSERT EXPECTED LINKHREF VALUE HERE'
AssertionError: expected 'https://semantic-ui.com/images/avatar2/large/kristy.png' to equal 'INSERT EXPECTED AVATAR SRC VALUE HERE'
AssertionError: expected 'Joined in 2013' to equal 'INSERT EXPECTED JOINING MESSAGE VALUE HERE'
AssertionError: expected 'Kristy is an art director living in New York.' to equal 'INSERT EXPECTED DESCRIPTION VALUE HERE'
AssertionError: expected ' 22 Friends' to equal 'INSERT EXPECTED FRIENDS TEXT VALUE HERE'
AssertionError: expected '' to equal 'INSERT EXPECTED FRIENDS LINK HREF VALUE HERE'

Cool it looks like I've mostly nailed the DOM selectors though: the correct values are being extracted by the tests. There's two that I'm not sure about though: the two link hrefs aren't actually in the mark-up, so their values are just blank. Well: all going well that's the reason, but I need to check that. A third thing to note is that there's a leading space in the friends text. Initially I thought this was a typo in the mark-up, but if we have a look, it's collapsed leading whitespace from the other content in the element I'm checking:

<div class="extra content">
    <a>
        <i class="user icon"></i>
        22 Friends
    </a>
</div>

That's legit, and we'll need our test to expect that. I've gone ahead and "fixed" the test mark-up to include the anchor tag hrefs (with dummy values):

<div class="content">
    <a class="header" href="GITHUB_PAGE_URL">Kristy</a>
    <div class="meta">
        <span class="date">Joined in 2013</span>
    </div>
    <div class="description">
        Kristy is an art director living in New York.
    </div>
</div>
<div class="extra content">
    <a href="GITHUB_FRIENDS_PAGE_URL">

Now to re-run the tests, and hopefully see those dummy values being compared to the test values. Again, I'll just include the AssertionExceptions:

AssertionError: expected 'Kristy' to equal 'INSERT EXPECTED NAME VALUE HERE'
AssertionError: expected 'http://webserver.backend/GITHUB_PAGE_URL' to equal 'INSERT EXPECTED LINKHREF VALUE HERE'
AssertionError: expected 'https://semantic-ui.com/images/avatar2/large/kristy.png' to equal 'INSERT EXPECTED AVATAR SRC VALUE HERE'
AssertionError: expected 'Joined in 2013' to equal 'INSERT EXPECTED JOINING MESSAGE VALUE HERE'
AssertionError: expected 'Kristy is an art director living in New York.' to equal 'INSERT EXPECTED DESCRIPTION VALUE HERE'
AssertionError: expected ' 22 Friends' to equal 'INSERT EXPECTED FRIENDS TEXT VALUE HERE'
AssertionError: expected 'http://webserver.backend/GITHUB_FRIENDS_PAGE_URL' to equal 'INSERT EXPECTED FRIENDS LINK HREF VALUE HERE'

OK, nice one. The tests are all good now: they are testing the correct elements in the DOM (except the whitespace one, I'll get to that), so I'm gonna update the tests to check for the actual placeholder values now. IE: make the tests pass for the boilerplate HTML.

let puppeteer = require("puppeteer");

let chai = require("chai");
chai.use(require("chai-string"));
let should = chai.should();

describe.only("Tests of githubProfiles page using placeholder data", function () {

    let browser;
    let page;

    let expectedUserData = {
        name : "Kristy",
        pageUrl : "http://webserver.backend/GITHUB_PAGE_URL",
        avatar : "https://semantic-ui.com/images/avatar2/large/kristy.png",
        joinedMessage : "Joined in 2013",
        description : "Kristy is an art director living in New York.",
        friends : "22 Friends",
        friendsPageUrl: "http://webserver.backend/GITHUB_FRIENDS_PAGE_URL"
    };

    // ...

    it("should have the expected person's name", async function () {
        let name = await page.$eval("#app>.card>.content>.header", headerElement => headerElement.innerText)
        name.should.equal(expectedUserData.name);
    });

    // ...
    
    it("should have the expected person's number of friends", async function () {
        let friendsText = await page.$eval("#app>.card>.extra.content>a", extraContentAnchorElement => extraContentAnchorElement.innerText);
        friendsText.should.containIgnoreSpaces(expectedUserData.friends);
    });

    // ...
});

I've elided a lot of code there due to it either being unchanged from before, or just the same as the examples I'm showing, and just focused on a coupla changes:

  • I've moved the expected values out into one object. This is partly to keep it all in one place, partly because I already know I'll be changing how I populate all that very soon.
  • I'm testing the actual values from the mark-up now. So the tests should be green.
  • I'm dealing with the extraneous whitespace.

When I run this:

> fullstackexercise@2.6.0 test
> mocha tests/**/*.js



  Tests of githubProfiles page using placeholder data
     should have the expected person's name
     should have the expected person's github page URL
     should have the expected person's avatar
     should have the expected person's joining year
     should have the expected person's description
     should have the expected person's number of friends
     should have the expected person's friends URL


  7 passing (3s)

root@ed4374d9ac6a:/usr/share/fullstackExercise#

Where are we now then? We have some tests that correctly check the correct part of the DOM of our test page. And all those tests pass. We can now start to do the actual work. The first thing we can do is a slight refactor: we can pull the inline mark-up out into a Vue template, and use the template's data object to furnish the template with the dynamic values. The desired result here is that no adjustments to tests should be necessary. First the mark-up file:

<html>
<head>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css">
</head>
<body>
<div id="app"
     class="ui container">
    <h1>GitHub Profiles</h1>
    <github-user-card username="hootlex"></github-user-card>
        
</div>

<script type="text/x-template" id="github-user-card-template">
    <div class="ui card">
        <div class="image">
            <img :src="avatar">
        </div>
        <div class="content">
            <a class="header" :href="pageUrl">{{name}}</a>
            <div class="meta">
                <span class="date">Joined in {{joinedYear}}</span>
            </div>
            <div class="description">
                {{description}}
            </div>
        </div>
        <div class="extra content">
            <a :href="friendsPageUrl">
                <i class="user icon"></i>
                {{friends}} Friends
            </a>
        </div>
    </div>
</script>

<script src="https://unpkg.com/vue"></script>
<script src="assets/scripts/githubProfiles.js"></script>
</body>
</html>

I've done a few things here:

  • Moved the mark-up for the "Github user card" into its own template.
  • And taken out the hard-coded values, which I will relocate into the JS application code (see below).
  • And I'm actually calling the JS application code.

That's actually the mark-up side of things complete now. Th JS code to support it is thus:

let githubUserCardComponent = {
    template : "#github-user-card-template",
    data : function () {
        return {
            name : "Kristy",
            pageUrl : "http://webserver.backend/GITHUB_PAGE_URL",
            avatar : "https://semantic-ui.com/images/avatar2/large/kristy.png",
            joinedYear : 2013,
            description : "Kristy is an art director living in New York.",
            friends : 22,
            friendsPageUrl: "http://webserver.backend/GITHUB_FRIENDS_PAGE_URL"
        };
    }
};

new Vue({
    el: '#app',
    components: {
        "github-user-card" : githubUserCardComponent
    }
});

Here we're just setting the data values expected by the template. It's still all hard-coded values for now. To check we've not messed anything up, we re-run the tests:

  Tests of githubProfiles page using placeholder data
     should have the expected person's name
     should have the expected person's github page URL
     should have the expected person's avatar
     should have the expected person's joining year
     should have the expected person's description
     should have the expected person's number of friends
     should have the expected person's friends URL

Cool. That all works.

Now we are going to need to do a code change: we need to source the actual data for the user from Github, via its API. But before we do that, we need to update our tests to expect that. We can't have our tests start to break because we've changed the code. We need to update the tests to expect the changes, and then… well… let them break that way instead ;-)

Now in the normal scheme of things, to keep this purely a functional test, I would mock the data provider for the data so when testing I'd not be getting live Github data, I'd be getting "known" values from a mock, and test that the known values are handled correctly. But I don't yet know how to test Vue components separately, so I don't know how to go about mocking the request to Github. What I'm gonna do is turn this into an integration test, and the test will get the correct data from Github, and then check if the app is getting the same (so accordingly correct) data too. Currently the test has this:

let expectedUserData = {
    name : "Kristy",
    pageUrl : "http://webserver.backend/GITHUB_PAGE_URL",
    avatar : "https://semantic-ui.com/images/avatar2/large/kristy.png",
    joinedMessage : "Joined in 2013",
    description : "Kristy is an art director living in New York.",
    friends : "22 Friends",
    friendsPageUrl: "http://webserver.backend/GITHUB_FRIENDS_PAGE_URL"
};

We're gonna get rid of those stubbed values with "live" values from Github. For reference, this is the JSON returned from the API call we're using:

{
  "login": "adamcameron",
  "id": 2041977,
  "node_id": "MDQ6VXNlcjIwNDE5Nzc=",
  "avatar_url": "https://avatars3.githubusercontent.com/u/2041977?v=4",
  "gravatar_id": "",
  "url": "https://api.github.com/users/adamcameron",
  "html_url": "https://github.com/adamcameron",
  "followers_url": "https://api.github.com/users/adamcameron/followers",
  "following_url": "https://api.github.com/users/adamcameron/following{/other_user}",
  "gists_url": "https://api.github.com/users/adamcameron/gists{/gist_id}",
  "starred_url": "https://api.github.com/users/adamcameron/starred{/owner}{/repo}",
  "subscriptions_url": "https://api.github.com/users/adamcameron/subscriptions",
  "organizations_url": "https://api.github.com/users/adamcameron/orgs",
  "repos_url": "https://api.github.com/users/adamcameron/repos",
  "events_url": "https://api.github.com/users/adamcameron/events{/privacy}",
  "received_events_url": "https://api.github.com/users/adamcameron/received_events",
  "type": "User",
  "site_admin": false,
  "name": "Adam Cameron",
  "company": null,
  "blog": "http://blog.adamcameron.me/",
  "location": "London",
  "email": null,
  "hireable": null,
  "bio": null,
  "twitter_username": "adam_cameron",
  "public_repos": 21,
  "public_gists": 211,
  "followers": 27,
  "following": 2,
  "created_at": "2012-07-25T18:02:54Z",
  "updated_at": "2021-01-16T16:57:34Z"
}

We won't need most of that.

Now I'll go ahead and re-jig my test code to make this call, and use it as the basis for the values to test against:

describe.only("Tests of githubProfiles page using github data", function () {
    let browser;
    let page;
    let expectedUserData;

    before("Load the page", async function () {
        this.timeout(5000);

        await loadTestPage();
        expectedUserData = await loadTestUserFromGithub();
    });

Here I have pushed the assignment of expectedUserData into the before handler, and also extracted the implementation into its own function, given there's a lot more code now:

let loadTestUserFromGithub = async function () {
    let githubUserData = await new Promise((resolve, reject) => {
        let request = https.get(
            "https://api.github.com/users/hootlex",
            {
                auth: `username: ${process.env.GITHUB_PERSONAL_ACCESS_TOKEN}`,
                headers: {'user-agent': 'node.js'}
            }, response => {
                let rawResponseData = "";

                response.on("data", data => {
                    rawResponseData += data;
                }).on("end", () => {
                    resolve(JSON.parse(rawResponseData));
                }).on("error", error => {
                    reject(error.message);
                });
            }
        );
        request.end();
    });
    return {
        name : githubUserData.name,
        pageUrl : githubUserData.html_url,
        avatar : githubUserData.avatar_url,
        joinedYear : new Date(githubUserData.created_at).getFullYear(),
        description : githubUserData.bio ?? "",
        friends : githubUserData.followers,
        friendsPageUrl: githubUserData.html_url + "?tab=followers"
    };
}

That's less complicated than it looks. Using the native Node.js https library requires one makes the request easily enough, but then one needs to receive the response data in chunks and assemble it yerself via the data event handler. And then once it's done one can use it in the end handler. To get it back to the calling code where it can be put to use, one needs to wrap all this in a promise, resolving with the data in the end handler. I really do wish there was a OhForGoodnessSakeStopMessingAroundAndJustGiveMeTheResponse handler, which I imagine is what people would use, 99% of the time. Ah well.

It's also worth noting those config params I'm setting in there:

{
    auth: `username: ${process.env.GITHUB_PERSONAL_ACCESS_TOKEN}`,
    headers: {'user-agent': 'node.js'}
}

The Github API has rate-limiting on it, and one is only allowed 60 requests per hour unless one uses some sort of authentication. One can just us a personal access token to work around this restriction. It's secret information so I don't want it anywhere in my code, so I'm just setting an environment variable on my host machine, and passing that through to the Node.js container. From docker-compose.yml:

  node:
    build:
      context: ./node
    environment:
      - GITHUB_PERSONAL_ACCESS_TOKEN=${GITHUB_PERSONAL_ACCESS_TOKEN}

And I'm passing a user agent there because the Github API requires one to.

That should all now "work", in that the tests will be broken, but it'll be testing the hard-coded Vue template data against the expected correct/live values from Github. AGain, I'll just show the AssertionExceptions here:

should have the expected person's name:
AssertionError: expected 'Kristy' to equal 'Alex Kyriakidis'

should have the expected person's github page URL:
AssertionError: expected 'http://webserver.backend/GITHUB_PAGE_URL' to equal 'https://github.com/hootlex'

should have the expected person's avatar:
AssertionError: expected 'https://semantic-ui.com/images/avatar2/large/kristy.png' to equal 'https://avatars0.githubusercontent.com/u/6147968?v=4'

should have the expected person's joining year:
AssertionError: expected 'Joined in 2013' to equal undefined

should have the expected person's description:
AssertionError: expected 'Kristy is an art director living in New York.' to equal 'Developer - Teacher - Author- Consultant'

should have the expected person's number of friends:
AssertionError: expected  22 Friends to contain 850 ignoring spaces

should have the expected person's friends URL:
AssertionError: expected 'http://webserver.backend/GITHUB_FRIENDS_PAGE_URL' to equal 'https://github.com/hootlex?tab=followers'

Argh. OK so the good news is that most of those are failing in the right way: they are testing the correct placeholder vs live values. But the one testing the joining year and the one testing the number of friends are failing for the wrong reasons:

Firstly the "should have the expected person's joining year" test has two problems:

    // test code
    it("should have the expected person's joining year", async function () {
        let joiningMessage = await page.$eval("#app>.card>.content>.meta>.date", joiningElement => joiningElement.innerText);
        joiningMessage.should.equal(expectedUserData.joinedMessage);
    });

    // test data
        return {
            name : githubUserData.name,
            pageUrl : githubUserData.html_url,
            avatar : githubUserData.avatar_url,
            joinedYear : new Date(githubUserData.created_at).getFullYear(),
            description : githubUserData.bio ?? "",
            friends : githubUserData.followers,
            friendsPageUrl: githubUserData.html_url + "?tab=followers"
        };

The test data is joinedYear (just the year), and the test is still comparing to the entire message: joinedMessage. So I'll update the test, thus:

it("should have the expected person's joining year", async function () {
    const expectedJoiningMessage = `Joined in ${expectedUserData.joinedYear}`;

    let joiningMessage = await page.$eval("#app>.card>.content>.meta>.date", joiningElement => joiningElement.innerText);
    joiningMessage.should.equal(expectedJoiningMessage);
});

And now this test actually passes because - by coincidence - the hard-coded year in the template matches the live one coming from Github. We can't have that so for now I'm changing the value in the template to be a different year. We can't be changing code if its test doesn't fail until before we make the changes to make the test pass.

The "should have the expected person's number of friends" is failing incorrectly for much the same reason as the previous one. Here's the relevant code:

    //test code
    it("should have the expected person's number of friends", async function () {
        let friendsText = await page.$eval("#app>.card>.extra.content>a", extraContentAnchorElement => extraContentAnchorElement.innerText);
        friendsText.should.containIgnoreSpaces(expectedUserData.friends);
    });

        // test data
        return {
            name : githubUserData.name,
            pageUrl : githubUserData.html_url,
            avatar : githubUserData.avatar_url,
            joinedYear : new Date(githubUserData.created_at).getFullYear(),
            description : githubUserData.bio ?? "",
            friends : githubUserData.followers,
            friendsPageUrl: githubUserData.html_url + "?tab=followers"
        };

Again in the test we're expecting the entire message - eg "" - to be in the value we're comparing, but obviously we're only getting the count back from Github. I'll change that in the same way as the previous one (I'll spare you the code, you get the idea). now those tests are failing in the correct way:

AssertionError: expected 'Joined in 2013_BREAK_ME' to equal 'Joined in 2013'
AssertionError: expected 22 Friends to contain 850 Friends ignoring spaces

Now we can update the app code to get the correct data.

Firstly we're gonna need to pay attn to the username value passed to the template in the parent mark-up:

<div id="app"
     class="ui container">
    <h1>GitHub Profiles</h1>
    <github-user-card username="hootlex"></github-user-card>
</div>

This is just a matter of adding a property to the template definition:

let githubUserCardComponent = {
    template : "#github-user-card-template",
    props : {
        username: {
            type: String,
            required: true
        }
    },

Now we can use this.username in the rest of the template code.

Next: a the moment all our data values are hard-coded sample data:

let githubUserCardComponent = {
    template : "#github-user-card-template",
    data : function () {
        return {
            name : "Kristy",
            pageUrl : "http://webserver.backend/GITHUB_PAGE_URL",
            avatar : "https://semantic-ui.com/images/avatar2/large/kristy.png",
            joinedYear : 2013,
            description : "Kristy is an art director living in New York.",
            friends : 22,
            friendsPageUrl: "http://webserver.backend/GITHUB_FRIENDS_PAGE_URL"
        };
    }
};

We're gonna null-out all those as we don't know what they will be when the template is first loaded. However, when the instance of the component is created, we can dart off to Github and get the relevant values.

let githubUserCardComponent = {
    // ...
    data : function () {
        return {
            name : null,
            pageUrl : null,
            avatar : null,
            joinedYear : null,
            description : null,
            friends : null,
            friendsPageUrl: null
        };
    },
    created () {
        axios.get(
            `https://api.github.com/users/${this.username}`,
            {
                auth: {
                    username: this.$route.query.GITHUB_PERSONAL_ACCESS_TOKEN
                }
            }
        )
        .then(response => {
            this.name = response.data.name;
            this.pageUrl = response.data.html_url;
            this.avatar = response.data.avatar_url;
            this.joinedYear = new Date(response.data.created_at).getFullYear();
            this.description = response.data.bio;
            this.friends = response.data.followers;
            this.friendsPageUrl = response.data.html_url + "?tab=followers";
        });
    }
};

There's a coupla things to note here. Firstly, just like in the test, I am passing a personal access token to the Github API so I don't get throttled by them. And, again, I don't want the actual token value to be in the code, so I am passing it on the URL. To get the value from the URL, I need to use the Vue Router (which I know nothing about other than what I found on Stack Overflow when I needed to get something from the query string). To use this I need to initialise the Vue app with a router:

let router = new VueRouter({
    mode: 'history',
    routes: []
});

new Vue({
    router,
    el: '#app',
    components: {
        "github-user-card" : githubUserCardComponent
    }
});

Then I can access my query string param as per the code above.

The other bit to look at in the created handler above is this Axios thing. It's just an HTTP lib that the tutorial suggested. It seems pretty slick and easier to use than the HTTPS library I was messing about with in the test code. It's relevant to note that Axios has an implementation for Node.js too, but I didn't know that when I wrote the tests. The code there works, I'll leave it.

Both the Vue Router and Axios libs need to be loaded in the main mark-up file too, obvs:

<script src="https://unpkg.com/vue"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<script src="https://unpkg.com/vue-router"></script>
<script src="assets/scripts/githubProfiles.js"></script>
</body>
</html>

OK so in theory now on the client-side we are loading-in the data from Github, and this should match the same data the tests grabbed, and our tests should be green…

AssertionError: expected '' to equal 'Alex Kyriakidis'
AssertionError: expected '' to equal 'https://github.com/hootlex'
AssertionError: expected '' to equal 'https://avatars0.githubusercontent.com/u/6147968?v=4'
AssertionError: expected 'Joined in' to equal 'Joined in 2013'
AssertionError: expected '' to equal 'Developer - Teacher - Author- Consultant'
AssertionError: expected Friends to contain 850 Friends ignoring spaces
AssertionError: expected '' to equal 'https://github.com/hootlex?tab=followers'

Um… where are the client-side values? I checked in the browser, and it all seemed legit:

What's worse is that about 25% of the time, the tests were actually passing! It took me quite a while to work out what was going on, but finally it turned out that I needed a lesson in how to think asynchronously, and what the various processing stages are of a web page. I'll detail the troubleshooting in another article (maybe), but it boiled down to this line of code:

let loadTestPage = async function () {
    browser = await puppeteer.launch( {args: ["--no-sandbox"]});
    page = await browser.newPage();

    await Promise.all([
        page.goto("http://webserver.backend/githubProfiles.html"),
        page.waitForNavigation()
    ]);
}

When a web page loads, it's navigable as soon as the asset files (mark-up, CSS, JS etc) are loaded. But it does not wait for asynchronous data requests to complete before the page is considered ready for navigation. So that promise is fulfilled whilst the call to Github is probably still under way. Sometimes it was completing in time for the test code to check the values; most of the time it had not, so the values weren't there. I did not notice this when I first wrote this code because the call to Github wasn't in there then, so there was no problem. Anyhow, this was easily solved:

page.waitForNavigation({waitUntil: "networkidle0"})

After that the tests still failed sometimes, but this time it was because loading the test page and then the test code hitting Github itself was occasionally taking longer than the 5000ms timeout I currently had, so I upped that to 10000ms:

before("Load the page", async function () {
    this.timeout(10000);

And after those two tweaks:

Tests of githubProfiles page using github data
     should have the expected person's name
     should have the expected person's github page URL
     should have the expected person's avatar
     should have the expected person's joining year
     should have the expected person's description
     should have the expected person's number of friends
     should have the expected person's friends URL


  7 passing (5s)

And I repeated the tests dozens of times, and they always passed now, so I'm happy I've nailed the code there.

I actually continued on from here and updated the code to allow passing an override username value in the URL, but this article is already massive and this is the third day working on it, so I'm gonna end here. This was probably a tedious read, but the exercise for me in TDDing some Vue stuff was absolute gold. I still need to find out how to separate Vue components out into separate files so I can test them individually, and outside the context of a web page that is using them, but that's for another day.

Righto.

--
Adam