Testing with JS is like magic, but is it science?

By admin
If that title doesn’t get you riled up, I know what will…

Last time on "

Testing Web Components
WORK_OF_ART

"

That’s right, a recap!

If you haven’t read the

first
ORDINAL

installment of

Testing Web Components with @web
ORG

/test-runner, go do it now… I’ll wait… If you just did, or you had previously, here are some fun things to have at the top of your mind before digging into

today
DATE

‘s episode:

you can quickly generate a new web component repo with

Open-WC
PRODUCT

the generator has a whole lot of options, but specifically, we’re cooking with "Testing (web-test-runner)" and Typescript

the web component generated thereby is powered by

Lit
WORK_OF_ART

and leverages many of its decorators

by default, it tests a few things intrinsic to the generated element, but mostly you’ll be replacing that functionality with your own

…and that’s where we come in!

Testing with JS

I can hear you now:

What do you mean, all of my JS tests are in JS, what’s so magic about it?

To which I’d respond, with a voice of disbelief, "You’re testing JS with JS, what isn’t magic about it?"

Then I’d remember that if you lived in my head already and knew all the jokes, you’d likely not be reading by this point. But, you’re here, so I’ll point out that I’m not talking about "magic"; that amazing, powerful stuff of lore that saves the heroine in your favorite story. I’m talking about "magic"; the thing that isn’t real. Yes, testing your JS with JS isn’t real. Or, it isn’t real for

at least half
CARDINAL

of your consumer types, which likely represent an overwhelmingly large percentage of your actual consumers. That is the consumers that actually visit the page or application that you are building.

To understand what I mean, let’s revisit one of the tests that we’ve been bequeathed by

the Open WC Generator
ORG

.

it ( ‘ increases the counter on button click ‘ , async () => { const el = await fixture <

TestingComponents
ORG

> ( html `<testing-components></testing-components>` );

el .
GPE

shadowRoot ! . querySelector ( ‘ button ‘ ) ! . click (); expect ( el . counter ). to . equal (

6
CARDINAL

); }); Enter fullscreen mode

Exit
PRODUCT

fullscreen mode

In this test, we confirm that "clicking" the <button> within the shadow root of the <testing-components> element causes the counter property on our element to equal

6
CARDINAL

.

This is an extremely common approach to interacting with the

UI
ORG

at unit testing time with JS.

Interacting with the

UI
ORG

.click() is a powerful part of the web API. It allows a developer to "simulate" the click segment of a pointer interaction (possibly the most common segment) from the JS context. In this way, it can be a great way to guarantee the developer API that you surface with code.

However, it’s not real.

How do I know it’s not real? Well, when was the last time you saw a visitor to your site or application type el.click() into the console of their browser’s DevTools? You, me, and our developer friends at debug time don’t count here!

This is a version of what’s called "mocking". Mocking can be an important part of unit testing as it allows you to isolate the unit under test. This means that you can ignore inputs, side effects, and other realities applied to your unit by other contexts when testing that unit. When testing

UI
ORG

interactions, whether and how to mock the human that will be interacting with your

UI
ORG

will always be important to work out.

Some testing libraries look to mitigate this by more expansively mocking the interaction. You should definitely spend some time deciding whether the work going on here is something you think your testing needs, but, in a quick overview, some of the things being handled here are:

possibly moving the pointer

deciding whether the pointer should be released before starting a new interaction

pressing down the pointer on a new (or the last) position

possibly releasing the pointer again

managing interaction with the "carat", which I couldn’t

100%
PERCENT

tell you what is in this context


Luckily
WORK_OF_ART

, this complexity is then hidden under helper methods because no one will remember to do all this every time they want to better mimic a .click() .

Is there something better?

— exasperated unit tester

Well, @web/test-runner offers a

Commands
ORG

API that supports you making requests to the browser runner of your tests from the JS test context. At a high level, this means that you can ask the browser runner (e.g. Playwright, Selenium, etc.) that you leverage at test time to act on the test context as if it actually were a person interacting with the page. By doing so, you can trigger things like:

pointer interactions (hello, .click() , but better!)

, but better!) keyboard interactions

interact with <select> menus (their default

UI
ORG

is outside of the browsing context)

menus (their default

UI
ORG

is outside of the browsing context) resize the window

