loading...
Cover image for Not Another To-Do App: Part 3

Not Another To-Do App: Part 3

westbrook profile image Westbrook Johnson Updated on ・9 min read

Getting your hands dirty and feet wet with Open Web Component Recommendations...sort of.

This a cross-post of a Feb 26, 2019 article from Medium that takes advantage of my recent decision to use Grammarly in my writing (so, small edits have been made here and there), thanks for looking again if you saw it there 🙇🏽‍♂️ and if this is your first time reading, welcome!

Welcome to “Not Another To-Do App”, an overly lengthy review of making one of the smallest applications every developer ends up writing at some point or another. If you’re here to read up on a specific technique to writing apps or have made your way from a previous installation, then likely you are in the right place and should read on! If not, it’s possible you want to start from the beginning so you too can know all of our characters’ backstories...

If you’ve made it this far, why quit now?


Test Early, Test Often

Testing...

Photo by ShareGrid on Unsplash

For me, the best part of having a tool like Karma available in my application from the starting gun is that I can pretend that I know how to do TDD (test-driven development). Don’t get me wrong, it’s not that I don’t know what it means, or how to do it, my issue is more one of conviction. I love a round of ping pong pairing when getting into a session of pair coding. It’s a great way to keep the time structured, and it can quickly bring to light any number of important conversations about the project/feature the two of you are entering on. It’s after I get back to my own desk where things start to slip. Write one test, add one feature, write a second test, write a second feature... and, right around there it all falls apart. It’s great when it works though, and in that ethos, I started my To-Do app as follows:

it('has a title', async () => {
    const el = await fixture(html`<open-wc-app></open-wc-app>`);
    const title = el.shadowRoot.querySelector('h1').textContent;
    expect(title).to.equal('open-wc');
});

Ok, ok, you got me, I didn’t start it that way. It came for free from our friends at open-wc. Test coverage before I type a key, yay! But, I did still start with a test and it looked something like:

const el = await fixture(html`<open-wc-app></open-wc-app>`);
expect(el.todos.length).to.equal(0);

Which of course fails (TDD, what?!). In true LitElement form the following gets the test back to green:

class OpenWcApp extends LitElement {
    static get properties() {
        return {
            todos: { type: Array }
        };
    }
    constructor() {
        super();
        this.todos = [];
    }
}

Realize that this is only the additions (with some of the preceding code to support them), not replacement code for the provided src/open-wc-app.js.

Now our initial test will pass, so it’s time to add another:

const el = await fixture(html`<open-wc-app></open-wc-app>`);
expect(el.todos.length).to.equal(0);
el.dispatchEvent(new CustomEvent('todo-new', {
    detail: 'New To Do',
    bubbles: true,
    composed: true
}));
expect(el.todos.length).to.equal(1);

This extension of our previous test will take us beyond the initialization processes and directly into event-based state management. This means that my app will be passing data and actions via new CustomEvent()s as triggered by dispatchEvent(). With a lot of work as of late in front end engineering being based around virtual DOM this can often be a surprising “new” feature of the browser, so if you’ve not gotten a chance to work with this API before I’d highly suggest you check it out more deeply. Knowing that this is what’s going on, we can now add code, again to src/open-wc-app.js, in order to get our tests passing again.

constructor() {
    super();
    this.addEventListener('todo-new', (e) => {
        let todo = e.detail;
        if (todo === null) return;
        this.todos = [
            ...this.todos,
            todo,
        ];
    });
}

My goal when doing TDD is to write code that passes, not explicitly the most beautiful code, or the most performant code, so I don’t worry too much if things aren’t “as they should be”™️. Let’s agree to make room for that later, and in the interim take a look at what’s going on here. We’re registering an event listener against our element for the todo-new event that our test is dispatching. When one such event is heard, we’ll take the to do that we’ve confirmed to be packed into the event’s detail (e.detail) and append it to the existing list of to-do items. Further, you’ll see we’re using the spread operator to maintain the identity of our individual to-dos while renewing the identity of our todos property which will notify LitElement to kick off the render pipeline while still being able to compare our individual to-dos.

With our test passing again, it’s time to get back in there and complete the round trip of creating a new to do. Notice how we complete the data processing test by confirming the content of the new to-do as well as the previous test of changes to the entire todos array. There is also a test for whether these changes are reflected in the actual rendered results.

it('adds a to do in response to a `todo-new` event', async () => {
    const newTodo = 'New To Do';
    const el = await fixture(html`<open-wc-app></open-wc-app>`);
    expect(el.shadowRoot.querySelectorAll('to-do').length)
        .to.equal(0);
    el.dispatchEvent(new CustomEvent('todo-new', {
        detail: newTodo,
        bubbles: true,
        composed: true
    }));
    await nextFrame();
    expect(el.todos.length).to.equal(1);
    expect(el.todos[0]).to.equal(newTodo);
    expect(el.shadowRoot.querySelectorAll('to-do').length)
        .to.equal(1);    
    expect(el.shadowRoot.querySelectorAll('to-do')[0].textContent)
        .to.equal(newTodo);
});

