Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
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 | | |
| [consistent-data-testid](docs/rules/consistent-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
42 changes: 42 additions & 0 deletions docs/rules/consistent-data-testid.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Enforces consistent naming for the data-testid attribute (consistent-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. `{fileName}` 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` | `'^{fileName}(\_\_([A-Z]+[a-z]_?)+)_\$'` |

## Example

```json
{
"testing-library/data-testid": [
2,
{
"testIdPattern": "^TestId(__[A-Z]*)?$"
}
]
}
```
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'),
'consistent-data-testid': require('./rules/consistent-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
74 changes: 74 additions & 0 deletions lib/rules/consistent-data-testid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
'use strict';

const FILENAME_PLACEHOLDER = '{fileName}';

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

create: function(context) {
const { options, getFilename } = context;
const { testIdPattern, testIdAttribute: attr } = options[0];

function getFileNameData() {
const splitPath = getFilename().split('/');
const fileNameWithExtension = splitPath.pop();
const parent = splitPath.pop();
const fileName = fileNameWithExtension.split('.').shift();

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

function getTestIdValidator({ fileName }) {
return new RegExp(testIdPattern.replace(FILENAME_PLACEHOLDER, fileName));
}

return {
[`JSXIdentifier[name=${attr}]`]: node => {
const { value } = (node && node.parent && node.parent.value) || {};
const { fileName } = getFileNameData();
const regex = getTestIdValidator({ fileName });

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

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

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

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

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

const ruleTester = new RuleTester({ parserOptions });
ruleTester.run('consistent-data-testid', rule, {
valid: [
{
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: '^{fileName}(__([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: '^{fileName}(__([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: '^{fileName}(__([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: '{fileName}',
},
],
filename: '/my/cool/__tests__/Parent/index.js',
},
{
code: `
import React from 'react';

const TestComponent = props => {
return (
<div data-testid="wrong" custom-attr="right-1">
Hello
</div>
)
};
`,
options: [
{
testIdPattern: '^right(.*)$',
testIdAttribute: 'custom-attr',
},
],
},
{
code: `
import React from 'react';

const TestComponent = props => {
return (
<div data-test-id="Parent">
Hello
</div>
)
};
`,
options: [
{
testIdPattern: '{fileName}',
testIdAttribute: 'data-test-id',
},
],
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',
},
],
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="Parent__cool" my-custom-attr="WrongComponent__cool">
Hello
</div>
)
};
`,
options: [
{
testIdPattern: '^{fileName}(__([A-Z]+[a-z]*?)+)*$',
testIdAttribute: 'my-custom-attr',
},
],
filename: '/my/cool/__tests__/Parent/index.js',
errors: [
{
message:
'`my-custom-attr` "WrongComponent__cool" should match `/^Parent(__([A-Z]+[a-z]*?)+)*$/`',
},
],
},
{
code: `
import React from 'react';

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