@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 +21 -0
- package/dist/__tests__/prefer-aria-label.test.d.ts +2 -0
- package/dist/__tests__/prefer-aria-label.test.d.ts.map +1 -0
- package/dist/__tests__/prefer-aria-label.test.js +197 -0
- package/dist/__tests__/prefer-aria-label.test.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/rules/prefer-aria-label.d.ts +4 -0
- package/dist/rules/prefer-aria-label.d.ts.map +1 -0
- package/dist/rules/prefer-aria-label.js +135 -0
- package/dist/rules/prefer-aria-label.js.map +1 -0
- package/package.json +50 -0
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 @@
|
|
|
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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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
|
+
}
|