Skip to content

Commit

Permalink
Await skeletons (#1)
Browse files Browse the repository at this point in the history
* ast utils

* no unawaited skeletons

* fixes
- add testId schema
- better wording in reported message

* fixes
- hide internal functions
- add comments

Co-authored-by: Valentyn Patsera <[email protected]>
  • Loading branch information
phewphewb and fsvapat authored Dec 21, 2022
1 parent cce81d5 commit a3ab48f
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { circularDependency } from "./circularDependency";
import { crossReference } from "./crossReference";
import { gqlObjects, gqlOperationName } from "./gqlRules";
import { noRenamedTranslationImport } from "./noRenamedTranslationImport";
import { noUnawaitedSkeletons } from "./noUnawaitedSkeletons";
import { oneTranslationImport } from "./oneTranslationImport";

const rules = {
Expand All @@ -11,6 +12,7 @@ const rules = {
"gql-operation-name": gqlOperationName,
"cross-reference": crossReference,
"circular-dependency": circularDependency,
"no-unawaited-skeletons": noUnawaitedSkeletons,
};

export { rules };
1 change: 1 addition & 0 deletions src/noUnawaitedSkeletons/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * as noUnawaitedSkeletons from "./noUnawaitedSkeletons";
119 changes: 119 additions & 0 deletions src/noUnawaitedSkeletons/noUnawaitedSkeletons.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { isCalleName, isCallExpression, isExpect, isLiteral, findByPaths } from "../utils";

const meta = {
type: "problem",
docs: {
category: "code",
description:
"Enforces skeletons to be wrapped into `waitFor` to avoid `should be wrapped in act(...) error`",
},
hasSuggestions: false,
fixable: false,
schema: [
{
type: "object",
properties: {
testIds: {
type: "array",
items: {
type: "string",
},
},
},
required: ["testIds"],
additionalProperties: false,
},
],
};

const isWaitFor = node => isCalleName(node, "waitFor");

/**
* @description the below constant array is for the following code examples
* [0]
* await waitFor(() => {
* expect(screen.queryByTestId("aviary-skeleton")).not.toBeInTheDocument();
* });
* [1]
* await waitFor(() => {
* expect(screen.getByTestId("aviary-skeleton")).toBeInTheDocument();
* });
* [2]
* await waitFor(() => expect(screen.queryAllByTestId("aviary-skeleton")).toHaveLength(0));
* [3]
* expect(screen.queryByTestId("aviary-skeleton")).not.toBeInTheDocument()
*/

const WAIT_FOR_PATHS = [
[
"CallExpression",
"MemberExpression",
"MemberExpression",
"CallExpression",
"ExpressionStatement",
"BlockStatement",
"ArrowFunctionExpression",
"CallExpression",
// "AwaitExpression", if we want to check for `await waitFor` instead of just `waitFor`
],
[
"CallExpression",
"MemberExpression",
"CallExpression",
"ExpressionStatement",
"BlockStatement",
"ArrowFunctionExpression",
"CallExpression",
// "AwaitExpression",
],
[
"CallExpression",
"MemberExpression",
"CallExpression",
"ArrowFunctionExpression",
"CallExpression",
// "AwaitExpression",
],
[
"CallExpression",
"MemberExpression",
"MemberExpression",
"CallExpression",
"ArrowFunctionExpression",
"CallExpression",
// "AwaitExpression",
],
];

const create = context => {
const [{ testIds }] = context.options;
return {
CallExpression: node => {
if (!isExpect(node)) return;

const [call] = node.arguments;
if (!isCallExpression(call)) return;

const [literal] = call.arguments;
if (!isLiteral(literal)) return;

const { value } = literal;
if (!testIds.includes(value)) return;
/**
* Taking expect(...) call as the base
* Going up by any of the paths the found node should be `waitFor` expressions
* Report if it is not.
* */
const callExpression = findByPaths(WAIT_FOR_PATHS, node);
if (isWaitFor(callExpression)) return;

context.report({
node,
message:
"Checks for loading state should be wrapped in a `waitFor` block to prevent act warnings",
});
},
};
};

export { meta, create };
28 changes: 28 additions & 0 deletions src/utils/ast/astUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// types
const isType = (node, type) => node?.type === type;
const isCallExpression = node => isType(node, "CallExpression");
const isLiteral = node => isType(node, "Literal");
const isAwaitExpression = node => isType(node, "AwaitExpression");

// calee
const isCalleName = (node, name) => node?.callee?.name === name;
const isExpect = node => isCalleName(node, "expect");

// finder
const findNode = (path, tree) => {
if (!tree || !path) return null;
const current = path.shift();
if (!isType(tree, current)) return null;
else if (path.length === 0) return tree;
return findNode(path, tree.parent);
};

const findByPaths = (paths, tree) => {
for (const path of paths) {
const found = findNode([...path], tree);
if (found) return found;
}
return null;
};

export { isCallExpression, isLiteral, isAwaitExpression, isCalleName, isExpect, findByPaths };
1 change: 1 addition & 0 deletions src/utils/ast/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./astUtils";
1 change: 1 addition & 0 deletions src/utils/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./isTranslationSource";
export * from "./relativePathToFile";
export * from "./ast";

0 comments on commit a3ab48f

Please sign in to comment.