salesforce / sfdx-lwc-jest

Run Jest against LWC components in SFDX workspace environment
MIT License
165 stars 82 forks source link

Incorrect coverage when mocked apex function and copyright placed before its import #320

Open framocma opened 1 year ago

framocma commented 1 year ago

Description

First of all, my apologies if the issue description lacks enough details or it is not thorough enough as it is the first issue I report via Github. After upgrading some dependencies in the company's repo it was noticed a degradation in the function's coverage due to a new function added to the calculation that turns out to be coming from the Apex function being imported. It occurs when this function is mocked in the test class and the JS class under testing has a copyright before the import (which is mandatory in our company's policy).

Steps to Reproduce

/**
 * @copyright 2023 FranM.
 */

import { getCreateComponent } from 'jest-utils';
import dummySayHi from '@salesforce/apex/CoverageIssueWithCopyrightAndApex.dummySayHi';
import coverageWithApexModal from 'c/coverageWithApexModal';

jest.mock('@salesforce/apex/CoverageIssueWithCopyrightAndApex.dummySayHi', () => ({ default: jest.fn() }), { virtual: true });

const createComponent = getCreateComponent(coverageWithApexModal, 'c-coverage-with-apex-modal');

describe('coverageWithApexModal component test', () => {
    describe('apply clicked', () => {
        it('controller is called', () => {
            //Given
            const { component } = createComponent();
            const applyBtn = component.shadowRoot.querySelector('.apply-btn');

            //When
            applyBtn.click();

            //Then
            expect(dummySayHi).toHaveBeenCalledTimes(1);
        });
    });
});
<!-- HTML for component under test -->
<template>
    <div class="slds-is-relative">
        <lightning-button label="apply" class="apply-btn slds-m-left_x-small" onclick={handleSayHi}></lightning-button>
    </div>
</template>
// JS for component under test
/**
 * @copyright 2023 FranM.
 */

import { LightningElement } from 'lwc';
import dummySayHi from '@salesforce/apex/CoverageIssueWithCopyrightAndApex.dummySayHi';

const NAME = 'Peter';

export default class CoverageWithApex extends LightningElement {
    handleSayHi() {
        const result = dummySayHi(NAME);
        console.log(result);
    }
}
public with sharing class CoverageIssueWithCopyrightAndApex {
    @AuraEnabled(cacheable=true)
    public static String dummySayHi(String name) {
        return 'Hi, ' + name;
    }
}
const { jestConfig } = require('@salesforce/sfdx-lwc-jest/config');
const { setupFilesAfterEnv } = jestConfig;

module.exports = {
    ...jestConfig,
    setupFilesAfterEnv: [...setupFilesAfterEnv, './jest.setup.js'],
    clearMocks: true,
    roots: ['<rootDir>/force-app/main'],
    transformIgnorePatterns: ['/node_modules/(?!(.*@salesforce/sfdx-lwc-jest/src/lightning-stubs)/)'],
    moduleDirectories: ['<rootDir>/node_modules', '<rootDir>/force-app/test/jest-utils'],
    moduleNameMapper: {
        '^lightning/actions$': '<rootDir>/force-app/main/common/test/jest-mocks/actions',
    },
    collectCoverageFrom: ['force-app/main/**/lwc/**/*.js', '!**/__tests__/**', '!**/__mocks__/**'],
    coverageThreshold: {
        global: {
            statements: 98.84,
            branches: 96.39,
            functions: 99.47,
            lines: 98.9,
        },
    },
    reporters: [
        'default',
        [
            'jest-junit',
            {
                suiteName: 'LWC Tests',
                outputDirectory: 'test-results/jest',
                outputName: './jest-result.xml',
                classNameTemplate: '{classname}',
                titleTemplate: '{title}',
                ancestorSeparator: ' → ',
            },
        ],
    ],
};
# Command to repro
npm run lwc-jest --coverage

Expected Results

Function coverage to be 100%

Actual Results

Function coverage is 66.66%

Version

Additional context/Screenshots

Screenshot 2023-06-28 at 14 20 56 Screenshot 2023-06-28 at 14 24 11
nolanlawson commented 1 year ago

W-13720527

BatemanVO commented 1 year ago

Our team was on v1.3.0 and just updated to v1.4.1 today, but began encountering the same issue sporadically with any comments whatsoever before import statements. It doesn't occur on all files / tests, but after updating to the latest version, a significant number of them started reporting that line 1 was uncovered, which was just the opening to a comment block.

Moving the comments beneath all import statements resolves the issue.

Here is a sample file that is affected:

// Used by the VNextComponent Visualforce Page to handle logic needed after the VNext Canvas App has been rendered
import { LightningElement, api, track } from 'lwc';
import { showError } from 'c/lwcUtilities';
import stampConversationId from '@salesforce/apex/VNextComponentController.stampConversationId';
import leaveConversation from '@salesforce/apex/VNextComponentController.leaveConversation';

export default class Vnext extends LightningElement {
    // @api properties passed from the expected parent VF Lightning Out page
    @api publish;
    @api subscribe;
    @api recordId;
    @api partnerId;
    @track isAttemptingToLeaveConversation = false;
    @track hasLeftConversation = false;

    connectedCallback() {
        this.subscribe([{
            name: 'WorkspaceEvent',
            onData: event => {
                if (!event.payload) { return; }

                if (event.payload.messageCategory === 'ConversationCreated' && event.payload.conversationId) {
                    stampConversationId({
                        recordId: this.recordId,
                        conversationId: event.payload.conversationId
                    })
                        .catch(this.showError.bind(this));
                }
            }
        }]);
    }
    handleLeaveConversation() {
        this.isAttemptingToLeaveConversation = true;
        leaveConversation({
            partnerId: this.partnerId,
            recordId: this.recordId
        }).then(() => {
            this.dispatchEvent(new CustomEvent('toastemulation', {
                detail: {
                    title: 'Conversation Ended',
                    variant: 'success',
                    type: 'success',
                    message: 'Conversation successfully left'
                }
            }));
            this.hasLeftConversation = true;
        }).catch(this.showError.bind(this))
            .finally(() => this.isAttemptingToLeaveConversation = false);
    }
    showError(error) {
        this.dispatchEvent(new CustomEvent('toastemulation', {
            detail: {
                variant: 'error',
                type: 'error',
                mode: 'sticky',
                message: showError.call(this, error),
                title: 'Error'
            }
        }));
    }
}

Moving the comment from line 1 to line 5, beneath the import statements, fixes the coverage issue.

Here is a sample file that is not affected:

// Reusable button used as part of the caseConsoleButtonPanel component. For buttons that don't need complex logic and
// should just always update a record, this component can be used to handle those updates.
import { LightningElement, api } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';
import { updateRecord } from 'lightning/uiRecordApi';
import { showError } from 'c/lwcUtilities';

export default class RecordUpdateButton extends LightningElement {
    @api recordId;
    @api label;
    @api variant;
    @api recordInput;

    /**
     * Handler for when the button is clicked. Calls the Apex method associated with this LWC and, if no errors are thrown,
     * shows a success Toast and fires a refresh event.
     */
    handleClick() {
        this.dispatchEvent(new CustomEvent('showspinner'));
        updateRecord(this.recordInput)
            .then(() => {
                this.dispatchEvent(new CustomEvent('refresh'));
                this.dispatchEvent(new ShowToastEvent({
                    title: 'Success',
                    variant: 'success',
                    message: 'Record updated'
                }));
            })
            .catch(showError.bind(this))
            .finally(() => this.dispatchEvent(new CustomEvent('hidespinner')));
    }
}

So as noted in the original issue, it appears to be related to files that import Apex functions.

sfdx-lwc-jest v1.4.1 Node v18.9.0 OS Windows 10

nolanlawson commented 1 year ago

I wonder if this would be fixed with a Jest update. Currently sfdx-lwc-jest is on Jest v27 (latest is v29).

nolanlawson commented 1 year ago

Could someone who is having this issue please provide a GitHub repo that we can use to test? From the bug description, it's hard to tell where to put which files (and with which filenames) in order to reproduce the issue. Thank you!

BatemanVO commented 1 year ago

@nolanlawson Here is a link to a barebones repo where I can reproduce the issue.

Here is the coverage reported with the comment left at the top of the file: with-comment-at-top uncovered-line

Here is the coverage reported if I move the comment two lines down, beneath the import statements: with-comment-below-imports

EDIT: As an additional note, if I uninstall 1.4.1 and reinstall to the latest version, 2.2.0, I still get the same issue.

nolanlawson commented 1 year ago

Thanks a lot @BatemanVO! With your repo (using npm install && npx lwc-jest --coverage, then opening coverage/lcov-report/index.html), I am able to repro:

Screenshot 2023-08-10 at 11 05 41 AM

Unfortunately, it looks like upgrading to Jest v29 does not resolve the issue:

Screenshot 2023-08-10 at 11 19 29 AM

This will require some more digging to figure out what's going on.

zoranmilovanovic commented 10 months ago

Same issue is present in our sfdx project. I created also similar git repository where it can be tested https://github.com/zoranmilovanovic/jest-coverage-issue

Is there any update on this issue?

BatemanVO commented 8 months ago

Just updated a couple of our repos to sfdx-lwc-jest 4.0.1 today and still encountering the same issue. Some files that were not affected with version 3.0.1 were affected after updating to 4.0.1, but I didn't see anything significant between the ones affected in 4.0.1, but not 3.0.1, compared to the ones that were affected in 3.0.1 - it still seems to be tied to importing apex methods.

BatemanVO commented 4 months ago

Still seeing the issue with sfdx-lwc-jest 5.0.0 as of today, though have noticed a slight change in behavior.

The coverage tool still reports comments before import statements as uncovered, but instead of always reporting line 1 as uncovered, the line reported is the last comment using //. For example:

// Some comment
/* Start of a comment block
Some comments
End of a comment block */

Will report line 2 as uncovered, whereas:

// A comment
// Another comment
// Yet another comment

will report line 3 as uncovered.

EDIT, 10/16/2024: Still occurring as of 5.1.0.