| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154 | "use strict";/** * @fileoverview Interactions should be awaited * @author Yann Braga */const create_storybook_rule_1 = require("../utils/create-storybook-rule");const constants_1 = require("../utils/constants");const ast_1 = require("../utils/ast");module.exports = (0, create_storybook_rule_1.createStorybookRule)({    name: 'await-interactions',    defaultOptions: [],    meta: {        docs: {            description: 'Interactions should be awaited',            categories: [constants_1.CategoryId.ADDON_INTERACTIONS, constants_1.CategoryId.RECOMMENDED],            recommended: 'error', // or 'warn'        },        messages: {            interactionShouldBeAwaited: 'Interaction should be awaited: {{method}}',            fixSuggestion: 'Add `await` to method',        },        type: 'problem',        fixable: 'code',        hasSuggestions: true,        schema: [],    },    create(context) {        // variables should be defined here        //----------------------------------------------------------------------        // Helpers        //----------------------------------------------------------------------        // any helper functions should go here or else delete this section        const FUNCTIONS_TO_BE_AWAITED = [            'waitFor',            'waitForElementToBeRemoved',            'wait',            'waitForElement',            'waitForDomChange',            'userEvent',            'play',        ];        const getMethodThatShouldBeAwaited = (expr) => {            const shouldAwait = (name) => {                return FUNCTIONS_TO_BE_AWAITED.includes(name) || name.startsWith('findBy');            };            // When an expression is a return value it doesn't need to be awaited            if ((0, ast_1.isArrowFunctionExpression)(expr.parent) || (0, ast_1.isReturnStatement)(expr.parent)) {                return null;            }            if ((0, ast_1.isMemberExpression)(expr.callee) &&                (0, ast_1.isIdentifier)(expr.callee.object) &&                shouldAwait(expr.callee.object.name)) {                return expr.callee.object;            }            if ((0, ast_1.isTSNonNullExpression)(expr.callee) &&                (0, ast_1.isMemberExpression)(expr.callee.expression) &&                (0, ast_1.isIdentifier)(expr.callee.expression.property) &&                shouldAwait(expr.callee.expression.property.name)) {                return expr.callee.expression.property;            }            if ((0, ast_1.isMemberExpression)(expr.callee) &&                (0, ast_1.isIdentifier)(expr.callee.property) &&                shouldAwait(expr.callee.property.name)) {                return expr.callee.property;            }            if ((0, ast_1.isMemberExpression)(expr.callee) &&                (0, ast_1.isCallExpression)(expr.callee.object) &&                (0, ast_1.isIdentifier)(expr.callee.object.callee) &&                (0, ast_1.isIdentifier)(expr.callee.property) &&                expr.callee.object.callee.name === 'expect') {                return expr.callee.property;            }            if ((0, ast_1.isIdentifier)(expr.callee) && shouldAwait(expr.callee.name)) {                return expr.callee;            }            return null;        };        const getClosestFunctionAncestor = (node) => {            const parent = node.parent;            if (!parent || (0, ast_1.isProgram)(parent))                return undefined;            if ((0, ast_1.isArrowFunctionExpression)(parent) ||                (0, ast_1.isFunctionExpression)(parent) ||                (0, ast_1.isFunctionDeclaration)(parent)) {                return node.parent;            }            return getClosestFunctionAncestor(parent);        };        const isUserEventFromStorybookImported = (node) => {            return (node.source.value === '@storybook/testing-library' &&                node.specifiers.find((spec) => (0, ast_1.isImportSpecifier)(spec) &&                    spec.imported.name === 'userEvent' &&                    spec.local.name === 'userEvent') !== undefined);        };        const isExpectFromStorybookImported = (node) => {            return (node.source.value === '@storybook/jest' &&                node.specifiers.find((spec) => (0, ast_1.isImportSpecifier)(spec) && spec.imported.name === 'expect') !== undefined);        };        //----------------------------------------------------------------------        // Public        //----------------------------------------------------------------------        /**         * @param {import('eslint').Rule.Node} node         */        let isImportedFromStorybook = true;        const invocationsThatShouldBeAwaited = [];        return {            ImportDeclaration(node) {                isImportedFromStorybook =                    isUserEventFromStorybookImported(node) || isExpectFromStorybookImported(node);            },            VariableDeclarator(node) {                isImportedFromStorybook =                    isImportedFromStorybook && (0, ast_1.isIdentifier)(node.id) && node.id.name !== 'userEvent';            },            CallExpression(node) {                var _a;                const method = getMethodThatShouldBeAwaited(node);                if (method && !(0, ast_1.isAwaitExpression)(node.parent) && !(0, ast_1.isAwaitExpression)((_a = node.parent) === null || _a === void 0 ? void 0 : _a.parent)) {                    invocationsThatShouldBeAwaited.push({ node, method });                }            },            'Program:exit': function () {                if (isImportedFromStorybook && invocationsThatShouldBeAwaited.length) {                    invocationsThatShouldBeAwaited.forEach(({ node, method }) => {                        const parentFnNode = getClosestFunctionAncestor(node);                        const parentFnNeedsAsync = parentFnNode && !('async' in parentFnNode && parentFnNode.async);                        const fixFn = (fixer) => {                            const fixerResult = [fixer.insertTextBefore(node, 'await ')];                            if (parentFnNeedsAsync) {                                fixerResult.push(fixer.insertTextBefore(parentFnNode, 'async '));                            }                            return fixerResult;                        };                        context.report({                            node,                            messageId: 'interactionShouldBeAwaited',                            data: {                                method: method.name,                            },                            fix: fixFn,                            suggest: [                                {                                    messageId: 'fixSuggestion',                                    fix: fixFn,                                },                            ],                        });                    });                }            },        };    },});
 |