and, more!

For a .click() , this looks a bit like the following:

await sendMouse ({ type : ‘ move ‘ , position : [ x , y ] }); Enter fullscreen mode

Exit
PRODUCT

fullscreen mode

Looks pretty awesome, right? Well, that’s not what we’re going to be diving into

today
DATE

, sorry. In the next couple of installments, we’ll take deeper looks at a number of these commands, and how you can write your own!

For

today
DATE

, we’re going to dive into the developer API that your newly generated custom element surfaces, and you’re gonna like it.

Interacting with the developer API

As mentioned above, .click() is one way to surface interactions on your element to your developer consumers. In that way, this test could be

100%
PERCENT

real, and actually magic (the amazing kind). In this context, however, it’s not.

The offending code in this case is .shadowRoot! . While .click() can be a powerful API for consuming developers, if you’re testing such an API on an element that a developer shouldn’t have access to (any in the shadow

DOM
ORG

), then it’s not real.

Using shadow

DOM
ORG

on an element encapsulates the contents of that shadow root from selectors from the parent application or component. This is why we can’t use el.querySelector directly to access this button. Not being able to do so, means that a consuming developer will not be able to do so as well. So, while reaching through the .shadowRoot! may be a way to complete the coverage play of a component’s unit tests, it’s not something that actually guarantees actual

API
ORG

that any of your consumers would actually leverage.

With this in mind, our test development cycle might go

one
CARDINAL

of a couple of different ways. You could:

say, "Output says the lines are covered, I’m going home", and you’d not be wrong.

leverage the commands from above and close over the

API
ORG

to external developers by only testing as a visitor and not as a developer consuming your component.

or, start a refactor party and look at what would be needed to make this

API
ORG

available to a developer.

Before you take the path of most resistance, ask yourself "As a developer would I want this capability to be surfaced in some way?". If the answer is "yes", then hopefully wanting it points you towards a path of how you’d want it surfaced, but if not, keep reading for some thoughts on what that might mean.

The developer API

Sometimes a developer

API
ORG

is already available on the surface of your custom element, you just have to hold it right. Let’s revisit our test to see if that might be appropriate here:

it ( ‘ increases the counter on button click ‘ , async () => { const el = await fixture <

TestingComponents
ORG

> ( html `<testing-components></testing-components>` );

el .
GPE

shadowRoot ! . querySelector ( ‘ button ‘ ) ! . click (); expect ( el . counter ). to . equal (

6
CARDINAL

); }); Enter fullscreen mode

Exit
PRODUCT

fullscreen mode

What does "increases the counter on button click" mean, again:

we load the element we find and click the button the counter equals

6
CARDINAL

Ignoring for the moment the lack of precision as to how much the counter is increased, or coverage of what the counter should be by default (that’s technically addressed in a previous test in our generated code), the operative word here is that a button clicked increases the counter . This test is

100%
PERCENT

for end consumers of your element. What might it be called if we were testing these capabilities for a developer consumer?

counter can be set?

can be set? counter can be increased?

can be increased? element can be clicked?

something else?

Let’s look at how we might build these tests and refactor our element to support them.

counter can be set

it ( ‘ `counter` can be set ‘ , async () => { const el = await fixture <

TestingComponents
ORG

> ( html `<testing-components></testing-components>` );

el .
GPE

counter =

6
CARDINAL

; // This isn’t really a test of the side effects of the counter being set, so wouldn’t be my

first
ORDINAL

choice. expect ( el . counter ). to . equal (

6
CARDINAL

); }); Enter fullscreen mode

Exit
PRODUCT

fullscreen mode

This test confirms that not only is counter a getable property as confirmed in ‘has a default title "Hey there" and counter

5
CARDINAL

‘ and the original version of ‘increases the counter on button click’, but it is also settable. Being this property leverages the @property decorator from

Lit
WORK_OF_ART

, I can tell you for sure that it does have a setter, though our tests certainly hadn’t guaranteed that previously. So, we’d be adding robustness to the generated code if we chose to add something like this. I would say that testing a setter with its getter might leave something to be desired. Once we get deep into them in subsequent articles, confirming that the new value of counter is leveraged by the rendered

DOM
ORG

