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,
- },
- ],
- });
- });
- }
- },
- };
- },
- });
|