Showing posts with label Vue.js. Show all posts
Showing posts with label Vue.js. Show all posts

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

Monday 15 February 2021

Part 11: setting up a Vue.js project and integrating some existing code into it

G'day:

OK so maybe this will be the last article in this series. But given what a monster it's become: who knows. As a recap, here are links to the earlier articles:

  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 (this article)
  12. Unit testing Vue.js components

It's up to you whether you read the rest of that lot, or just skim it, or whatever. Looking at the source code might help. This is where it's at as I am writing this sentence: Fullstack Exercise v 2.10. That has already got some of the code in it that is "new" to this article. Any other code links in here I'll link to the final version of the work. I think so far I've done the Vue.js project install, and reconfigured it and Nginx so that I'm using Nginx as the front-end web server, not the one that ships with the Vue.js project. Other than that the Vue code is just the "hello world" stuff the project starts with.

OK so the object of this exercise is to take my gdayWorldViaVue.html page which is is built with its Vue template embedded in just a JS file. I discuss the creation of this work in the article "Part 8: Testing a simple web page built with Vue.js using Mocha, Chai and Puppeteer". Here's the code:

frontend/public/gdayWorldViaVue.html:

<!doctype html>

<html lang="en">
<head>
    <meta charset="utf-8">

    <title id="title">{{ message }}</title>
</head>

<body>
<div id="app">
    <greeting :message="message"></greeting>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script src="assets/scripts/gdayWorldViaVue.js"></script>
</body>
</html>

And frontend/public/assets/scripts/gdayWorldViaVue.js:

Vue.component('greeting', {
    props : {
        message : {
            type: String,
            required: true
        }
    },
    template : `
    <div>
        <h1>{{ message }}</h1>
        <p>{{ message }}</p>
    </div>
    `
});


let appData = {message: "G'day world via Vue"};
new Vue({el: '#title', data: appData});
new Vue({el: '#app', data: appData});

Oh, an for the sake of completeness, the output:



Ah, and of course there's a test (frontend/test/functional/GdayWorldViaVueTest.js):

let puppeteer = require('puppeteer');

let chai = require("chai");
let chaiAsPromised = require("chai-as-promised");
chai.use(chaiAsPromised);
let should = chai.should();

describe("Baseline test of vue.js working", function () {
    let browser;
    let page;

    const expectedText = "G'day world via Vue";

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

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

        await page.goto("http://fullstackexercise.frontend/gdayWorldViaVue.html");
    });

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

    it("should return the correct page title", async function () {
        await page.title().should.eventually.equal(expectedText);
    });

    it("should return the correct page heading", async function () {
        let headingText = await page.$eval("h1", headingElement => headingElement.innerText);

        headingText.should.equal(expectedText);
    });

    it("should return the correct page content", async function () {
        let paragraphContent = await page.$eval("p", paragraphElement => paragraphElement.innerText);

        paragraphContent.should.equal(expectedText);
    });
});

Output:

root@18a88721ed2a:/usr/share/fullstackExercise# npm test

> full-stack-exercise@2.6.0 test
> mocha test/**/*.js



  Baseline test of vue.js working
     should return the correct page title
     should return the correct page heading
     should return the correct page content


  3 passing (535ms)

root@18a88721ed2a:/usr/share/fullstackExercise#

