@masknet/eslint-plugin
Version:
eslint plugin for masknet
100 lines (94 loc) • 3.12 kB
text/typescript
import type { TSESTree } from '@typescript-eslint/types'
import { createRule } from '../../rule.ts'
export interface Options {
'id': string
'elements': string[]
'attributes': string[]
'ignore-attributes': string[]
}
const DEFAULT_ELEMENT_LIST = ['a', 'button', 'input', 'textarea']
const DEFAULT_ATTRIBUTE_LIST = ['editable', 'onInput', 'onChange', 'onClick', 'onKeyDown', 'onKeyUp', 'onSubmit']
const DEFAULT_IGNORE_ATTRIBUTE_LIST = ['disabled', 'readonly']
export default createRule({
name: 'jsx/prefer-test-id',
meta: {
type: 'suggestion',
docs: {
description: 'Enforces `data-test-id` attribute is present on interactive DOM elements to help with UI testing',
},
schema: [
{
type: 'object',
properties: {
'id': { type: 'string' },
'elements': { type: 'array', items: { type: 'string' }, uniqueItems: true },
'attributes': { type: 'array', items: { type: 'string' }, uniqueItems: true },
'ignore-attributes': { type: 'array', items: { type: 'string' }, uniqueItems: true },
},
additionalProperties: false,
},
],
messages: {
element: '<{{element}}> elements must have a `{{id}}` attribute',
attribute: '<{{element}}> with an `{{name}}` handler must have a `{{id}}` attribute',
},
},
defaultOptions: [
{
'id': 'data-test-id',
'elements': DEFAULT_ELEMENT_LIST,
'attributes': DEFAULT_ATTRIBUTE_LIST,
'ignore-attributes': DEFAULT_IGNORE_ATTRIBUTE_LIST,
},
],
create(context, [options]) {
const source = context.sourceCode
return {
JSXOpeningElement(node) {
const element = source.getText(node.name)
const attributes = new Map(
node.attributes
.filter((node): node is TSESTree.JSXAttribute => node.type === 'JSXAttribute')
.map(({ name, value }) => [source.getText(name), value] as const),
)
if (isValid(attributes.get(options.id))) return
if (shouldIgnore(attributes, options['ignore-attributes'])) return
if (options.elements.includes(element)) {
context.report({
node,
messageId: 'element',
data: { element, id: options.id },
})
} else {
const name = options.attributes.find((name) => attributes.has(name))
if (!name) return
context.report({
node,
messageId: 'attribute',
data: { element, name, id: options.id },
})
}
},
}
},
})
function shouldIgnore(attributes: Map<string, TSESTree.JSXAttribute['value']>, ignore: string[]) {
return ignore.some((name) => {
const node = attributes.get(name)
if (node === null) return true
if (node?.type === 'JSXExpressionContainer') return false
return isValid(node)
})
}
function isValid(node?: TSESTree.Node | null): boolean {
if (!node) return false
switch (node.type) {
case 'Literal': {
return Boolean(node.value)
}
case 'JSXExpressionContainer': {
return true
}
}
return false
}