@scenetest/eslint-plugin 0.1.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Michael Snook
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=prefer-aria-label.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefer-aria-label.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/prefer-aria-label.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,197 @@
1
+ import { describe } from 'vitest';
2
+ import { RuleTester } from 'eslint';
3
+ import rule from '../rules/prefer-aria-label.js';
4
+ const ruleTester = new RuleTester({
5
+ languageOptions: {
6
+ ecmaVersion: 2022,
7
+ sourceType: 'module',
8
+ parserOptions: {
9
+ ecmaFeatures: { jsx: true },
10
+ },
11
+ },
12
+ });
13
+ /** Helper: build the two suggestions that every string-valued data-testid produces */
14
+ function suggestions(value, code) {
15
+ return [
16
+ {
17
+ messageId: 'addAriaLabel',
18
+ data: { value },
19
+ output: code.replace(`data-testid="${value}"`, `data-testid="${value}" aria-label="${value}"`),
20
+ },
21
+ {
22
+ messageId: 'replaceWithAriaLabel',
23
+ data: { value },
24
+ output: code.replace(`data-testid="${value}"`, `aria-label="${value}"`),
25
+ },
26
+ ];
27
+ }
28
+ describe('prefer-aria-label', () => {
29
+ ruleTester.run('prefer-aria-label', rule, {
30
+ valid: [
31
+ // No data-testid at all
32
+ '<button>Click me</button>',
33
+ // data-testid on non-interactive element (div)
34
+ '<div data-testid="wrapper">content</div>',
35
+ // data-testid on non-interactive element (span)
36
+ '<span data-testid="label">text</span>',
37
+ // Interactive element with both data-testid and aria-label
38
+ '<button data-testid="submit" aria-label="Submit form">Go</button>',
39
+ // Interactive element with aria-labelledby (also acceptable)
40
+ '<button data-testid="submit" aria-labelledby="label-id">Go</button>',
41
+ // Input with aria-label already present
42
+ '<input data-testid="email" aria-label="Email address" />',
43
+ // Link with aria-label
44
+ '<a data-testid="home-link" aria-label="Home" href="/">Home</a>',
45
+ // Non-interactive element with onClick but has aria-label
46
+ '<div data-testid="card" onClick={handleClick} aria-label="Open card">content</div>',
47
+ // Element with role but has aria-label
48
+ '<div role="button" data-testid="toggle" aria-label="Toggle menu">☰</div>',
49
+ ],
50
+ invalid: [
51
+ // Button with data-testid, no aria-label
52
+ {
53
+ code: '<button data-testid="submit-btn">Submit</button>',
54
+ errors: [
55
+ {
56
+ messageId: 'preferAriaLabel',
57
+ data: { element: '<button>' },
58
+ suggestions: suggestions('submit-btn', '<button data-testid="submit-btn">Submit</button>'),
59
+ },
60
+ ],
61
+ },
62
+ // Link with data-testid, no aria-label
63
+ {
64
+ code: '<a data-testid="nav-home" href="/">Home</a>',
65
+ errors: [
66
+ {
67
+ messageId: 'preferAriaLabel',
68
+ data: { element: '<a>' },
69
+ suggestions: suggestions('nav-home', '<a data-testid="nav-home" href="/">Home</a>'),
70
+ },
71
+ ],
72
+ },
73
+ // Input with data-testid, no aria-label
74
+ {
75
+ code: '<input data-testid="email-input" type="email" />',
76
+ errors: [
77
+ {
78
+ messageId: 'preferAriaLabel',
79
+ data: { element: '<input>' },
80
+ suggestions: suggestions('email-input', '<input data-testid="email-input" type="email" />'),
81
+ },
82
+ ],
83
+ },
84
+ // Select with data-testid
85
+ {
86
+ code: '<select data-testid="country-select"><option>US</option></select>',
87
+ errors: [
88
+ {
89
+ messageId: 'preferAriaLabel',
90
+ data: { element: '<select>' },
91
+ suggestions: suggestions('country-select', '<select data-testid="country-select"><option>US</option></select>'),
92
+ },
93
+ ],
94
+ },
95
+ // Textarea with data-testid
96
+ {
97
+ code: '<textarea data-testid="comment-box" />',
98
+ errors: [
99
+ {
100
+ messageId: 'preferAriaLabel',
101
+ data: { element: '<textarea>' },
102
+ suggestions: suggestions('comment-box', '<textarea data-testid="comment-box" />'),
103
+ },
104
+ ],
105
+ },
106
+ // Div with onClick (interactive via handler)
107
+ {
108
+ code: '<div data-testid="clickable-card" onClick={handleClick}>content</div>',
109
+ errors: [
110
+ {
111
+ messageId: 'preferAriaLabel',
112
+ data: { element: '<div>' },
113
+ suggestions: suggestions('clickable-card', '<div data-testid="clickable-card" onClick={handleClick}>content</div>'),
114
+ },
115
+ ],
116
+ },
117
+ // Div with role="button" (interactive via role)
118
+ {
119
+ code: '<div role="button" data-testid="toggle">☰</div>',
120
+ errors: [
121
+ {
122
+ messageId: 'preferAriaLabel',
123
+ data: { element: '<div>' },
124
+ suggestions: suggestions('toggle', '<div role="button" data-testid="toggle">☰</div>'),
125
+ },
126
+ ],
127
+ },
128
+ // Span with role="link"
129
+ {
130
+ code: '<span role="link" data-testid="fake-link">click here</span>',
131
+ errors: [
132
+ {
133
+ messageId: 'preferAriaLabel',
134
+ data: { element: '<span>' },
135
+ suggestions: suggestions('fake-link', '<span role="link" data-testid="fake-link">click here</span>'),
136
+ },
137
+ ],
138
+ },
139
+ // Div with role="tab"
140
+ {
141
+ code: '<div role="tab" data-testid="settings-tab">Settings</div>',
142
+ errors: [
143
+ {
144
+ messageId: 'preferAriaLabel',
145
+ data: { element: '<div>' },
146
+ suggestions: suggestions('settings-tab', '<div role="tab" data-testid="settings-tab">Settings</div>'),
147
+ },
148
+ ],
149
+ },
150
+ // Element with onChange handler
151
+ {
152
+ code: '<div data-testid="toggle" onChange={handleChange}>toggle</div>',
153
+ errors: [
154
+ {
155
+ messageId: 'preferAriaLabel',
156
+ data: { element: '<div>' },
157
+ suggestions: suggestions('toggle', '<div data-testid="toggle" onChange={handleChange}>toggle</div>'),
158
+ },
159
+ ],
160
+ },
161
+ // Summary element (interactive - toggles details)
162
+ {
163
+ code: '<summary data-testid="faq-toggle">FAQ</summary>',
164
+ errors: [
165
+ {
166
+ messageId: 'preferAriaLabel',
167
+ data: { element: '<summary>' },
168
+ suggestions: suggestions('faq-toggle', '<summary data-testid="faq-toggle">FAQ</summary>'),
169
+ },
170
+ ],
171
+ },
172
+ // Element with role="checkbox"
173
+ {
174
+ code: '<div role="checkbox" data-testid="agree-checkbox">I agree</div>',
175
+ errors: [
176
+ {
177
+ messageId: 'preferAriaLabel',
178
+ data: { element: '<div>' },
179
+ suggestions: suggestions('agree-checkbox', '<div role="checkbox" data-testid="agree-checkbox">I agree</div>'),
180
+ },
181
+ ],
182
+ },
183
+ // Element with role="switch"
184
+ {
185
+ code: '<div role="switch" data-testid="dark-mode">Dark mode</div>',
186
+ errors: [
187
+ {
188
+ messageId: 'preferAriaLabel',
189
+ data: { element: '<div>' },
190
+ suggestions: suggestions('dark-mode', '<div role="switch" data-testid="dark-mode">Dark mode</div>'),
191
+ },
192
+ ],
193
+ },
194
+ ],
195
+ });
196
+ });
197
+ //# sourceMappingURL=prefer-aria-label.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefer-aria-label.test.js","sourceRoot":"","sources":["../../src/__tests__/prefer-aria-label.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ declare const plugin: {
2
+ meta: {
3
+ name: string;
4
+ version: string;
5
+ };
6
+ rules: {
7
+ 'prefer-aria-label': import("eslint").Rule.RuleModule;
8
+ };
9
+ configs: Record<string, unknown>;
10
+ };
11
+ export default plugin;
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,QAAA,MAAM,MAAM;;;;;;;;aAQK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CACvC,CAAA;AAYD,eAAe,MAAM,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,22 @@
1
+ import preferAriaLabel from './rules/prefer-aria-label.js';
2
+ const plugin = {
3
+ meta: {
4
+ name: '@scenetest/eslint-plugin',
5
+ version: '0.1.0',
6
+ },
7
+ rules: {
8
+ 'prefer-aria-label': preferAriaLabel,
9
+ },
10
+ configs: {},
11
+ };
12
+ // Flat config (ESLint 9+) - recommended preset
13
+ plugin.configs['recommended'] = {
14
+ plugins: {
15
+ scenetest: plugin,
16
+ },
17
+ rules: {
18
+ 'scenetest/prefer-aria-label': 'warn',
19
+ },
20
+ };
21
+ export default plugin;
22
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,MAAM,8BAA8B,CAAA;AAE1D,MAAM,MAAM,GAAG;IACb,IAAI,EAAE;QACJ,IAAI,EAAE,0BAA0B;QAChC,OAAO,EAAE,OAAO;KACjB;IACD,KAAK,EAAE;QACL,mBAAmB,EAAE,eAAe;KACrC;IACD,OAAO,EAAE,EAA6B;CACvC,CAAA;AAED,+CAA+C;AAC/C,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC,GAAG;IAC9B,OAAO,EAAE;QACP,SAAS,EAAE,MAAM;KAClB;IACD,KAAK,EAAE;QACL,6BAA6B,EAAE,MAAM;KACtC;CACF,CAAA;AAED,eAAe,MAAM,CAAA"}
@@ -0,0 +1,4 @@
1
+ import type { Rule } from 'eslint';
2
+ declare const rule: Rule.RuleModule;
3
+ export default rule;
4
+ //# sourceMappingURL=prefer-aria-label.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefer-aria-label.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-aria-label.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAgFlC,QAAA,MAAM,IAAI,EAAE,IAAI,CAAC,UAuGhB,CAAA;AAED,eAAe,IAAI,CAAA"}
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Interactive HTML elements where data-testid could be replaced with aria-label.
3
+ */
4
+ const INTERACTIVE_ELEMENTS = new Set([
5
+ 'button',
6
+ 'a',
7
+ 'input',
8
+ 'select',
9
+ 'textarea',
10
+ 'summary',
11
+ 'option',
12
+ ]);
13
+ /**
14
+ * Interactive ARIA roles.
15
+ */
16
+ const INTERACTIVE_ROLES = new Set([
17
+ 'button',
18
+ 'link',
19
+ 'tab',
20
+ 'menuitem',
21
+ 'menuitemcheckbox',
22
+ 'menuitemradio',
23
+ 'checkbox',
24
+ 'radio',
25
+ 'switch',
26
+ 'slider',
27
+ 'spinbutton',
28
+ 'textbox',
29
+ 'combobox',
30
+ 'listbox',
31
+ 'searchbox',
32
+ 'option',
33
+ 'treeitem',
34
+ ]);
35
+ /**
36
+ * Event handler props that indicate an element is interactive.
37
+ */
38
+ const INTERACTIVE_HANDLERS = new Set([
39
+ 'onClick',
40
+ 'onChange',
41
+ 'onSubmit',
42
+ 'onInput',
43
+ 'onKeyDown',
44
+ 'onKeyUp',
45
+ 'onKeyPress',
46
+ ]);
47
+ function getJSXAttributeValue(attr) {
48
+ const value = attr.value;
49
+ if (!value)
50
+ return null;
51
+ if (value.type === 'Literal' && typeof value.value === 'string') {
52
+ return value.value;
53
+ }
54
+ if (value.type === 'JSXExpressionContainer' &&
55
+ value.expression &&
56
+ value.expression.type === 'Literal' &&
57
+ typeof value.expression.value === 'string') {
58
+ return value.expression.value;
59
+ }
60
+ return null;
61
+ }
62
+ const rule = {
63
+ meta: {
64
+ type: 'suggestion',
65
+ docs: {
66
+ description: 'Suggest using aria-label instead of data-testid on interactive elements for better accessibility',
67
+ recommended: true,
68
+ },
69
+ hasSuggestions: true,
70
+ messages: {
71
+ preferAriaLabel: 'Psst — this {{ element }} has a data-testid but no aria-label. An aria-label is accessible to all users AND works as a test selector.',
72
+ addAriaLabel: 'Add aria-label="{{ value }}" (keeps data-testid)',
73
+ replaceWithAriaLabel: 'Replace data-testid with aria-label="{{ value }}"',
74
+ },
75
+ schema: [],
76
+ },
77
+ create(context) {
78
+ return {
79
+ JSXOpeningElement(node) {
80
+ const el = node;
81
+ // Only handle simple element names (not member expressions like Foo.Bar)
82
+ if (el.name.type !== 'JSXIdentifier')
83
+ return;
84
+ const tagName = el.name.name ?? '';
85
+ const attributes = el.attributes.filter((attr) => attr.type === 'JSXAttribute');
86
+ // Find data-testid attribute
87
+ const testIdAttr = attributes.find((attr) => attr.name.type === 'JSXIdentifier' && attr.name.name === 'data-testid');
88
+ if (!testIdAttr)
89
+ return;
90
+ // Check if already has aria-label or aria-labelledby
91
+ const hasAriaLabel = attributes.some((attr) => attr.name.type === 'JSXIdentifier' &&
92
+ (attr.name.name === 'aria-label' || attr.name.name === 'aria-labelledby'));
93
+ if (hasAriaLabel)
94
+ return;
95
+ // Determine if element is interactive
96
+ const isInteractiveTag = INTERACTIVE_ELEMENTS.has(tagName);
97
+ const roleAttr = attributes.find((attr) => attr.name.type === 'JSXIdentifier' && attr.name.name === 'role');
98
+ const roleValue = roleAttr ? getJSXAttributeValue(roleAttr) : null;
99
+ const hasInteractiveRole = roleValue !== null && INTERACTIVE_ROLES.has(roleValue);
100
+ const hasEventHandler = attributes.some((attr) => attr.name.type === 'JSXIdentifier' &&
101
+ typeof attr.name.name === 'string' &&
102
+ INTERACTIVE_HANDLERS.has(attr.name.name));
103
+ if (!isInteractiveTag && !hasInteractiveRole && !hasEventHandler)
104
+ return;
105
+ // Get the data-testid value for the suggestion
106
+ const testIdValue = getJSXAttributeValue(testIdAttr);
107
+ const suggestions = [];
108
+ if (testIdValue) {
109
+ suggestions.push({
110
+ messageId: 'addAriaLabel',
111
+ data: { value: testIdValue },
112
+ fix(fixer) {
113
+ // Insert aria-label right after data-testid
114
+ return fixer.insertTextAfter(testIdAttr, ` aria-label="${testIdValue}"`);
115
+ },
116
+ }, {
117
+ messageId: 'replaceWithAriaLabel',
118
+ data: { value: testIdValue },
119
+ fix(fixer) {
120
+ return fixer.replaceText(testIdAttr, `aria-label="${testIdValue}"`);
121
+ },
122
+ });
123
+ }
124
+ context.report({
125
+ node: testIdAttr,
126
+ messageId: 'preferAriaLabel',
127
+ data: { element: `<${tagName}>` },
128
+ suggest: suggestions,
129
+ });
130
+ },
131
+ };
132
+ },
133
+ };
134
+ export default rule;
135
+ //# sourceMappingURL=prefer-aria-label.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefer-aria-label.js","sourceRoot":"","sources":["../../src/rules/prefer-aria-label.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@scenetest/eslint-plugin",
3
+ "version": "0.1.0",
4
+ "description": "ESLint plugin for scenetest - accessibility and testing best practices",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/scenetest/scenetest-js",
10
+ "directory": "packages/eslint-plugin"
11
+ },
12
+ "keywords": [
13
+ "eslint",
14
+ "eslintplugin",
15
+ "eslint-plugin",
16
+ "testing",
17
+ "accessibility",
18
+ "aria",
19
+ "data-testid"
20
+ ],
21
+ "main": "./dist/index.js",
22
+ "types": "./dist/index.d.ts",
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.js"
27
+ }
28
+ },
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "devDependencies": {
36
+ "@types/eslint": "^9.6.0",
37
+ "eslint": "^9.0.0",
38
+ "typescript": "^5.3.3",
39
+ "vitest": "^2.0.0"
40
+ },
41
+ "peerDependencies": {
42
+ "eslint": ">=8.0.0"
43
+ },
44
+ "scripts": {
45
+ "build": "tsc",
46
+ "test": "vitest run",
47
+ "test:watch": "vitest",
48
+ "typecheck": "tsc --noEmit"
49
+ }
50
+ }