Having those test cases there are gold, because it means all this work is basically a refactoring exercise (we've done the red / green bit in the earlier article), so the object of this exercise is to separate-out the template from the JS file and into a .vue file, and know the work is done because those test cases still pass.

Right, so I have hit the Vue.JS website, and done some reading, and to use .vue files, I need to create a Vue project. First I just want to see what the project-creation does, to check if it'll be easier to integrate my work into the project, or the project into the work: you might remember a similar drama when I was installing Symphony ("Part 6: Installing Symfony"). Once-bitten, twice-shy, I'm just gonna see what Vue thinks it's doing first. I'm gonna create this project in /tmp.

Oh Adam… back-up! First of all I need to install Vue CLI. This is the app that deals with project creation and stuff like that. This is easy, I just integrate it into the node/Dockerfile:

FROM node
RUN apt-get update \
    && apt-get install -y wget gnupg \
    && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
    && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
    && apt-get update \
    && apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 \
      --no-install-recommends \
    && rm -rf /var/lib/apt/lists/*
WORKDIR  /usr/share/fullstackExercise/
COPY config/* ./
RUN npm install -g @vue/cli
RUN npm install
EXPOSE 8080

Just to be clear, the first npm install there is installing Vue CLI globally, the second one is installing the modules necessary for my app. Incidentally, it was getting this stuff working that was the inspiration for two of my recent articles: "Part 9: I mess up how I configure my Docker containers" and "Part 10: An article about moving files and changing configuration". But it's all working now.

Now I've got Vue CLI installed, I'll install a project:

adam@DESKTOP-QV1A45U:/mnt/c/src/fullstackExercise/docker$ docker exec --interactive --tty fullstackexercise_node_1 /bin/bash
root@18a88721ed2a:/usr/share/fullstackExercise# cd /tmp
root@18a88721ed2a:/tmp# ll
total 20
drwxrwxrwt 1 root root  4096 Feb 12 12:18 ./
drwxr-xr-x 1 root root  4096 Feb 10 16:43 ../
drwx------ 2 root root  4096 Feb  6 19:32 apt-key-gpghome.b8FchIBzPr/
-rw-r--r-- 1 root staff  543 Feb  9 12:36 core-js-banners
drwxr-xr-x 3 root root  4096 Feb  6 03:03 v8-compile-cache-0/
root@18a88721ed2a:/tmp# vue create hello-world


Vue CLI v4.5.11
? Please pick a preset:
  Default ([Vue 2] babel, eslint)
> Default (Vue 3 Preview) ([Vue 3] babel, eslint)
  Manually select features


? Please pick a preset: Default ([Vue 2] babel, eslint)
? Pick the package manager to use when installing dependencies:
  Use Yarn
> Use NPM


✨  Creating project in /tmp/hello-world.
🗃  Initializing git repository...
⚙️  Installing CLI plugins. This might take a while...


added 1269 packages, and audited 1270 packages in 57s

62 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
🚀  Invoking generators...
📦  Installing additional dependencies...


added 71 packages, and audited 1341 packages in 6s

68 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
⚓  Running completion hooks...

📄  Generating README.md...

🎉  Successfully created project hello-world.
👉  Get started with the following commands:

$ cd hello-world
$ npm run serve

 WARN  Skipped git commit due to missing username and email in git config, or failed to sign commit.
You will need to perform the initial commit yourself.


root@18a88721ed2a:/tmp#

Lovely. Let's do this run-serve thing:

root@18a88721ed2a:/tmp# cd hello-world/
root@18a88721ed2a:/tmp/hello-world# npm run serve

> hello-world@0.1.0 serve
> vue-cli-service serve

 INFO  Starting development server...
98% after emitting CopyPlugin

 DONE  Compiled successfully in 2221ms                                                                        1:27:33 PM

  App running at:
  - Local:   http://localhost:8081/

  It seems you are running Vue CLI inside a container.
  Access the dev server via http://localhost:<your container's external mapped port>/

  Note that the development build is not optimized.
  To create a production build, run npm run build.

This won't work because I need to poke a hole through from the container to the host machine on 8081, but I'll quickly do this (I'll spare you the detail). Gimme a minute.


Cool. My first challenge is that I've already got a web server, and I want to use it. I don't want to be using Vue's stub web server. Now for dev there's no real need for this: Vue's web server would be fine, but I've got Nginx there, so I'm going to use it. Also I don't want to use localhost to access it. I figured I needed to configure a proxy_pass "thing" (sorry for technical buzzword there), but I had no idea how to do it. I'm a noob with Nginx. Anyhow, I googled about and found this article "VueJS dev serve with reverse proxy" by someone called Marko Mitranić . I didn't understand all the settings it was suggesting, but I grabbed them and it all works (docker/nginx/sites/frontend.conf):

server {
    listen 80;
    listen [::]:80;

    server_name fullstackexercise.frontend;
    root /usr/share/nginx/html/frontend;
    index index.html;

    location / {
        #autoindex on;
        #try_files $uri $uri/ =404;

        # from https://medium.com/homullus/vuejs-dev-serve-with-reverse-proxy-cdc3c9756aeb
        proxy_pass  http://vuejs.backend:8080/;
        proxy_set_header Host vuejs.backend;
        proxy_set_header Origin vuejs.backend;
        proxy_hide_header Access-Control-Allow-Origin;
        add_header Access-Control-Allow-Origin "http://fullstackexercise.frontend";
    }

    # from https://medium.com/homullus/vuejs-dev-serve-with-reverse-proxy-cdc3c9756aeb
    location /sockjs-node/ {
        proxy_pass http://vuejs.backend:8080;
        proxy_redirect off;
        proxy_set_header Host vuejs.backend;
        proxy_set_header Origin vuejs.backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_hide_header Access-Control-Allow-Origin;
        add_header Access-Control-Allow-Origin "http://fullstackexercise.frontend";
    }

    location ~ /\.ht {
        deny all;
    }
}

The first block is for the website (on http://fullstackexercise.frontend); the second block is for the listener for changes in the source code. Vue does some magic with web sockets and ping up to the browser to reload any code changes that are back on the server. This is quite cool. Oh I also needed to put some settings in docker/node/config/vue.config.js:

module.exports = {
    devServer: {
        host: "vuejs.backend",
        disableHostCheck: false,
        port: 8080,
        watchOptions : {
            ignored: /node_modules/,
            poll: 1000
        }
    }
}

But I'm getting ahead of myself here. The Nginx stuff is for the fullstackExercise websites. I'm still with this one in /tmp/ directory. All I want to check is what it installs, which is this lot:

root@61c6341dc75c:/tmp/hello-world# tree -aF --dirsfirst -L 3 .
.
|-- node_modules/
|   `-- [… seemingly millions of files elided…]
|-- public/
|   |-- favicon.ico
|   `-- index.html
|-- src/
|   |-- assets/
|   |   `-- logo.png
|   |-- components/
|   |   `-- HelloWorld.vue
|   |-- App.vue
|   `-- main.js
|-- .gitignore
|-- README.md
|-- babel.config.js
|-- package-lock.json
`-- package.json

1760 directories, 5161 files

root@61c6341dc75c:/tmp/hello-world#

OK so it seems to be just a bunch of NPM libs and stuff in package.json:

root@61c6341dc75c:/tmp/hello-world# cat package.json
{
  "name": "hello-world",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "serve": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "core-js": "^3.6.5",
    "vue": "^3.0.0"
  },
  "devDependencies": {
    "@vue/cli-plugin-babel": "~4.5.0",
    "@vue/cli-plugin-eslint": "~4.5.0",
    "@vue/cli-service": "~4.5.0",
    "@vue/compiler-sfc": "^3.0.0",
    "babel-eslint": "^10.1.0",
    "eslint": "^6.7.2",
    "eslint-plugin-vue": "^7.0.0-0"
  },
  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/vue3-essential",
      "eslint:recommended"
    ],
    "parserOptions": {
      "parser": "babel-eslint"
    },
    "rules": {}
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not dead"
  ]
}
root@61c6341dc75c:/tmp/hello-world#

All of which will merge into my existing package.json just fine. And some source code files and assets. I took a punt and just integrated all of that into my app, stuck CMD ["npm", "run", "serve"] in my docker/node/Dockerfile, and rebuilt my containers…

Cool. Plus I re-ran all my own tests on the other code in the app, and everything is still currently green. Also I can change my source code on the server, and almost instantly it's displayed in the browser without a reload:

Right. So now I have to try to integrate this gdayWorldViaVue.html page into my Vue app. It took a while to work out how to do this: I'm sure it's in the Vue.js docs somewhere, but I couldn't find it. Ultimately I found a Q&A on Stack Overflow that explained it: "multiple pages in Vue.js CLI" (and a handy Github repo with example code in it). Basically Vue CLI assumes everyone wants a single-page app, and doesn't really expose how to add additional pages into this SPA. But it's just a matter of defining the pages in vue.config.js (and I have the doc reference now: Configuration Reference > Pages. First I just shifted the project's own index page into a page definition:

module.exports = {
    pages : {
        index: {
            entry: "src/index/main.js",
            template: "public/index.html",
            filename: "index.html"
        }
    },
    devServer: {
        host: "vuejs.backend",
        disableHostCheck: false,
        port: 8080,
        watchOptions : {
            ignored: /node_modules/,
            poll: 1000
        }
    }
}

This also necessitated moving main.js, components/HelloWorld.vue and App.vue from the base of the src/ directory structure, into the src/index/ subdirectory, as well as changing a relative reference to ./assets/logo.png to ../assets/logo.png, given it's being referenced from a file in that index/ subdirectory now. I rebuilt the container, and restarted it, and the Hello World page still worked. Now to add the gdayWorldViaVue.html page into it.

This was pretty easy, and was just a matter of moving some stuff around in the file system and within files. Previously we had a single HTML file, gdayWorldViaVue.html, and a single JS file, gdayWorldViaVue.js, as per above in this article. First I implemented the page mapping for this page in vue.config.js, as per the index example above:

        gdayWorld: {
            entry: "src/gdayWorld/main.js",
            template: "public/gdayWorldViaVue.html",
            filename: "gdayWorldViaVue.html"
        }

And then I need to distribute the contents of the existing .html and .js file to their more Vue / page / component-centric equivalents. src/gdayWorld/main.js is identical to the one for the index page:

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

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

frontend/public/gdayWorldViaVue.html has had the reference to the JS files in the foot of the <body> removed as the application now handles those, it also has the body of the #app <div/> removed as this is handled by the App.vue file now, and also has placeholder text put in for the page title:

<!doctype html>

<html lang="en">
<head>
    <meta charset="utf-8">

    <title id="title">PAGE-TITLE</title>
</head>

<body>
<div id="app"></div>
</body>
</html>

I was handling the page title in quite an unorthodox fashion before, and I'm going to fix this now.

frontend/src/gdayWorld/App.vue defines what goes in the #app <div/>, and loads in the GreetingMessage component:

<template>
    <greeting-message :message="message"></greeting-message>
</template>

<script>
import GreetingMessage from './components/GreetingMessage.vue'
export default {
    name: 'App',
    components: {
        GreetingMessage
    },
    data () {
        return {
            message: "G'day world via Vue"
        };
    },
    created () {
        // document.title = this.message;
    }
}
</script>

Note it will also sets the document.title value now, from the app data. It's commented-out for now because I want to see the test for this fail initially, so I am sure what we're seeing is the refactored implementation.

And frontend/src/gdayWorld/components/GreetingMessage.vue defines the behaviour of itself. So this file is a stand-alone component now, which is what we were aiming to do all along.

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

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

I also deleted frontend/public/assets/scripts/gdayWorldViaVue.js as it's now surplus to requirement. Because we've changed the Vue app's config, and that is copied over in Dockerfile, I need to rebuild the containers to see this "change". Once that's done I re-run my tests:

root@ebd7dc7b4a3d:/usr/share/fullstackExercise# npm test

> full-stack-exercise@2.6.0 test
> mocha test/**/*.js



  Baseline test of vue.js working
    1) should return the correct page title
     should return the correct page heading
     should return the correct page content


  2 passing (319ms)
  1 failing

  1) Baseline test of vue.js working
       should return the correct page title:

      AssertionError: expected 'PAGE-TITLE' to equal 'G\'day world via Vue'
      + expected - actual

      -PAGE-TITLE
      +G'day world via Vue

      at /usr/share/fullstackExercise/node_modules/chai-as-promised/lib/chai-as-promised.js:302:22
      at processTicksAndRejections (node:internal/process/task_queues:94:5)
      at async Context.<anonymous> (test/functional/GdayWorldViaVueTest.js:29:9)



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

npm ERR! A complete log of this run can be found in:
npm ERR!     /root/.npm/_logs/2021-02-12T17_13_50_305Z-debug.log
root@ebd7dc7b4a3d:/usr/share/fullstackExercise#

Woohoo! Two test are passing, and the expected one is failing. If I now uncomment that line above (and as soon as I do that my page in the browser now gets its title too btw), the test is now passing:

  Baseline test of vue.js working
     should return the correct page title
     should return the correct page heading
     should return the correct page content

Similarly if I go and change the message in frontend/src/gdayWorld/App.vue the tests fail appropriately, so I'm happy the tests are testing the new implementation.


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. This is fine for an end-to-end test, but not so good for a unit test. TBH in this simple case the current approach is actually fine, but I want to know how to test components.

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. So. I'm gonna leave this article here at the "and lo, I have .vue-file-based Vue components working now, and kinda understand how that side of things comes together, but - nor now at least - I'm gonna do my front-end testing via HTTP requests of the whole document, not via each component. For what I need to do right now this is fine anyhow, I think. Sigh.

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