├── example ├── .npmignore ├── index.html ├── tsconfig.json ├── index.tsx └── package.json ├── .gitignore ├── src ├── types.ts ├── config.ts ├── index.tsx ├── test-config-context.ts └── components │ └── TestAttribute.tsx ├── tsconfig.json ├── test ├── config-provider.test.tsx └── basic.test.tsx ├── LICENSE ├── package.json └── README.md /example/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | dist -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .cache 5 | dist 6 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface ConfigInterface { 2 | suffix?: string; 3 | enableInProductionMode?: boolean; 4 | } 5 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { ConfigInterface } from './types'; 2 | 3 | const defaultConfig: ConfigInterface = { 4 | suffix: 'testid', 5 | enableInProductionMode: false, 6 | }; 7 | 8 | export default defaultConfig; 9 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import TestConfigContext from './test-config-context'; 2 | import { TestAttribute } from './components/TestAttribute'; 3 | 4 | const TestAttributesConfig = TestConfigContext.Provider; 5 | 6 | export { TestAttributesConfig }; 7 | export default TestAttribute; 8 | -------------------------------------------------------------------------------- /src/test-config-context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | import { ConfigInterface } from './types'; 3 | 4 | const TestConfigContext = createContext({}); 5 | TestConfigContext.displayName = 'TestAttributesConfigContext'; 6 | 7 | export default TestConfigContext; 8 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Playground 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": false, 4 | "target": "es5", 5 | "module": "commonjs", 6 | "jsx": "react", 7 | "moduleResolution": "node", 8 | "noImplicitAny": false, 9 | "noUnusedLocals": false, 10 | "noUnusedParameters": false, 11 | "removeComments": true, 12 | "strictNullChecks": true, 13 | "preserveConstEnums": true, 14 | "sourceMap": true, 15 | "lib": ["es2015", "es2016", "dom"], 16 | "baseUrl": ".", 17 | "types": ["node"] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /example/index.tsx: -------------------------------------------------------------------------------- 1 | import 'react-app-polyfill/ie11'; 2 | import * as React from 'react'; 3 | import * as ReactDOM from 'react-dom'; 4 | import Test, { TestAttributesConfig } from '../.'; 5 | 6 | const App = () => { 7 | return ( 8 | 9 | 10 |

Title

11 |
12 | 13 |

Subtitle

