Jest Tests for KeyPress Event

lightning-web-componentslwc-jest

I am trying to step up my Jest skills and working through Wes Boss classic 30 Vanilla JS projects in 30 Days. I thought I would do them all in LWCs with Jest for a challenge.

I built my Drum Kit and it works like a charm. But I can't figure out how to test this thing.

The root cause seems to be from calling .play() on an audio file.

When I run the test I get an error in the console 'console.error
Error: Not implemented: HTMLMediaElement.prototype.play'

Any advice on the best way forward to test this LWC would be appreciated and any suggestions about material to help with Jest mastery.

Here is my HTML file.

<template>
    <div class="container">
        <div class='key' data-key="65" name="a" ontransitionend={removeTransition}>
            <kbd>A</kbd>
            <span class="sound">Clap</span>
        </div>
        <div class="key" data-key="83" ontransitionend= {removeTransition}>
            <kbd>S</kbd>
            <span class="sound">hihat</span>
        </div>
        <div class="key" data-key="68" ontransitionend={removeTransition}>
            <kbd>D</kbd>
            <span class="sound">kick</span >
        </div>
        <div class="key" data-key="70" ontransitionend={removeTransition}>
            <kbd>F</kbd>
            <span class="sound">openhat</span>
        </div>
        <div class="key" data-key="71" ontransitionend={removeTransition}>
            <kbd>G</kbd>
            <span class="sound">boom</span>
        </div>
        <div class="key" data-key="72" ontransitionend={removeTransition}>
            <kbd>H</kbd>
            <span class="sound">ride</span>
        </div>
        <div class="key" data-key="74" ontransitionend={removeTransition}>
            <kbd>J</kbd>
            <span class="sound">snare</span>
        </div>
        <div class="key" data-key="75" ontransitionend={removeTransition}>
            <kbd>K</kbd>
            <span class="sound">tom</span>
        </div>
        <div class="key" data-key="76" ontransitionend={removeTransition}>
            <kbd>L</kbd>
            <span class="sound">tink</span>
        </div>

    </div>
</template>

Here is my JS file.

import {LightningElement} from 'lwc';
import Boom from '@salesforce/resourceUrl/sounds_boom';
import Clap from '@salesforce/resourceUrl/sounds_clap';
import Hihat from '@salesforce/resourceUrl/sounds_hihat';
import Kick from '@salesforce/resourceUrl/sounds_kick';
import Openhat from '@salesforce/resourceUrl/sounds_openhat'
import Ride from '@salesforce/resourceUrl/sounds_ride';
import Snare from '@salesforce/resourceUrl/sounds_snare';
import Tink from '@salesforce/resourceUrl/sounds_tink';
import Tom from '@salesforce/resourceUrl/sounds_tom'
export default class Drumkit extends LightningElement {
    wavMap = new Map();

    constructor() {
        super();
        this.wavMap.set(65, Clap);
        this.wavMap.set(83, Hihat);
        this.wavMap.set(68, Kick);
        this.wavMap.set(70, Openhat);
        this.wavMap.set(71, Boom);
        this.wavMap.set(72, Ride);
        this.wavMap.set(74, Snare);
        this.wavMap.set(75, Tom);
        this.wavMap.set(76, Tink)
    }

    connectedCallback() {
        window.addEventListener('keypress', (event) => {
            this.playSound(event)
        })
    }

    disconnectedCallback() {
        window.removeEventListener("keypress", (event) => {
            this.playSound(event) })
    }

    playSound(event) {
        const sound = new Audio(this.wavMap.get(event.keyCode));
        if (!sound){
            return;
        }
        const element = this.template.querySelector(`[data-key="${event.keyCode}"]`);
        sound.currentTime = 0;
        sound.play();
        element.classList.add('playing')
    }

    removeTransition(event){
       if (event.propertyName !== 'transform'){
           return;
       }
        event.target.classList.remove('playing');
    }
}

The Test

import {createElement} from 'lwc';
import Drumkit from 'c/drumkit'

describe('c-drumkit', ()=> {
    beforeEach(() => {
        const element = createElement('c-drumkit', {
            is: Drumkit
        });
        document.body.appendChild(element)
    })
    test('attach event listener to window', ()=> {
        const drumkit = document.querySelector('c-drumkit');
        const element = drumkit.shadowRoot.querySelector('div[data-key="65"]');
        expect(element).not.toBeNull();
       const keyPressEvent = new KeyboardEvent('keypress', {
           'keyCode': 65
       })
        window.dispatchEvent(keyPressEvent);
       return Promise.resolve().then( () => {
           const element = drumkit.shadowRoot.querySelector('div[data-key="65"]');
           console.log(element.innerHTML)
           expect(element.classList).toContain('playing');
       })
    })
})

Best Answer

With jest, you end up mocking modules/imports or apis that don't actually exist within the context of the test. That way, when your javascript interacts with them - a value is still returned when its called to let it go through its logic. For example, an apex method called by your lwc isn't actually called (as it isn't able to be) - but, you define a mock that returns a value you define when it's called.

In this case, you're interacting with HTMLMediaElement, but it does not exist in the context of a jest test. You'll have to mock the return/API so jest does not error and you can assert whether it was appropriately called or not.

You can leverage spyOn to mock it and keep track of it being called or not. Note that, within that method, it mentions that

By default, jest.spyOn also calls the spied method. This is different behavior from most other test libraries. If you want to overwrite the original function, you can use jest.spyOn(object, methodName).mockImplementation(() => customImplementation)

So, you also want to provide a mock implementation (value that is returned instead of actually calling it). In this case, it can just be empty so you can verify it's called.

cont playStub = jest
    .spyOn(window.HTMLMediaElement.prototype, 'play')
    .mockImplementation( () => {} );

window.dispatchEvent(keyPressEvent);
       return Promise.resolve().then( () => {
       expect(playStub).toHaveBeenCalled();
})

The above confirms that the keypress does call HTMLMediaElement.play()

Related Topic