eslint-plugin-power-esrules 0.1.12 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -91,7 +91,8 @@ function getImportType(node) {
91
91
  spec.type === "ImportSpecifier" &&
92
92
  spec.imported &&
93
93
  /^[A-Z][a-zA-Z]*$/.test(spec.imported.name) && // Имя начинается с большой буквы и не все заглавные
94
- spec.imported.name !== spec.imported.name.toUpperCase(), // Не константа (не все заглавные)
94
+ spec.imported.name !== spec.imported.name.toUpperCase() && // Не константа (не все заглавные)
95
+ !spec.imported.name.endsWith("Context"), // не содержит ключевого слова Context
95
96
  );
96
97
  if (isDefaultImport || hasComponentName) {
97
98
  return "component";
@@ -0,0 +1,223 @@
1
+ // /**
2
+ // * Rule: require-data-testid
3
+ // *
4
+ // * Проверяет наличие data-testid атрибута у корневого контейнера JSX файла
5
+ // * и рекомендует запустить codemod скрипт для его добавления, если атрибут отсутствует.
6
+ // */
7
+
8
+ // const path = require("path");
9
+
10
+ // const packetName = "@hubex/power-linter";
11
+
12
+ // /**
13
+ // * Получает имя JSX элемента
14
+ // */
15
+ // function getJSXName(jsxName) {
16
+ // if (!jsxName) return null;
17
+ // switch (jsxName.type) {
18
+ // case "JSXIdentifier":
19
+ // return jsxName.name || null;
20
+ // case "JSXMemberExpression":
21
+ // // Берем правую часть: UI.Button -> Button
22
+ // return getJSXName(jsxName.property);
23
+ // case "JSXNamespacedName":
24
+ // return `${jsxName.namespace?.name || "ns"}:${
25
+ // jsxName.name?.name || "Name"
26
+ // }`;
27
+ // default:
28
+ // return null;
29
+ // }
30
+ // }
31
+
32
+ // /**
33
+ // * Проверяет, является ли узел верхнеуровневым JSX элементом
34
+ // * (не вложенным в другой JSX элемент или Fragment)
35
+ // */
36
+ // function isTopLevelJSX(node) {
37
+ // let current = node.parent;
38
+ // while (current) {
39
+ // const parentType = current.type;
40
+ // // Если родитель - JSX элемент или Fragment, значит это не верхнеуровневый элемент
41
+ // if (parentType === "JSXElement" || parentType === "JSXFragment") {
42
+ // return false;
43
+ // }
44
+ // // Если родитель - ReturnStatement, это возвращаемое значение компонента (верхнеуровневый)
45
+ // if (parentType === "ReturnStatement") {
46
+ // return true;
47
+ // }
48
+ // // Если родитель - ExportDefaultDeclaration или ExportNamedDeclaration,
49
+ // // это может быть верхнеуровневый JSX
50
+ // if (
51
+ // parentType === "ExportDefaultDeclaration" ||
52
+ // parentType === "ExportNamedDeclaration"
53
+ // ) {
54
+ // return true;
55
+ // }
56
+ // current = current.parent;
57
+ // }
58
+ // return false;
59
+ // }
60
+
61
+ // /**
62
+ // * Спускается от Fragment к первому рендеримому JSXElement
63
+ // */
64
+ // function descendToFirstRenderable(node) {
65
+ // if (!node) return null;
66
+
67
+ // // 1) Если это JSXFragment — обходим детей
68
+ // if (node.type === "JSXFragment") {
69
+ // for (const child of node.children || []) {
70
+ // if (child.type === "JSXElement") {
71
+ // const resolved = descendToFirstRenderable(child);
72
+ // if (resolved) return resolved;
73
+ // }
74
+ // }
75
+ // return null;
76
+ // }
77
+
78
+ // // 2) Если это JSXElement с именем Fragment
79
+ // if (node.type === "JSXElement") {
80
+ // const name = getJSXName(node.openingElement?.name);
81
+ // if (name === "Fragment") {
82
+ // for (const child of node.children || []) {
83
+ // if (child.type === "JSXElement") {
84
+ // const resolved = descendToFirstRenderable(child);
85
+ // if (resolved) return resolved;
86
+ // }
87
+ // }
88
+ // return null;
89
+ // }
90
+ // // Иначе это реальный рендеримый элемент
91
+ // return node;
92
+ // }
93
+
94
+ // return null;
95
+ // }
96
+
97
+ // /**
98
+ // * Проверяет наличие data-testid или dataTestID атрибута
99
+ // */
100
+ // function hasDataTestId(openingElement) {
101
+ // if (!openingElement || !openingElement.attributes) {
102
+ // return false;
103
+ // }
104
+ // return openingElement.attributes.some((attr) => {
105
+ // if (!attr || attr.type !== "JSXAttribute" || !attr.name) {
106
+ // return false;
107
+ // }
108
+ // const attrName = attr.name.name;
109
+ // return attrName === "data-testid" || attrName === "dataTestID";
110
+ // });
111
+ // }
112
+
113
+ // /**
114
+ // * Получает относительный путь к файлу от корня проекта
115
+ // */
116
+ // function getRelativeFilePath(context) {
117
+ // const filename = context.getFilename();
118
+ // const workspaceRoot = context.getCwd ? context.getCwd() : process.cwd();
119
+ // return path.relative(workspaceRoot, filename);
120
+ // }
121
+
122
+ // module.exports = {
123
+ // meta: {
124
+ // type: "suggestion",
125
+ // docs: {
126
+ // description:
127
+ // "Проверяет наличие data-testid атрибута у корневого контейнера JSX файла и рекомендует запустить codemod",
128
+ // category: "Best Practices",
129
+ // recommended: false,
130
+ // },
131
+ // fixable: null,
132
+ // schema: [],
133
+ // messages: {
134
+ // missingDataTestId:
135
+ // "Корневой контейнер JSX не содержит атрибут data-testid. " +
136
+ // `Запустите: node node_modules/${packetName}/scripts/addDataTestId/run-codemod.js {{filePath}}` +
137
+ // ` или node node_modules/${packetName}/scripts/addDataTestId/run-codemod.js src для всего проекта`,
138
+ // },
139
+ // },
140
+
141
+ // create(context) {
142
+ // let hasReported = false;
143
+ // let rootJSXNode = null;
144
+
145
+ // return {
146
+ // ReturnStatement(node) {
147
+ // // Проверяем только один раз на файл
148
+ // if (hasReported || rootJSXNode) {
149
+ // return;
150
+ // }
151
+
152
+ // const returnArgument = node.argument;
153
+ // if (!returnArgument) {
154
+ // return;
155
+ // }
156
+
157
+ // // Если возвращается JSX элемент или Fragment
158
+ // if (
159
+ // returnArgument.type === "JSXElement" ||
160
+ // returnArgument.type === "JSXFragment"
161
+ // ) {
162
+ // // Проверяем, что это не вложенный JSX (не имеет JSX родителя)
163
+ // if (isTopLevelJSX(returnArgument)) {
164
+ // rootJSXNode = returnArgument;
165
+ // }
166
+ // }
167
+ // },
168
+
169
+ // JSXElement(node) {
170
+ // // Проверяем только один раз на файл
171
+ // if (hasReported || rootJSXNode) {
172
+ // return;
173
+ // }
174
+
175
+ // // Проверяем верхнеуровневые JSX элементы вне ReturnStatement
176
+ // // (например, экспортируемые напрямую)
177
+ // if (isTopLevelJSX(node)) {
178
+ // rootJSXNode = node;
179
+ // }
180
+ // },
181
+
182
+ // JSXFragment(node) {
183
+ // // Проверяем только один раз на файл
184
+ // if (hasReported || rootJSXNode) {
185
+ // return;
186
+ // }
187
+
188
+ // // Проверяем верхнеуровневые Fragment вне ReturnStatement
189
+ // if (isTopLevelJSX(node)) {
190
+ // rootJSXNode = node;
191
+ // }
192
+ // },
193
+
194
+ // "Program:exit"() {
195
+ // // После обхода всего файла проверяем корневой JSX элемент
196
+ // if (hasReported || !rootJSXNode) {
197
+ // return;
198
+ // }
199
+
200
+ // // Если это Fragment, спускаемся к первому рендеримому элементу
201
+ // const rootElement = descendToFirstRenderable(rootJSXNode);
202
+ // const elementToCheck = rootElement || rootJSXNode;
203
+
204
+ // if (!elementToCheck.openingElement) {
205
+ // return;
206
+ // }
207
+
208
+ // // Проверяем наличие data-testid
209
+ // if (!hasDataTestId(elementToCheck.openingElement)) {
210
+ // hasReported = true;
211
+ // const filePath = getRelativeFilePath(context);
212
+ // context.report({
213
+ // node: elementToCheck.openingElement,
214
+ // messageId: "missingDataTestId",
215
+ // data: {
216
+ // filePath,
217
+ // },
218
+ // });
219
+ // }
220
+ // },
221
+ // };
222
+ // },
223
+ // };
@@ -1,24 +1,20 @@
1
1
  /**
2
2
  * Rule: require-data-testid
3
3
  *
4
- * Проверяет наличие data-testid атрибута у корневого контейнера JSX файла
4
+ * Проверяет наличие data-testid атрибута у корневого JSX элемента React-компонента
5
5
  * и рекомендует запустить codemod скрипт для его добавления, если атрибут отсутствует.
6
6
  */
7
7
 
8
8
  const path = require("path");
9
-
10
9
  const packetName = "@hubex/power-linter";
11
10
 
12
- /**
13
- * Получает имя JSX элемента
14
- */
11
+ /** Получает имя JSX элемента */
15
12
  function getJSXName(jsxName) {
16
13
  if (!jsxName) return null;
17
14
  switch (jsxName.type) {
18
15
  case "JSXIdentifier":
19
16
  return jsxName.name || null;
20
17
  case "JSXMemberExpression":
21
- // Берем правую часть: UI.Button -> Button
22
18
  return getJSXName(jsxName.property);
23
19
  case "JSXNamespacedName":
24
20
  return `${jsxName.namespace?.name || "ns"}:${
@@ -29,42 +25,81 @@ function getJSXName(jsxName) {
29
25
  }
30
26
  }
31
27
 
32
- /**
33
- * Проверяет, является ли узел верхнеуровневым JSX элементом
34
- * (не вложенным в другой JSX элемент или Fragment)
35
- */
36
- function isTopLevelJSX(node) {
37
- let current = node.parent;
38
- while (current) {
39
- const parentType = current.type;
40
- // Если родитель - JSX элемент или Fragment, значит это не верхнеуровневый элемент
41
- if (parentType === "JSXElement" || parentType === "JSXFragment") {
42
- return false;
43
- }
44
- // Если родитель - ReturnStatement, это возвращаемое значение компонента (верхнеуровневый)
45
- if (parentType === "ReturnStatement") {
46
- return true;
47
- }
48
- // Если родитель - ExportDefaultDeclaration или ExportNamedDeclaration,
49
- // это может быть верхнеуровневый JSX
28
+ /** Проверка PascalCase */
29
+ function isPascalCase(name) {
30
+ return /^[A-Z]/.test(name);
31
+ }
32
+
33
+ /** Находит имя компонента, учитывая HOC (memo, forwardRef, observer) */
34
+ function getComponentName(node) {
35
+ if (!node) return null;
36
+
37
+ // Функция объявлена напрямую
38
+ if (node.type === "FunctionDeclaration") {
39
+ return node.id?.name ?? null;
40
+ }
41
+
42
+ // Переменная с функцией
43
+ if (
44
+ (node.type === "ArrowFunctionExpression" ||
45
+ node.type === "FunctionExpression") &&
46
+ node.parent?.type === "VariableDeclarator"
47
+ ) {
48
+ return node.parent.id?.name ?? null;
49
+ }
50
+
51
+ // HOC обёртка: memo(() => {}) или forwardRef(() => {}) или observer(() => {})
52
+ if (node.type === "CallExpression" && node.arguments.length > 0) {
53
+ return getComponentName(node.arguments[0]);
54
+ }
55
+
56
+ return null;
57
+ }
58
+
59
+ /** Проверяет, является ли функция React-компонентом */
60
+ function isReactComponent(node) {
61
+ const name = getComponentName(node);
62
+ return Boolean(name && isPascalCase(name));
63
+ }
64
+
65
+ /** Находит корневой JSX элемент компонента, поддержка HOC */
66
+ function getRootJSXFromComponent(fnNode) {
67
+ if (!fnNode) return null;
68
+
69
+ // Разворачиваем HOC
70
+ if (fnNode.type === "CallExpression" && fnNode.arguments.length > 0) {
71
+ return getRootJSXFromComponent(fnNode.arguments[0]);
72
+ }
73
+
74
+ // Arrow function без блока: const X = () => <Page />
75
+ if (
76
+ fnNode.body?.type === "JSXElement" ||
77
+ fnNode.body?.type === "JSXFragment"
78
+ ) {
79
+ return fnNode.body;
80
+ }
81
+
82
+ // Function с блоком
83
+ if (fnNode.body?.type !== "BlockStatement") return null;
84
+
85
+ for (const stmt of fnNode.body.body) {
50
86
  if (
51
- parentType === "ExportDefaultDeclaration" ||
52
- parentType === "ExportNamedDeclaration"
87
+ stmt.type === "ReturnStatement" &&
88
+ stmt.argument &&
89
+ (stmt.argument.type === "JSXElement" ||
90
+ stmt.argument.type === "JSXFragment")
53
91
  ) {
54
- return true;
92
+ return stmt.argument;
55
93
  }
56
- current = current.parent;
57
94
  }
58
- return false;
95
+
96
+ return null;
59
97
  }
60
98
 
61
- /**
62
- * Спускается от Fragment к первому рендеримому JSXElement
63
- */
99
+ /** Рекурсивно спускается через Fragment к первому рендеримому JSXElement */
64
100
  function descendToFirstRenderable(node) {
65
101
  if (!node) return null;
66
102
 
67
- // 1) Если это JSXFragment — обходим детей
68
103
  if (node.type === "JSXFragment") {
69
104
  for (const child of node.children || []) {
70
105
  if (child.type === "JSXElement") {
@@ -75,7 +110,6 @@ function descendToFirstRenderable(node) {
75
110
  return null;
76
111
  }
77
112
 
78
- // 2) Если это JSXElement с именем Fragment
79
113
  if (node.type === "JSXElement") {
80
114
  const name = getJSXName(node.openingElement?.name);
81
115
  if (name === "Fragment") {
@@ -87,32 +121,24 @@ function descendToFirstRenderable(node) {
87
121
  }
88
122
  return null;
89
123
  }
90
- // Иначе это реальный рендеримый элемент
91
124
  return node;
92
125
  }
93
126
 
94
127
  return null;
95
128
  }
96
129
 
97
- /**
98
- * Проверяет наличие data-testid или dataTestID атрибута
99
- */
130
+ /** Проверяет наличие data-testid или dataTestID */
100
131
  function hasDataTestId(openingElement) {
101
- if (!openingElement || !openingElement.attributes) {
102
- return false;
103
- }
132
+ if (!openingElement || !openingElement.attributes) return false;
133
+
104
134
  return openingElement.attributes.some((attr) => {
105
- if (!attr || attr.type !== "JSXAttribute" || !attr.name) {
106
- return false;
107
- }
135
+ if (!attr || attr.type !== "JSXAttribute" || !attr.name) return false;
108
136
  const attrName = attr.name.name;
109
137
  return attrName === "data-testid" || attrName === "dataTestID";
110
138
  });
111
139
  }
112
140
 
113
- /**
114
- * Получает относительный путь к файлу от корня проекта
115
- */
141
+ /** Получает относительный путь к файлу */
116
142
  function getRelativeFilePath(context) {
117
143
  const filename = context.getFilename();
118
144
  const workspaceRoot = context.getCwd ? context.getCwd() : process.cwd();
@@ -124,7 +150,7 @@ module.exports = {
124
150
  type: "suggestion",
125
151
  docs: {
126
152
  description:
127
- "Проверяет наличие data-testid атрибута у корневого контейнера JSX файла и рекомендует запустить codemod",
153
+ "Проверяет наличие data-testid атрибута у корневого JSX элемента React-компонента",
128
154
  category: "Best Practices",
129
155
  recommended: false,
130
156
  },
@@ -139,83 +165,37 @@ module.exports = {
139
165
  },
140
166
 
141
167
  create(context) {
142
- let hasReported = false;
143
- let rootJSXNode = null;
144
-
145
- return {
146
- ReturnStatement(node) {
147
- // Проверяем только один раз на файл
148
- if (hasReported || rootJSXNode) {
149
- return;
150
- }
151
-
152
- const returnArgument = node.argument;
153
- if (!returnArgument) {
154
- return;
155
- }
156
-
157
- // Если возвращается JSX элемент или Fragment
158
- if (
159
- returnArgument.type === "JSXElement" ||
160
- returnArgument.type === "JSXFragment"
161
- ) {
162
- // Проверяем, что это не вложенный JSX (не имеет JSX родителя)
163
- if (isTopLevelJSX(returnArgument)) {
164
- rootJSXNode = returnArgument;
165
- }
166
- }
167
- },
168
-
169
- JSXElement(node) {
170
- // Проверяем только один раз на файл
171
- if (hasReported || rootJSXNode) {
172
- return;
173
- }
174
-
175
- // Проверяем верхнеуровневые JSX элементы вне ReturnStatement
176
- // (например, экспортируемые напрямую)
177
- if (isTopLevelJSX(node)) {
178
- rootJSXNode = node;
179
- }
180
- },
168
+ const components = new Set();
181
169
 
182
- JSXFragment(node) {
183
- // Проверяем только один раз на файл
184
- if (hasReported || rootJSXNode) {
185
- return;
186
- }
170
+ function collectComponent(node) {
171
+ if (isReactComponent(node)) {
172
+ components.add(node);
173
+ }
174
+ }
187
175
 
188
- // Проверяем верхнеуровневые Fragment вне ReturnStatement
189
- if (isTopLevelJSX(node)) {
190
- rootJSXNode = node;
191
- }
192
- },
176
+ return {
177
+ FunctionDeclaration: collectComponent,
178
+ FunctionExpression: collectComponent,
179
+ ArrowFunctionExpression: collectComponent,
180
+ // CallExpression больше не нужен, HOC разворачиваются внутри getComponentName
181
+ //"CallExpression": collectComponent,
193
182
 
194
183
  "Program:exit"() {
195
- // После обхода всего файла проверяем корневой JSX элемент
196
- if (hasReported || !rootJSXNode) {
197
- return;
198
- }
199
-
200
- // Если это Fragment, спускаемся к первому рендеримому элементу
201
- const rootElement = descendToFirstRenderable(rootJSXNode);
202
- const elementToCheck = rootElement || rootJSXNode;
203
-
204
- if (!elementToCheck.openingElement) {
205
- return;
206
- }
207
-
208
- // Проверяем наличие data-testid
209
- if (!hasDataTestId(elementToCheck.openingElement)) {
210
- hasReported = true;
211
- const filePath = getRelativeFilePath(context);
212
- context.report({
213
- node: elementToCheck.openingElement,
214
- messageId: "missingDataTestId",
215
- data: {
216
- filePath,
217
- },
218
- });
184
+ for (const component of components) {
185
+ const rootJSX = getRootJSXFromComponent(component);
186
+ if (!rootJSX) continue;
187
+
188
+ const elementToCheck = descendToFirstRenderable(rootJSX) || rootJSX;
189
+ if (!elementToCheck?.openingElement) continue;
190
+
191
+ if (!hasDataTestId(elementToCheck.openingElement)) {
192
+ const filePath = getRelativeFilePath(context);
193
+ context.report({
194
+ node: elementToCheck.openingElement,
195
+ messageId: "missingDataTestId",
196
+ data: { filePath },
197
+ });
198
+ }
219
199
  }
220
200
  },
221
201
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-power-esrules",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "custom ESLint rules",
5
5
  "main": "index.js",
6
6
  "peerDependencies": {
@@ -17,7 +17,7 @@
17
17
  "lib": "lib"
18
18
  },
19
19
  "scripts": {
20
- "test": "echo \"Error: no test specified\" && exit 1"
20
+ "test": "jest"
21
21
  },
22
22
  "repository": {
23
23
  "type": "git",
@@ -29,6 +29,7 @@
29
29
  },
30
30
  "homepage": "https://github.com/Vollmond148259/eslint-plugin-power-esrules#readme",
31
31
  "devDependencies": {
32
- "eslint": "^8.57.1"
32
+ "eslint": "^8.57.1",
33
+ "jest": "^29.3.1"
33
34
  }
34
35
  }
@@ -11,42 +11,42 @@ const ruleTester = new RuleTester({
11
11
 
12
12
  ruleTester.run("props-destructuring-sort", rule, {
13
13
  valid: [
14
- {
15
- name: "sorted destructuring from props in functional component",
16
- code: `
14
+ {
15
+ name: "sorted destructuring from props in functional component",
16
+ code: `
17
17
  function MyComponent(props) {
18
18
  const { alpha, beta } = props;
19
19
  return <div>{alpha}{beta}</div>;
20
20
  }
21
21
  `,
22
- },
23
- {
24
- name: "single property from props",
25
- code: `
22
+ },
23
+ {
24
+ name: "single property from props",
25
+ code: `
26
26
  function MyComponent(props) {
27
27
  const { only } = props;
28
28
  return <div>{only}</div>;
29
29
  }
30
30
  `,
31
- },
32
- {
33
- name: "unsorted props outside React component",
34
- code: `
31
+ },
32
+ {
33
+ name: "unsorted props outside React component",
34
+ code: `
35
35
  function util(props) {
36
36
  const { z, a } = props;
37
37
  return z + a;
38
38
  }
39
39
  `,
40
- },
41
- {
42
- name: "sorted params in arrow component",
43
- code: `
40
+ },
41
+ {
42
+ name: "sorted params in arrow component",
43
+ code: `
44
44
  const MyComponent = ({ alpha, beta }) => <div>{alpha}{beta}</div>;
45
45
  `,
46
- },
47
- {
48
- name: "sorted this.props in class component",
49
- code: `
46
+ },
47
+ {
48
+ name: "sorted this.props in class component",
49
+ code: `
50
50
  class MyComponent extends React.Component {
51
51
  render() {
52
52
  const { alpha, beta } = this.props;
@@ -54,62 +54,62 @@ ruleTester.run("props-destructuring-sort", rule, {
54
54
  }
55
55
  }
56
56
  `,
57
- },
58
- {
59
- name: "rest element last when already sorted",
60
- code: `
57
+ },
58
+ {
59
+ name: "rest element last when already sorted",
60
+ code: `
61
61
  function MyComponent(props) {
62
62
  const { alpha, beta, ...rest } = props;
63
63
  return <div>{alpha}</div>;
64
64
  }
65
65
  `,
66
- },
66
+ },
67
67
  ],
68
68
 
69
69
  invalid: [
70
- {
71
- name: "const from props in functional component",
72
- code: `
70
+ {
71
+ name: "const from props in functional component",
72
+ code: `
73
73
  function MyComponent(props) {
74
74
  const { zebra, alpha } = props;
75
75
  return <div>{zebra}{alpha}</div>;
76
76
  }
77
77
  `,
78
- output: `
78
+ output: `
79
79
  function MyComponent(props) {
80
80
  const { alpha, zebra } = props;
81
81
  return <div>{zebra}{alpha}</div>;
82
82
  }
83
83
  `,
84
- errors: [{ messageId: "incorrectOrder" }],
85
- },
86
- {
87
- name: "destructured params in function declaration",
88
- code: `
84
+ errors: [{ messageId: "incorrectOrder" }],
85
+ },
86
+ {
87
+ name: "destructured params in function declaration",
88
+ code: `
89
89
  function MyComponent({ zebra, alpha }) {
90
90
  return <div>{zebra}{alpha}</div>;
91
91
  }
92
92
  `,
93
- output: `
93
+ output: `
94
94
  function MyComponent({ alpha, zebra }) {
95
95
  return <div>{zebra}{alpha}</div>;
96
96
  }
97
97
  `,
98
- errors: [{ messageId: "incorrectOrder" }],
99
- },
100
- {
101
- name: "destructured params in arrow component with JSX",
102
- code: `
98
+ errors: [{ messageId: "incorrectOrder" }],
99
+ },
100
+ {
101
+ name: "destructured params in arrow component with JSX",
102
+ code: `
103
103
  const MyComponent = ({ zebra, alpha }) => <div>{zebra}{alpha}</div>;
104
104
  `,
105
- output: `
105
+ output: `
106
106
  const MyComponent = ({ alpha, zebra }) => <div>{zebra}{alpha}</div>;
107
107
  `,
108
- errors: [{ messageId: "incorrectOrder" }],
109
- },
110
- {
111
- name: "this.props in class component",
112
- code: `
108
+ errors: [{ messageId: "incorrectOrder" }],
109
+ },
110
+ {
111
+ name: "this.props in class component",
112
+ code: `
113
113
  class MyComponent extends Component {
114
114
  render() {
115
115
  const { zebra, alpha } = this.props;
@@ -117,7 +117,7 @@ ruleTester.run("props-destructuring-sort", rule, {
117
117
  }
118
118
  }
119
119
  `,
120
- output: `
120
+ output: `
121
121
  class MyComponent extends Component {
122
122
  render() {
123
123
  const { alpha, zebra } = this.props;
@@ -125,33 +125,35 @@ ruleTester.run("props-destructuring-sort", rule, {
125
125
  }
126
126
  }
127
127
  `,
128
- errors: [{ messageId: "incorrectOrder" }],
129
- },
130
- {
131
- name: "rest stays last after sort",
132
- code: `
128
+ errors: [{ messageId: "incorrectOrder" }],
129
+ },
130
+ {
131
+ name: "rest stays last after sort",
132
+ code: `
133
133
  function MyComponent(props) {
134
134
  const { zebra, alpha, ...rest } = props;
135
135
  return <div>{zebra}</div>;
136
136
  }
137
137
  `,
138
- output: `
138
+ output: `
139
139
  function MyComponent(props) {
140
140
  const { alpha, zebra, ...rest } = props;
141
141
  return <div>{zebra}</div>;
142
142
  }
143
143
  `,
144
- errors: [{ messageId: "incorrectOrder" }],
145
- },
146
- {
147
- name: "memo-wrapped component params",
148
- code: `
144
+ errors: [{ messageId: "incorrectOrder" }],
145
+ },
146
+ {
147
+ name: "memo-wrapped component params",
148
+ code: `
149
149
  const MyComponent = memo(({ zebra, alpha }) => <div>{zebra}</div>);
150
150
  `,
151
- output: `
151
+ output: `
152
152
  const MyComponent = memo(({ alpha, zebra }) => <div>{zebra}</div>);
153
153
  `,
154
- errors: [{ messageId: "incorrectOrder" }],
155
- },
154
+ errors: [{ messageId: "incorrectOrder" }],
155
+ },
156
156
  ],
157
157
  });
158
+
159
+ console.log("OK");
@@ -0,0 +1,112 @@
1
+ const { RuleTester } = require("eslint");
2
+ const rule = require("../lib/rules/require-data-testid");
3
+
4
+ const ruleTester = new RuleTester({
5
+ parserOptions: {
6
+ ecmaVersion: 2022,
7
+ sourceType: "module",
8
+ ecmaFeatures: { jsx: true },
9
+ },
10
+ });
11
+
12
+ ruleTester.run("require-data-testid", rule, {
13
+ valid: [
14
+ {
15
+ name: "function declaration with data-testid",
16
+ code: `
17
+ function MyComponent() {
18
+ return <div data-testid="my-component">Hello</div>;
19
+ }
20
+ `,
21
+ },
22
+ {
23
+ name: "arrow component with dataTestID",
24
+ code: `
25
+ const MyComponent = () => <section dataTestID="section">Hi</section>;
26
+ `,
27
+ },
28
+ {
29
+ name: "memo-wrapped component with data-testid",
30
+ code: `
31
+ const MyComponent = memo(() => <div data-testid="memo">Memo</div>);
32
+ `,
33
+ },
34
+ {
35
+ name: "forwardRef component with data-testid",
36
+ code: `
37
+ const MyComponent = forwardRef((props, ref) => <div ref={ref} data-testid="ref">Ref</div>);
38
+ `,
39
+ },
40
+ {
41
+ name: "nested JSX inside Fragment",
42
+ code: `
43
+ const MyComponent = () => (
44
+ <>
45
+ <div data-testid="inside">Hello</div>
46
+ </>
47
+ );
48
+ `,
49
+ },
50
+ {
51
+ name: "non-PascalCase function is ignored",
52
+ code: `
53
+ function utilFunction() {
54
+ return <div>Hello</div>;
55
+ }
56
+ `,
57
+ },
58
+ ],
59
+
60
+ invalid: [
61
+ {
62
+ name: "function declaration without data-testid",
63
+ code: `
64
+ function MyComponent() {
65
+ return <div>Hello</div>;
66
+ }
67
+ `,
68
+ errors: [{ messageId: "missingDataTestId" }],
69
+ },
70
+ {
71
+ name: "arrow component missing data-testid",
72
+ code: `
73
+ const MyComponent = () => <section>Hi</section>;
74
+ `,
75
+ errors: [{ messageId: "missingDataTestId" }],
76
+ },
77
+ {
78
+ name: "memo-wrapped component missing data-testid",
79
+ code: `
80
+ const MyComponent = memo(() => <div>Memo</div>);
81
+ `,
82
+ errors: [{ messageId: "missingDataTestId" }],
83
+ },
84
+ {
85
+ name: "forwardRef component missing data-testid",
86
+ code: `
87
+ const MyComponent = forwardRef((props, ref) => <div ref={ref}>Ref</div>);
88
+ `,
89
+ errors: [{ messageId: "missingDataTestId" }],
90
+ },
91
+ {
92
+ name: "nested JSX in Fragment missing data-testid",
93
+ code: `
94
+ const MyComponent = () => (
95
+ <>
96
+ <div>Hello</div>
97
+ </>
98
+ );
99
+ `,
100
+ errors: [{ messageId: "missingDataTestId" }],
101
+ },
102
+ {
103
+ name: "HOC wrapped component without data-testid",
104
+ code: `
105
+ const MyComponent = observer(() => <div>Hello</div>);
106
+ `,
107
+ errors: [{ messageId: "missingDataTestId" }],
108
+ },
109
+ ],
110
+ });
111
+
112
+ console.log("OK");