Skip to content

Commit 49c6eb3

Browse files
committed
feat(pencil): add option for unsafe chaining rule to check for custom cypress methods
1 parent b442c5c commit 49c6eb3

File tree

2 files changed

+120
-21
lines changed

2 files changed

+120
-21
lines changed

lib/rules/unsafe-to-chain-command.js

Lines changed: 94 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,123 @@
11
'use strict'
22

3+
const { basename } = require('path')
4+
5+
const NAME = basename(__dirname)
6+
const DESCRIPTION = 'Actions should be in the end of chains, not in the middle'
7+
8+
/**
9+
* Commands listed in the documentation with text: 'It is unsafe to chain further commands that rely on the subject after xxx.'
10+
* See {@link https://docs.cypress.io/guides/core-concepts/retry-ability#Actions-should-be-at-the-end-of-chains-not-the-middle Actions should be at the end of chains, not the middle}
11+
* for more information.
12+
*/
13+
const unsafeToChainActions = [
14+
'blur',
15+
'clear',
16+
'click',
17+
'check',
18+
'dblclick',
19+
'each',
20+
'focus',
21+
'rightclick',
22+
'screenshot',
23+
'scrollIntoView',
24+
'scrollTo',
25+
'select',
26+
'selectFile',
27+
'spread',
28+
'submit',
29+
'type',
30+
'trigger',
31+
'uncheck',
32+
'within',
33+
]
34+
35+
const getDefaultOptions = (schema, context) => {
36+
return {
37+
...Object.entries(schema.properties).reduce((acc, [key, value]) => {
38+
if (value.default === undefined) return acc
39+
40+
return {
41+
...acc,
42+
[key]: value.default,
43+
}
44+
}, {}),
45+
...context.options[0],
46+
}
47+
}
48+
49+
const schema = {
50+
title: NAME,
51+
description: DESCRIPTION,
52+
type: 'object',
53+
properties: {
54+
methods: {
55+
type: 'array',
56+
description:
57+
'An additional list of methods to check for unsafe chaining.',
58+
default: [],
59+
},
60+
},
61+
}
62+
63+
/** @type {import('eslint').Rule.RuleModule} */
364
module.exports = {
465
meta: {
566
docs: {
6-
description: 'Actions should be in the end of chains, not in the middle',
67+
description: DESCRIPTION,
768
category: 'Possible Errors',
869
recommended: true,
970
url: 'https://docs.cypress.io/guides/core-concepts/retry-ability#Actions-should-be-at-the-end-of-chains-not-the-middle',
1071
},
11-
schema: [],
72+
schema: [schema],
1273
messages: {
13-
unexpected: 'It is unsafe to chain further commands that rely on the subject after this command. It is best to split the chain, chaining again from `cy.` in a next command line.',
74+
unexpected:
75+
'It is unsafe to chain further commands that rely on the subject after this command. It is best to split the chain, chaining again from `cy.` in a next command line.',
1476
},
1577
},
1678
create (context) {
79+
const { methods } = getDefaultOptions(schema, context)
80+
1781
return {
1882
CallExpression (node) {
19-
if (isRootCypress(node) && isActionUnsafeToChain(node) && node.parent.type === 'MemberExpression') {
20-
context.report({ node, messageId: 'unexpected' })
83+
if (
84+
isRootCypress(node) &&
85+
isActionUnsafeToChain(node, methods) &&
86+
node.parent.type === 'MemberExpression'
87+
) {
88+
context.report({
89+
node,
90+
messageId: 'unexpected',
91+
})
2192
}
2293
},
2394
}
2495
},
2596
}
2697

2798
function isRootCypress (node) {
28-
while (node.type === 'CallExpression') {
29-
if (node.callee.type !== 'MemberExpression') return false
30-
31-
if (node.callee.object.type === 'Identifier' &&
32-
node.callee.object.name === 'cy') {
33-
return true
34-
}
99+
if (node.type !== 'CallExpression' || node.callee.type !== 'MemberExpression') return
35100

36-
node = node.callee.object
101+
if (
102+
node.callee.object.type === 'Identifier' &&
103+
node.callee.object.name === 'cy'
104+
) {
105+
return true
37106
}
38107

39-
return false
108+
return isRootCypress(node.callee.object)
40109
}
41110

42-
function isActionUnsafeToChain (node) {
43-
// commands listed in the documentation with text: 'It is unsafe to chain further commands that rely on the subject after xxx'
44-
const unsafeToChainActions = ['blur', 'clear', 'click', 'check', 'dblclick', 'each', 'focus', 'rightclick', 'screenshot', 'scrollIntoView', 'scrollTo', 'select', 'selectFile', 'spread', 'submit', 'type', 'trigger', 'uncheck', 'within']
111+
function isActionUnsafeToChain (node, additionalMethods) {
112+
const unsafeActionsRegex = new RegExp([
113+
...unsafeToChainActions,
114+
...additionalMethods.map((method) => method instanceof RegExp ? method.source : method),
115+
].join('|'))
45116

46-
return node.callee && node.callee.property && node.callee.property.type === 'Identifier' && unsafeToChainActions.includes(node.callee.property.name)
117+
return (
118+
node.callee &&
119+
node.callee.property &&
120+
node.callee.property.type === 'Identifier' &&
121+
unsafeActionsRegex.test(node.callee.property.name)
122+
)
47123
}

tests/lib/rules/unsafe-to-chain-command.js

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,34 @@ const parserOptions = { ecmaVersion: 6 }
1010

1111
ruleTester.run('action-ends-chain', rule, {
1212
valid: [
13-
{ code: 'cy.get("new-todo").type("todo A{enter}"); cy.get("new-todo").type("todo B{enter}"); cy.get("new-todo").should("have.class", "active");', parserOptions },
13+
{
14+
code: 'cy.get("new-todo").type("todo A{enter}"); cy.get("new-todo").type("todo B{enter}"); cy.get("new-todo").should("have.class", "active");',
15+
parserOptions,
16+
},
1417
],
1518

1619
invalid: [
17-
{ code: 'cy.get("new-todo").type("todo A{enter}").should("have.class", "active");', parserOptions, errors },
18-
{ code: 'cy.get("new-todo").type("todo A{enter}").type("todo B{enter}");', parserOptions, errors },
20+
{
21+
code: 'cy.get("new-todo").type("todo A{enter}").should("have.class", "active");',
22+
parserOptions,
23+
errors,
24+
},
25+
{
26+
code: 'cy.get("new-todo").type("todo A{enter}").type("todo B{enter}");',
27+
parserOptions,
28+
errors,
29+
},
30+
{
31+
code: 'cy.get("new-todo").customType("todo A{enter}").customClick();',
32+
parserOptions,
33+
errors,
34+
options: [{ methods: ['customType', 'customClick'] }],
35+
},
36+
{
37+
code: 'cy.get("new-todo").customPress("Enter").customScroll();',
38+
parserOptions,
39+
errors,
40+
options: [{ methods: [/customPress/, /customScroll/] }],
41+
},
1942
],
2043
})

0 commit comments

Comments
 (0)