salesforce / sfdx-lwc-jest

Run Jest against LWC components in SFDX workspace environment
MIT License
164 stars 81 forks source link

Jest throws error because of LWC restriction on accessing tagName #299

Closed jeffhube closed 1 year ago

jeffhube commented 1 year ago

Description

Jest, in certain cases, may attempt to print out a lightning component element. In doing so, it will attempt to access the tagName, which throws an error:

Usage of property `tagName` is disallowed because the component itself does not know which tagName will be used to create the element, therefore writing code that check for that value is error prone.

This error comes from https://github.com/salesforce/lwc/blob/61a89c3f41a211bcc27b4878f2f2d83b279e1ea6/packages/%40lwc/engine-core/src/framework/restrictions.ts#L272

I encountered this when attempting to assert that a method was not called, when it was in fact called, and a lightning component element was passed as an argument. Jest attempts to print out the arguments to the method that was not expected to be called. If one of those arguments is lightning component element, it attempts to access tagName and throws an error.

For reference, this is what Jest's output normally looks like, when the argument is "a string" rather than a lightning component element.

expect(jest.fn()).not.toBeCalled()

Expected number of calls: 0
Received number of calls: 1

1: "a string"

Steps to Reproduce

// simplified test case
import { createElement } from 'lwc';
import { foo } from 'c/util';
import MyComponent from 'c/myComponent';

jest.mock('c/util', () => ({ foo: jest.fn() }));

describe('c-my-component', () => {
    it('does not call foo', () => {
        const element = createElement('c-my-component', { is: MyComponent });
        document.body.appendChild(element);

        expect(foo).not.toBeCalled();
    });
});
<!-- HTML for component under test -->
<template></template>
// JS for component under test (myComponent)
import { LightningElement } from 'lwc';
import { foo } from 'c/util';

export default class MyComponent extends LightningElement {
    connectedCallback() {
        foo(this);
    }
}
// JS for helper component (util)
export function foo(bar) { }
// Jest config overrides
const { jestConfig } = require('@salesforce/sfdx-lwc-jest/config');
module.exports = {
    ...jestConfig,
    modulePathIgnorePatterns: ['<rootDir>/.localdevserver']
};
# Command to repro
sfdx-lwc-jest -- --no-cache

Expected Results

The unit test fails with an error about how foo was called 1 time but was expected to be called 0 times.

Actual Results

The unit test fails with an error about accessing tagName.

 FAIL  force-app/main/default/lwc/myComponent/__tests__/myComponent.test.js
  c-my-component
    × does not call foo (25 ms)

  ● c-my-component › does not call foo

    Usage of property `tagName` is disallowed because the component itself does not know which tagName will be used to create the element, therefore writing code that check for that value is error prone.

      10 |         document.body.appendChild(element);
      11 |
    > 12 |         expect(foo).not.toBeCalled();
         |                         ^
at/build/index.js:602:22)
      at node_modules/.pnpm/expect@28.1.3/node_modules/expect/build/spyMatchers.js:40:51
          at Array.map (<anonymous>)
      at printReceivedArgs (node_modules/.pnpm/expect@28.1.3/node_modules/expect/build/spyMatchers.js:35:10)
      at node_modules/.pnpm/expect@28.1.3/node_modules/expect/build/spyMatchers.js:376:41
          at Array.reduce (<anonymous>)
      at node_modules/.pnpm/expect@28.1.3/node_modules/expect/build/spyMatchers.js:374:14
      at getMessage (node_modules/.pnpm/expect@28.1.3/node_modules/expect/build/index.js:169:15)
      at processResult (node_modules/.pnpm/expect@28.1.3/node_modules/expect/build/index.js:293:25)
      at Object.throwingMatcher [as toBeCalled] (node_modules/.pnpm/expect@28.1.3/node_modules/expect/build/index.js:362:16)      
      at Object.toBeCalled (force-app/main/default/lwc/myComponent/__tests__/myComponent.test.js:12:25)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        3.99 s
Ran all test suites.

Version

Additional context/Screenshots

Here are some details about how I encountered this. I have a utility component that contains a method for displaying an error as a toast.

export function errorToast(element, error) {
    element.dispatchEvent(new ShowToastEvent(/* ... */));
}

When a component wants to display an error toast, it calls errorToast, passing itself and the error.

import { errorToast } from 'c/util';
// ...
errorToast(this, error);

In a test, I want to confirm that the errorToast function was not called.

expect(errorToast).not.toHaveBeenCalled();
nolanlawson commented 1 year ago

Thanks for opening the issue. This is currently by design.

However, there is a case to be made that we should not throw an error in dev mode where the same error would not be thrown in prod mode: https://github.com/salesforce/lwc/issues/3245

As a quick fix, we can disable this check in Jest.

nolanlawson commented 1 year ago

Fixed by https://github.com/salesforce/lwc/issues/3245. Thanks for reporting!