via snapshot testing (not my favorite, I’ll share more about that later) or actually rendered to the page via visual regression testing (key for

UI
ORG

libraries) would be a valuable extension of the test here.

counter can be increased

it ( ‘ `counter` can be increased ‘ , async () => { const el = await fixture <

TestingComponents
ORG

> ( html `<testing-components></testing-components>` ); // The

API
ORG

doesn’t currently exist on the element but could be surface as a public method for developers and leveraged within the click callback.

el .
GPE

increaseCounter (); expect ( el . counter ). to . equal (

6
CARDINAL

); }); Enter fullscreen mode

Exit
PRODUCT

fullscreen mode

"Increased" here is playing fast and loose with the context of the original test. "increases the counter", as the only spec for the element, implies (in a way that is played out in the definition of the custom element) that you can only increase the value of counter . If this were true, I might also suggest you cover over the setter of counter to prevent developer consumers from decreasing the value of counter . For now, we’ll just go with increasing being the only thing we can do.

Based on this assumption, the right thing might be just to surface the internal __increment() method in a public manner. For brevities sake, we could rename it increaseCounter , as seen in the test:

– __increment() { + increaseCounter() { Enter fullscreen mode

Exit
PRODUCT

fullscreen mode

Then we’d refactor the click handler to leverage the same:

– <button @click=${this.__increment}>increment</button> + <button @click=${this.increaseCounter}>increment</button> Enter fullscreen mode

Exit
PRODUCT

fullscreen mode

And, then we’d be back to passing tests.

element can be clicked

Rather than requiring a consuming developer to find a specific element to click on, where you are sure that this was the only clickable functionality in your element, you might be better off by surfacing a custom click() method directly on your custom element. This can be tested as follows:

it ( ‘ element can be clicked ‘ , async () => { const el = await fixture <

TestingComponents
ORG

> ( html `<testing-components></testing-components>` ); // This

API
ORG

is also not currently available, but by abstracting the idea of what clicking the element does you may be setting down a path where it does many things. el . click (); expect ( el . counter ). to . equal (

6
CARDINAL

); }); Enter fullscreen mode

Exit
PRODUCT

fullscreen mode

The refactor to make this possible is only a few steps in this case:

add the click() method

method on, just

one
CARDINAL

step, I guess

+ click() { + this.__increment(); + } Enter fullscreen mode

Exit
PRODUCT

fullscreen mode

Ship it!

This means that you are binding your custom element to a singular action at click time, so be sure this is what is actually intended for the functionality that you are shipping, but, if so, you’re on your way to version

1.0
CARDINAL

!

something else

All of these, and more, could serve your developer consumers by ensuring the

API
ORG

contract that you surface to them at unit test time. Within each is a sea of nuance that can be navigated more appropriately with the deeper knowledge of your goals that only you can bring to the discussion.

Should your custom element support the https://github.com/webcomponents-cg/community-protocols/blob/main/proposals/context.md as a path to developer interaction?

Should a more proprietary state interface be surfaced? Web components and

Lit
WORK_OF_ART

pair quite nicely with popular projects like

Mobx
PERSON

,

Redux, XState
ORG

, and on and on.

Should you devise a purpose-built custom events API for orchestrating imperative cross-DOM interactions? (Probably not..but, you could.)

Should a similar approach to increaseCounter() above be leveraged against a more specific method name clarifying the internal reaction to the state change?

Only you can answer these questions. However, what’s great about code is that if you can imagine it then with the right amount of time you can code it, so go wild!

Being less "magic" and more "amazing"

Focusing your approach to testing on how the actual consumers of your code will interact with that code is an important part of ensuring that the work you do is less magic (the not real kind). The less magic in your tests, the more predictable your work will become. Contrary to popular belief shipping simple, dependable, well-tested features is much more amazing for you consumers than something that could disappear right in front of them.

Above we’ve learned about some refactoring strategies that our testing cycle can point us towards as we work to make our

API
ORG

contracts simpler, more dependable, and more likely to support our developer consumers to amazing results. Next time, we’ll get back to our

UI
ORG



first
ORDINAL

consumers and how we can better embody them at test time, as well. Specifically, we’ll focus on the

Commands
ORG

API from @web/test-runner and ways to more accurately mock pointer interactions with your code. See you then!