Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ To enable this configuration use the `extends` property in your
| [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] |
| [prefer-expect-query-by](docs/rules/prefer-expect-query-by.md) | Disallow the use of `expect(getBy*)` | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | |
| [data-testid](docs/rules/data-testid.md) | Ensure `data-testid` values match a provided regex. | | |

[build-badge]: https://img.shields.io/travis/Belco90/eslint-plugin-testing-library?style=flat-square
[build-url]: https://travis-ci.org/belco90/eslint-plugin-testing-library
Expand Down
46 changes: 46 additions & 0 deletions docs/rules/data-testid.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Enforces consistent naming for the data-testid attribute (data-testid)

Ensure `data-testid` values match a provided regex. This rule is un-opinionated, and requires configuration.

## Rule Details

> Assuming the rule has been configured with the following regex: `^TestId(\_\_[A-Z]*)?$`
Examples of **incorrect** code for this rule:

```js
const foo = props => <div data-testid="my-test-id">...</div>;
const foo = props => <div data-testid="myTestId">...</div>;
const foo = props => <div data-testid="TestIdEXAMPLE">...</div>;
```

Examples of **correct** code for this rule:

```js
const foo = props => <div data-testid="TestId__EXAMPLE">...</div>;

const bar = props => <div data-testid="TestId">...</div>;

const baz = props => <div>...</div>;
```

## Options

| Option | Details | Example |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
| testIdPattern | A regex used to validate the format of the `data-testid` value. `{componentName}` can optionally be used as a placeholder and will be substituted with the name of the file OR the name of the files parent directory in the case when the fileName is `index.js` | `'^{componentName}(\_\_([A-Z]+[a-z]_?)+)_\$'` |
| excludePaths | An array of path strings to exclude from the check | `["__tests__"]` |

## Example

```json
{
"testing-library/data-testid": [
2,
{
"testIdPattern": "^TestId(__[A-Z]*)?$",
"excludePaths": ["__tests__"]
}
]
}
```
1 change: 1 addition & 0 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const rules = {
'await-async-query': require('./rules/await-async-query'),
'await-fire-event': require('./rules/await-fire-event'),
'data-testid': require('./rules/data-testid'),
'no-await-sync-query': require('./rules/no-await-sync-query'),
'no-debug': require('./rules/no-debug'),
'no-dom-import': require('./rules/no-dom-import'),
Expand Down
81 changes: 81 additions & 0 deletions lib/rules/data-testid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
'use strict';

module.exports = {
meta: {
docs: {
description: 'Ensures consistent usage of `data-testid`',
category: 'Best Practices',
recommended: false,
},
messages: {
invalidTestId: '`data-testid` "{{value}}" should match `{{regex}}`',
},
fixable: null,
schema: [
{
type: 'object',
additionalProperties: false,
properties: {
testIdPattern: {
type: 'string',
default: '',
},
excludePaths: {
type: 'array',
items: { type: 'string' },
default: [],
},
},
},
],
},

create: function(context) {
const { options, getFilename } = context;
const defaultOptions = { testIdPattern: '', excludePaths: [] };
const ruleOptions = options.length ? options[0] : defaultOptions;

function getComponentData() {
const splitPath = getFilename().split('/');
const exclude = ruleOptions.excludePaths.some(path =>
splitPath.includes(path)
);
const fileNameWithExtension = splitPath.pop();
const parent = splitPath.pop();
const fileName = fileNameWithExtension.split('.').shift();

return {
componentDescriptor: fileName === 'index' ? parent : fileName,
exclude,
};
}

function getTestIdValidator({ componentName }) {
return new RegExp(
ruleOptions.testIdPattern.replace('{componentName}', componentName)
);
}

return {
'JSXIdentifier[name=data-testid]': node => {
const { value } = (node && node.parent && node.parent.value) || {};
const {
componentDescriptor: componentName,
exclude,
} = getComponentData();
const regex = getTestIdValidator({ componentName });

if (!exclude && value && !regex.test(value)) {
context.report({
node,
messageId: 'invalidTestId',
data: {
value,
regex,
},
});
}
},
};
},
};
217 changes: 217 additions & 0 deletions tests/lib/rules/data-testid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
'use strict';

// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------

const rule = require('../../../lib/rules/data-testid');
const RuleTester = require('eslint').RuleTester;

// ------------------------------------------------------------------------------
// Tests
// ------------------------------------------------------------------------------

const parserOptions = {
ecmaVersion: 2018,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
};

const ruleTester = new RuleTester({ parserOptions });
ruleTester.run('data-testid', rule, {
valid: [
{
code: `
import React from 'react';

const TestComponent = props => {
return (
<div data-testid="cool">
Hello
</div>
)
};
`,
options: [],
},
{
code: `
import React from 'react';

const TestComponent = props => {
return (
<div data-testid="cool">
Hello
</div>
)
};
`,
options: [{ testIdPattern: 'cool' }],
},
{
code: `
import React from 'react';

const TestComponent = props => {
return (
<div className="cool">
Hello
</div>
)
};
`,
options: [{ testIdPattern: 'cool' }],
},
{
code: `
import React from 'react';

const TestComponent = props => {
return (
<div data-testid="Awesome__CoolStuff">
Hello
</div>
)
};
`,
options: [
{
testIdPattern: '^{componentName}(__([A-Z]+[a-z]*?)+)*$',
},
],
filename: '/my/cool/file/path/Awesome.js',
},
{
code: `
import React from 'react';

const TestComponent = props => {
return (
<div data-testid="Awesome">
Hello
</div>
)
};
`,
options: [
{
testIdPattern: '^{componentName}(__([A-Z]+[a-z]*?)+)*$',
},
],
filename: '/my/cool/file/path/Awesome.js',
},
{
code: `
import React from 'react';

const TestComponent = props => {
return (
<div data-testid="Parent">
Hello
</div>
)
};
`,
options: [
{
testIdPattern: '^{componentName}(__([A-Z]+[a-z]*?)+)*$',
},
],
filename: '/my/cool/file/Parent/index.js',
},
{
code: `
import React from 'react';

const TestComponent = props => {
return (
<div data-testid="Parent">
Hello
</div>
)
};
`,
options: [
{
testIdPattern: '^{componentName}(__([A-Z]+[a-z]*?)+)*$',
excludePaths: ['__tests__'],
},
],
filename: '/my/cool/__tests__/Parent/index.js',
},
],
invalid: [
{
code: `
import React from 'react';

const TestComponent = props => {
return (
<div data-testid="Awesome__CoolStuff">
Hello
</div>
)
};
`,
options: [{ testIdPattern: 'error' }],
errors: [
{
message: '`data-testid` "Awesome__CoolStuff" should match `/error/`',
},
],
},
{
code: `
import React from 'react';

const TestComponent = props => {
return (
<div data-testid="Nope">
Hello
</div>
)
};
`,
options: [
{
testIdPattern: 'matchMe',
excludePaths: ['__mocks__'],
},
],
filename: '/my/cool/__tests__/Parent/index.js',
errors: [
{
message: '`data-testid` "Nope" should match `/matchMe/`',
},
],
},
{
code: `
import React from 'react';

const TestComponent = props => {
return (
<div data-testid="WrongComponent__cool">
Hello
</div>
)
};
`,
options: [
{
testIdPattern: '^{componentName}(__([A-Z]+[a-z]*?)+)*$',
excludePaths: ['__mocks__'],
},
],
filename: '/my/cool/__tests__/Parent/index.js',
errors: [
{
message:
'`data-testid` "WrongComponent__cool" should match `/^Parent(__([A-Z]+[a-z]*?)+)*$/`',
},
],
},
],
});