You’ll see that with the addition of tests against the rendered output we put to use the first of many open-wc tools that will be of benefit to building your app. nextFrame as acquired by import { nextFrame } from ‘@open-wc/testing'; is a helper method that supports working with LitElement's asynchronous rendering process. Because rendering with LitElement happens with micro-task timing you won’t be able to test changes to the DOM synchronously, the nextFrame helper delays your test until the next frame so that tests against the DOM will have the newly rendered results to test against. To achieve the desired changes, the following update to the render() method is required:

render() {
    return html`
        ${this.todos.map(todo => html`
            <to-do>${todo}</to-do>
        `)}
    `;
}

Et voilà, you have fully tested Custom Event-based management of the to-do adding process.

No, we haven’t created UI or testing of the element that might dispatch such an event. However, to see our code so far working in an actual browser, visit the console and run code similar to what you see in the test to publish a to do to the page:

$0.dispatchEvent(new CustomEvent('todo-new', {
    detail: 'Fancy Console Created To Do',
    bubbles: true,
    composed: true
}));

$0 is the currently selected node in the Elements panel.

Yes, there is a lot more to test and build, but as I mentioned before this isn’t a “How to Make a To-Do App” article. I’m merely introducing the benefits of having testing built into your project from day one. Whether you leverage that by applying the tenants of test-driven development, or some other philosophy, I’d love to hear more...drop me a note in the comments. With this capability in your project, I’ll be sending good vibes into the world that you make it further with whatever approach you choose before the excitement of coding overcomes your conviction to the process.

Note: I made it through three full tests, the above being one, before becoming unable to maintain the rigor of TDD. I was pretty proud, particularly in that those three tests covered a good amount of the application's main features. In the end, it took me 20+ tests, which is probably more than absolutely necessary, to support 100% code coverage of the final To Do app.

Pro Tip

When I do the work of testing my application (I promise I do it as much a possible) the end goal is always something like this:

All the lines are covered!

All the lines are covered!

However, the open-wc Starter App provides the following style of reporting by default:

I wanna be covered, but I don’t know what’s showing...

I wanna be covered, but I don’t know what’s showing...

I got into my test results and I was immediately struck by the questions of “What haven’t I tested yet?” and “How can I know where it is?”. I just wanted to see my uncovered lines. I just wanted this:

Yay! I know what I’ve got on my to-do list still. (See what I did there?)

Yay! I know what I’ve got on my to-do list still. (See what I did there?)

I wanted it so much, I went straight to the source and created an issue. (Issues can be a great way to thank open source projects you rely on. Not only does it build their knowledge base, and sometimes yours, it starts the conversations needed to outline the sorts of PRs that would be acceptable to the project to either solve your problem or document why certain things are true) There I was schooled on something that I had included in the settings of my testing suites since the beginning of my unit testing existence: the html reporter. If you’ve run into wanting to see coverage lines too, run some yarn test && yarn start on your new open-wc Starter App and then checkout localhost:8080/coverage to get your mind blown by no only a more complete coverage report than I had ever seen before, but also an interactive breakdown of what parts of your code have yet to be tested. Yes, I accept comments (below) in judgment of the things I should probably have already known, as well as for my bad jokes.

If you’re still looking to get the uncovered lines reported in your terminal, take a swing at the following changes to karma.config.js:

module.exports = config => {
    config.set(
        merge.strategy(
            {
                'coverageIstanbulReporter.reports': 'replace',
            }
        )(defaultSettings(config), {
            files: [
                // allows running single tests with the --grep flag
                config.grep ? config.grep : 'test/**/*.test.js',
            ],
            // your custom config
            coverageIstanbulReporter: {
                reports: ['html', 'lcovonly', 'text']
            }
        })
    );
    return config;
};

Notice the usage of 'replace' in the 'coverageIstanbulReporter.reports' property, this allows you to overwrite (rather than “merge”, which is what the code is set to do by default) such that you aren’t given both types of reports. Unless, of course, you’re into that sort of thing...more is actually more, after all.


The Short Game

As voted on by a plurality of people with opinions on such topics that are both forced to see my tweets in their Twitter feed and had a free minute this last week, a 9000+ word article is a no, no.

So, it is with the deepest reverence to you my dear reader that I’ve broken the upcoming conversations into a measly ten sections. Congratulations, you’re nearing the end of the first! If you’ve enjoyed yourself so far, or are one of those people that give a new sitcom a couple of episodes to hit its stride, here’s a list of the others for you to put on your Netflix queue:


Special thanks to the team at Open Web Components for the great set of tools and recommendations that they’ve been putting together to support the ever-growing community of engineers and companies bringing high-quality web components into the industry. Visit them on GitHub and create an issue, submit a PR, or fork a repo to get in on the action!

Posted on by:

Discussion

markdown guide