Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new "maxNestedDescribe" rule #57

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,3 +270,52 @@ await page.locator('check').check();

await page.locator('input').fill('something');
```

### `max-nested-describe`

Enforces a maximum depth to nested `.describe()` calls. Useful for improving readability and parallelization of tests.

Uses a default max depth option of `{ "max": 5 }`.

Examples of **incorrect** code for this rule (using defaults):

```js
test.describe('level 1', () => {
test.describe('level 2', () => {
test.describe('level 3', () => {
test.describe('level 4', () => {
test.describe('level 5', () => {
test.describe('level 6', () => {
test('this test', async ({ page }) => {});
test('that test', async ({ page }) => {});
});
});
});
});
});
});
```

Examples of **correct** code for this rule (using defaults):

```js
test.describe('first level', () => {
test.describe('second level', () => {
test('this test', async ({ page }) => {});
test('that test', async ({ page }) => {});
});
});
```

#### Options

The rule accepts a non-required option to override the default maximum nested describe depth (5).

```json
{
"playwright/max-nested-describe": [
"error",
{ "max": 3 }
]
}
```
3 changes: 3 additions & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const noFocusedTest = require("./rules/no-focused-test");
const noSkippedTest = require("./rules/no-skipped-test");
const noWaitForTimeout = require("./rules/no-wait-for-timeout");
const noForceOption = require("./rules/no-force-option");
const maxNestedDescribe = require("./rules/max-nested-describe");

module.exports = {
configs: {
Expand All @@ -24,6 +25,7 @@ module.exports = {
"playwright/no-skipped-test": "warn",
"playwright/no-wait-for-timeout": "warn",
"playwright/no-force-option": "warn",
"playwright/max-nested-describe": "warn",
},
},
"jest-playwright": {
Expand Down Expand Up @@ -68,5 +70,6 @@ module.exports = {
"no-skipped-test": noSkippedTest,
"no-wait-for-timeout": noWaitForTimeout,
"no-force-option": noForceOption,
"max-nested-describe": maxNestedDescribe,
},
};
70 changes: 70 additions & 0 deletions lib/rules/max-nested-describe.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@

const { isCallExpression, isDescribeCall } = require('../utils/ast');

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
create(context) {
const { options } = context;
const defaultOptions = { max: 5 };
const { max } = options[0] || defaultOptions;
const describeCallbackStack = [];

function pushDescribeCallback(node) {
const { parent } = node;

if(!isCallExpression(parent) || !isDescribeCall(parent)) {
return;
}

describeCallbackStack.push(0);

if (describeCallbackStack.length > max) {
context.report({
node: parent,
messageId: 'exceededMaxDepth',
data: { depth: describeCallbackStack.length, max },
});
}
}

function popDescribeCallback(node) {
const { parent } = node;

if (isCallExpression(parent) && isDescribeCall(parent)) {
describeCallbackStack.pop();
}
}

return {
FunctionExpression: pushDescribeCallback,
'FunctionExpression:exit': popDescribeCallback,
ArrowFunctionExpression: pushDescribeCallback,
'ArrowFunctionExpression:exit': popDescribeCallback,
};
},
meta: {
docs: {
category: 'Best Practices',
description: 'Enforces a maximum depth to nested describe calls',
recommended: false,
url: 'https://github.com/playwright-community/eslint-plugin-playwright#max-nested-describe',
},
messages: {
exceededMaxDepth: 'Maximum describe call depth exceeded ({{ depth }}). Maximum allowed is {{ max }}.',
},
type: 'suggestion',
fixable: 'code',
schema: [
{
type: 'object',
properties: {
max: {
type: 'integer',
minimum: 0,
},
},
additionalProperties: false,
},
],
},
};
61 changes: 59 additions & 2 deletions lib/utils/ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ function isObjectProperty({ object }, name) {
);
}

function isStringLiteral(node) {
return node && node.type === 'Literal' && typeof node.value === 'string';
function isStringLiteral(node, value) {
return node && node.type === 'Literal'
&& typeof node.value === 'string'
&& (value === undefined || node.value === value);
}

function isBooleanLiteral(node) {
Expand All @@ -49,6 +51,58 @@ function isBinaryExpression(node) {
return node && node.type === 'BinaryExpression';
}

function isCallExpression(node) {
return node && node.type === 'CallExpression';
}

function isMemberExpression(node) {
return node && node.type === 'MemberExpression';
}

function isIdentifier(node, name) {
return node
&& node.type === 'Identifier'
&& (name === undefined || node.name === name);
}

function isDescribeAlias(node) {
return isIdentifier(node, 'describe');
}

function isDescribeProperty(node) {
const describeProperties = new Set(['parallel', 'serial', 'only', 'skip']);

return isIdentifier(node) && describeProperties.has(node.name);
}

function isDescribeCall(node) {
if (isDescribeAlias(node.callee)) {
return true;
}

const callee =
node.callee.type === 'TaggedTemplateExpression'
? node.callee.tag
: node.callee.type === 'CallExpression'
? node.callee.callee
: node.callee;

if(callee.type === 'MemberExpression' && isDescribeAlias(callee.property)) {
return true;
}

if (callee.type === 'MemberExpression' && isDescribeProperty(callee.property)) {

return callee.object.type === 'MemberExpression'
? callee.object.object.type === 'MemberExpression'
? isDescribeAlias(callee.object.object.property)
: isDescribeAlias(callee.object.property)
: (isDescribeAlias(callee.property) || isDescribeAlias(callee.object));
}

return false;
};

module.exports = {
isObject,
isCalleeProperty,
Expand All @@ -58,4 +112,7 @@ module.exports = {
isStringLiteral,
isBooleanLiteral,
isBinaryExpression,
isCallExpression,
isMemberExpression,
isDescribeCall
};
Loading