UNPKG

@masknet/eslint-plugin

Version:
100 lines (94 loc) 3.12 kB
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 }