diff --git a/README.md b/README.md
index 190e1159..a5b2f21b 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/docs/rules/consistent-data-testid.md b/docs/rules/consistent-data-testid.md
new file mode 100644
index 00000000..9ec5589e
--- /dev/null
+++ b/docs/rules/consistent-data-testid.md
@@ -0,0 +1,43 @@
+# 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 =>
...
;
+const foo = props => ...
;
+const foo = props => ...
;
+```
+
+Examples of **correct** code for this rule:
+
+```js
+const foo = props => ...
;
+const bar = props => ...
;
+const baz = props => ...
;
+```
+
+## Options
+
+| Option | Required | Default | Details | Example |
+| ----------------- | -------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
+| `testIdPattern` | Yes | None | 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 file name is `index.js` | `^{fileName}(\_\_([A-Z]+[a-z]_?)+)_\$` |
+| `testIdAttribute` | No | `data-testid` | A string used to specify the attribute used for querying by ID. This is only required if data-testid has been explicitly overridden in the [RTL configuration](https://testing-library.com/docs/dom-testing-library/api-queries#overriding-data-testid) | `data-my-test-attribute` |
+
+## Example
+
+```json
+{
+ "testing-library/data-testid": [
+ 2,
+ {
+ "testIdPattern": "^TestId(__[A-Z]*)?$"
+ }
+ ]
+}
+```
diff --git a/lib/index.js b/lib/index.js
index 21b3cd4d..04a2fb1f 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -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'),
diff --git a/lib/rules/consistent-data-testid.js b/lib/rules/consistent-data-testid.js
new file mode 100644
index 00000000..9c06c362
--- /dev/null
+++ b/lib/rules/consistent-data-testid.js
@@ -0,0 +1,75 @@
+'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 && node.parent.value.value;
+ const { fileName } = getFileNameData();
+ const regex = getTestIdValidator({ fileName });
+
+ if (value && !regex.test(value)) {
+ context.report({
+ node,
+ messageId: 'invalidTestId',
+ data: {
+ attr,
+ value,
+ regex,
+ },
+ });
+ }
+ },
+ };
+ },
+};
diff --git a/tests/lib/rules/consistent-data-testid.js b/tests/lib/rules/consistent-data-testid.js
new file mode 100644
index 00000000..401995f1
--- /dev/null
+++ b/tests/lib/rules/consistent-data-testid.js
@@ -0,0 +1,280 @@
+'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 (
+
+ Hello
+
+ )
+ };
+ `,
+ options: [{ testIdPattern: 'cool' }],
+ },
+ {
+ code: `
+ import React from 'react';
+
+ const TestComponent = props => {
+ return (
+
+ Hello
+
+ )
+ };
+ `,
+ options: [{ testIdPattern: 'cool' }],
+ },
+ {
+ code: `
+ import React from 'react';
+
+ const TestComponent = props => {
+ return (
+
+ Hello
+
+ )
+ };
+ `,
+ options: [
+ {
+ testIdPattern: '^{fileName}(__([A-Z]+[a-z]*?)+)*$',
+ },
+ ],
+ filename: '/my/cool/file/path/Awesome.js',
+ },
+ {
+ code: `
+ import React from 'react';
+
+ const TestComponent = props => {
+ return (
+
+ Hello
+
+ )
+ };
+ `,
+ options: [
+ {
+ testIdPattern: '^{fileName}(__([A-Z]+[a-z]*?)+)*$',
+ },
+ ],
+ filename: '/my/cool/file/path/Awesome.js',
+ },
+ {
+ code: `
+ import React from 'react';
+
+ const TestComponent = props => {
+ return (
+
+ Hello
+
+ )
+ };
+ `,
+ options: [
+ {
+ testIdPattern: '^{fileName}(__([A-Z]+[a-z]*?)+)*$',
+ },
+ ],
+ filename: '/my/cool/file/Parent/index.js',
+ },
+ {
+ code: `
+ import React from 'react';
+
+ const TestComponent = props => {
+ return (
+
+ Hello
+
+ )
+ };
+ `,
+ options: [
+ {
+ testIdPattern: '{fileName}',
+ },
+ ],
+ filename: '/my/cool/__tests__/Parent/index.js',
+ },
+ {
+ code: `
+ import React from 'react';
+
+ const TestComponent = props => {
+ return (
+
+ Hello
+
+ )
+ };
+ `,
+ options: [
+ {
+ testIdPattern: '^right(.*)$',
+ testIdAttribute: 'custom-attr',
+ },
+ ],
+ },
+ {
+ code: `
+ import React from 'react';
+
+ const TestComponent = props => {
+ return (
+
+ Hello
+
+ )
+ };
+ `,
+ options: [
+ {
+ testIdPattern: '{fileName}',
+ testIdAttribute: 'data-test-id',
+ },
+ ],
+ filename: '/my/cool/__tests__/Parent/index.js',
+ },
+ {
+ code: `
+ import React from 'react';
+
+ const TestComponent = props => {
+ const dynamicTestId = 'somethingDynamic';
+ return (
+
+ Hello
+
+ )
+ };
+ `,
+ options: [{ testIdPattern: 'somethingElse' }],
+ },
+ ],
+ invalid: [
+ {
+ code: `
+ import React from 'react';
+
+ const TestComponent = props => {
+ return (
+
+ Hello
+
+ )
+ };
+ `,
+ options: [{ testIdPattern: 'error' }],
+ errors: [
+ {
+ message: '`data-testid` "Awesome__CoolStuff" should match `/error/`',
+ },
+ ],
+ },
+ {
+ code: `
+ import React from 'react';
+
+ const TestComponent = props => {
+ return (
+
+ Hello
+
+ )
+ };
+ `,
+ 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 (
+
+ Hello
+
+ )
+ };
+ `,
+ 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 (
+
+ Hello
+
+ )
+ };
+ `,
+ 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]*?)+)*$/`',
+ },
+ ],
+ },
+ ],
+});