14 |
15 | 16 | 17 | 18 |
19 | ); 20 | }; 21 | 22 | ReactDOM.render(, document.getElementById('root')); 23 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "parcel index.html", 8 | "build": "parcel build index.html" 9 | }, 10 | "dependencies": { 11 | "react-app-polyfill": "^1.0.0" 12 | }, 13 | "alias": { 14 | "react": "../node_modules/react", 15 | "react-dom": "../node_modules/react-dom/profiling", 16 | "scheduler/tracing": "../node_modules/scheduler/tracing-profiling" 17 | }, 18 | "devDependencies": { 19 | "@types/react": "^16.9.11", 20 | "@types/react-dom": "^16.8.4", 21 | "parcel": "^1.12.3", 22 | "typescript": "^3.4.5" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "types", "test"], 3 | "compilerOptions": { 4 | "target": "es5", 5 | "module": "esnext", 6 | "lib": ["dom", "dom.iterable", "esnext"], 7 | "importHelpers": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "rootDir": "./", 11 | "strict": true, 12 | "noImplicitAny": true, 13 | "strictNullChecks": true, 14 | "strictFunctionTypes": true, 15 | "strictPropertyInitialization": true, 16 | "noImplicitThis": true, 17 | "alwaysStrict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noImplicitReturns": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "moduleResolution": "node", 23 | "baseUrl": "./", 24 | "paths": { 25 | "*": ["src/*", "node_modules/*"] 26 | }, 27 | "jsx": "react", 28 | "esModuleInterop": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/config-provider.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import Test, { TestAttributesConfig } from '../src'; 4 | 5 | describe('Config provider', () => { 6 | it('Correctly inherits attribute suffix from Provider', () => { 7 | const { container } = render( 8 | 9 | 10 |

Title

11 |
12 |
13 | ); 14 | 15 | expect(container.querySelector('[data-tid="title"]')).toBeInstanceOf( 16 | HTMLHeadingElement 17 | ); 18 | }); 19 | 20 | it('Correctly handles configuration precedence', () => { 21 | const { container } = render( 22 | 23 | 24 |

Title

25 |
26 |
27 | ); 28 | 29 | expect( 30 | container.querySelector('[data-test-inner-id="title"]') 31 | ).toBeInstanceOf(HTMLHeadingElement); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dennis Morello 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /test/basic.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import Test from '../src'; 4 | 5 | describe('Basic actions', () => { 6 | it('Renders without crashing', () => { 7 | render( 8 | 9 |

Title

10 |
11 | ); 12 | }); 13 | 14 | it('Correctly sets "data-testid" attribute', () => { 15 | const { container } = render( 16 | 17 |

Title

18 |
19 | ); 20 | 21 | expect(container.querySelector('[data-testid="title"]')).toBeInstanceOf( 22 | HTMLHeadingElement 23 | ); 24 | }); 25 | 26 | it('Correctly handles React fragments', () => { 27 | const { container } = render( 28 | 29 | 30 |

Title

31 |
32 |
33 | ); 34 | 35 | expect(container.querySelector('[data-testid="title"]')).toBeInstanceOf( 36 | HTMLDivElement 37 | ); 38 | }); 39 | 40 | it('Correctly sets custom "data-" attribute', () => { 41 | const { container } = render( 42 | 43 |

Title

44 |
45 | ); 46 | 47 | expect(container.querySelector('[data-test="title"]')).toBeInstanceOf( 48 | HTMLHeadingElement 49 | ); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/components/TestAttribute.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactIs from 'react-is'; 3 | import defaultConfig from '../config'; 4 | import { ConfigInterface } from '../types'; 5 | import TestConfigContext from '../test-config-context'; 6 | 7 | interface TestAttributeProps extends ConfigInterface { 8 | id: string; 9 | } 10 | 11 | export const TestAttribute: React.FC = ({ 12 | children, 13 | ...props 14 | }) => { 15 | const isProduction = process.env.NODE_ENV === 'production'; 16 | 17 | const config = Object.assign( 18 | {}, 19 | defaultConfig, 20 | React.useContext(TestConfigContext), 21 | props 22 | ); 23 | 24 | function withTestAttribute(nodes: React.ReactNode): React.ReactNode { 25 | const node = React.Children.only(nodes); 26 | const testAttributeName = `data-${config.suffix}`; 27 | 28 | if (ReactIs.isFragment(node)) { 29 | return React.createElement( 30 | 'div', 31 | { [testAttributeName]: config.id }, 32 | node 33 | ); 34 | } else if (ReactIs.isElement(node)) { 35 | return React.cloneElement(node as React.ReactElement, { 36 | [testAttributeName]: config.id, 37 | }); 38 | } else { 39 | return node; 40 | } 41 | } 42 | 43 | return ( 44 | 45 | {!config.enableInProductionMode && isProduction 46 | ? children 47 | : withTestAttribute(children)} 48 | 49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-test-attributes", 3 | "description": "React library to add data-* attributes to DOM elements.", 4 | "version": "1.0.0", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Dennis Morello", 8 | "email": "dennismorello@gmail.com", 9 | "url": "https://morello.dev" 10 | }, 11 | "keywords": [ 12 | "react", 13 | "dom", 14 | "e2e", 15 | "data", 16 | "test", 17 | "testing", 18 | "selenium", 19 | "cypress", 20 | "javascript", 21 | "typescript", 22 | "attributes" 23 | ], 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/dennismorello/react-test-attributes" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/dennismorello/react-test-attributes/issues" 30 | }, 31 | "main": "dist/index.js", 32 | "module": "dist/react-test-attributes.esm.js", 33 | "typings": "dist/index.d.ts", 34 | "files": [ 35 | "dist" 36 | ], 37 | "scripts": { 38 | "start": "tsdx watch", 39 | "build": "tsdx build", 40 | "test": "tsdx test --passWithNoTests", 41 | "lint": "tsdx lint", 42 | "prepare": "tsdx build", 43 | "postpublish": "PACKAGE_VERSION=$(cat package.json | grep \\\"version\\\" | head -1 | awk -F: '{ print $2 }' | sed 's/[\",]//g' | tr -d '[[:space:]]') && git tag v$PACKAGE_VERSION && git push --tags" 44 | }, 45 | "peerDependencies": { 46 | "react": ">=16" 47 | }, 48 | "husky": { 49 | "hooks": { 50 | "pre-commit": "tsdx lint" 51 | } 52 | }, 53 | "prettier": { 54 | "printWidth": 80, 55 | "semi": true, 56 | "singleQuote": true, 57 | "trailingComma": "es5" 58 | }, 59 | "devDependencies": { 60 | "@testing-library/react": "^9.4.0", 61 | "@types/jest": "^24.0.25", 62 | "@types/react": "^16.9.17", 63 | "@types/react-dom": "^16.9.4", 64 | "@types/react-is": "^16.7.1", 65 | "husky": "^3.1.0", 66 | "react": "^16.12.0", 67 | "react-dom": "^16.12.0", 68 | "tsdx": "^0.12.1", 69 | "tslib": "^1.10.0", 70 | "typescript": "^3.7.4" 71 | }, 72 | "dependencies": { 73 | "react-is": "^16.12.0" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Test Attributes 2 | 3 | [![Version](https://badgen.net/npm/v/react-test-attributes)](https://www.npmjs.com/package/react-test-attributes/v/latest) 4 | [![Last Commit](https://badgen.net/github/last-commit/dennismorello/react-test-attributes)](https://github.com/dennismorello/react-test-attributes/commits/master) 5 | [![Downloads](https://badgen.net/npm/dt/react-test-attributes)](https://www.npmjs.com/package/react-test-attributes/v/latest) 6 | [![Size](https://badgen.net/bundlephobia/minzip/react-test-attributes)](https://bundlephobia.com/result?p=react-test-attributes@latest) 7 | [![License](https://badgen.net/npm/license/react-test-attributes)](https://www.npmjs.com/package/react-test-attributes/v/latest) 8 | 9 | [React Test Attributes](https://github.com/dennismorello/react-test-attributes) is a library for React apps that decorates the DOM with custom attributes that can be used to uniquely indentify elements in a page. The main use case is for E2E testing using tools like [Cypress](https://www.cypress.io) or [Selenium](https://selenium.dev). 10 | 11 | ## Table Of Contents 12 | 13 | - [Features](#features) 14 | - [Installation](#installation) 15 | - [Quick Start](#quick-start) 16 | - [Usage](#usage) 17 | - [API](#api) 18 | - [Global Configuration](#global-configuration) 19 | - [Contributing](#contributing) 20 | - [License](#license) 21 | 22 | ## Features 23 | 24 | - 🏷 **TypeScript support** - It is written in TypeScript to make it easier and faster to use the library 25 | - 🍃 **Lightweight** - Almost zero footprint on your project and no other dependencies required 26 | - 🚀 **Production mode** - `data-*` attributes are added to the DOM only when not in production mode 27 | - 🌳 **Tree-shakeable** - Only the parts you use will be included in your final bundle 28 | 29 | ## Installation 30 | 31 | To add this package as a dependency to your app, simply run 32 | 33 | ```sh 34 | npm install react-test-attributes --save 35 | ``` 36 | 37 | or, if you are using Yarn (as I strongly suggest): 38 | 39 | ```sh 40 | yarn add react-test-attributes 41 | ``` 42 | 43 | ## Quick Start 44 | 45 | Import [React Test Attributes](https://www.npmjs.com/package/react-test-attributes) to your React component (note that, since it is the default export of this package, you can name it whatever you want): 46 | 47 | ```js 48 | import Test from 'react-test-attributes'; 49 | ``` 50 | 51 | Then simply wrap the components you want to decorate: 52 | 53 | ```jsx 54 | 55 | 56 | 57 | ``` 58 | 59 | The resulting DOM will be the following, depending on the value of `NODE_ENV` environment variable when your project is built: 60 | 61 | ```html 62 | 63 | 64 | 65 | 66 | 67 | ``` 68 | 69 | ### Usage 70 | 71 | #### API 72 | 73 | The `Test` component accepts the following props: 74 | 75 | - `id` is the value of the added attribute 76 | - `suffix` is the string to append to `"data-"` when building the attribute name (default to `"testid"`) 77 | - `enableInProductionMode` indicates whether or not adding the test attribute in production mode (default to `false`) 78 | 79 | For example, if you want to name the attribute `data-tid` and give it the value `"link-home"` you should write: 80 | 81 | ```jsx 82 | 83 | Home 84 | 85 | ``` 86 | 87 | This produces the following DOM: 88 | 89 | ```html 90 | Home 91 | ``` 92 | 93 | #### Global Configuration 94 | 95 | The context `TestAttributesConfig` can provide a global configuration to all of its `Test` descendants. 96 | 97 | For example, we can globally override the suffix and enable writing the test attributes also in production mode by doing this: 98 | 99 | ```jsx 100 | import Test, { TestAttributesConfig } from 'react-test-attributes'; 101 | 102 | const App = () => { 103 | return ( 104 | 107 | 108 |

I am the title

109 |
110 | 111 | Home 112 | 113 |
114 | ); 115 | }; 116 | ``` 117 | 118 | This produces the following DOM: 119 | 120 | ```html 121 |

I am the title

122 | Home 123 | ``` 124 | 125 | ## Contributing 126 | 127 | If you find any bug or if you have ideas on how to improve this project, you are more than welcome to open issues and/or making pull requests! 128 | 129 | ## License 130 | 131 | Project source code is licensed under the MIT license. You are free to fork this repository, edit the code, share and use it both for non-commercial and commercial purposes. 132 | --------------------------------------------------------------------------------