eslint-plugin-ng-testid
v1.0.1
Published
ESLint plugin that enforces data-testid attributes on interactive Angular template elements
Downloads
213
Maintainers
Readme
eslint-plugin-ng-testid
ESLint plugin that enforces test attributes on interactive elements in Angular templates.
Defaults to
data-testid— configurable todata-cy,data-test, or any custom attribute.
Stop hunting for why your Playwright / Cypress selectors stopped working. This rule flags every interactive Angular element that is missing a data-testid at lint time, before the code ever reaches CI.
Features
- ✅ Detects interactive elements by tag,
role,tabindex, event bindings, and[routerLink] - ✅ Accepts both static (
data-testid="foo") and bound ([attr.data-testid]="expr") attributes - ✅ Configurable attribute name — use
data-cy,data-test, or any custom attribute - ✅ Optional regex pattern validation on static values
- ✅ Optional strict mode — rejects opaque dynamic expressions, validates string literals
- ✅
allowListto opt out specific tags project-wide - ✅ Ships two ready-made flat config presets (
recommended/strict) - ✅ Angular 19+ / ESLint 9+ / Flat config only
Requirements
| Peer dependency | Version |
| -------------------------- | --------- |
| eslint | ^9.0.0 |
| @angular-eslint/utils | ^19.0.0 |
| @typescript-eslint/utils | ^8.0.0 |
Installation
npm install --save-dev eslint-plugin-ng-testidUsage
Quickstart — recommended preset
// eslint.config.js
import ngTestId from "eslint-plugin-ng-testid";
export default [
// ... your other configs
ngTestId.configs.recommended, // adds the plugin + turns on the rule as 'error'
];Manual setup
// eslint.config.js
import ngTestId from "eslint-plugin-ng-testid";
export default [
{
plugins: { "ng-testid": ngTestId },
rules: {
"ng-testid/require-data-testid": "error",
},
},
];Rule: require-data-testid
Requires a non-empty data-testid on every interactive Angular template element that has no interactive descendants.
What counts as interactive?
| Signal | Example |
| ---------------------------- | ------------------------------------------------------ |
| Interactive HTML tag | <button>, <a>, <input>, <select>, <textarea> |
| role attribute | role="button", role="menuitem", role="tab", … |
| tabindex ≠ -1 and ≠ "" | tabindex="0" |
| Output / event binding | (click), (keydown), (focus), … |
| [routerLink] input | [routerLink]="['/home']" |
What suppresses the rule?
| Condition | Example |
| -------------------------------------- | ------------------------------------------------ |
| disabled attribute (static or bound) | <button disabled> or <button [disabled]="x"> |
| aria-hidden="true" | <button aria-hidden="true"> |
| input type="hidden" | <input type="hidden"> |
| Tag in allowList | configured per project |
Elements wrapped around another interactive element are exempt (e.g. a <label> containing an <input> does not also need a testid).
Options
{
attribute?: string; // default: "data-testid". Use "data-cy", "data-test", etc.
pattern?: string; // RegExp source — validates static attribute values
allowDynamic?: boolean; // default: true
allowList?: string[]; // tag names to skip entirely
}attribute — custom attribute name
Not everyone uses data-testid. If your project uses data-cy, data-test, or any custom attribute, configure it here. The rule name stays require-data-testid regardless — it's just a name.
rules: {
'ng-testid/require-data-testid': ['error', {
attribute: 'data-cy',
}],
}<!-- ✅ passes — data-cy is present -->
<button data-cy="save-btn">Save</button>
<!-- ❌ fails — data-cy is missing; data-testid is irrelevant when attribute is overridden -->
<button data-testid="save-btn">Save</button>pattern — enforce a naming convention
// eslint.config.js
rules: {
'ng-testid/require-data-testid': ['error', {
pattern: '^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$', // kebab-case
}],
}<!-- ✅ passes -->
<button data-testid="save-btn">Save</button>
<!-- ❌ fails: "SaveBtn" does not match pattern -->
<button data-testid="SaveBtn">Save</button>allowDynamic — control bound expressions
When true (default), any non-empty [attr.data-testid] binding is accepted without pattern validation.
When false, dynamic expressions that are string literals are validated against pattern. Non-literal expressions (variables, ternaries, calls) are still accepted.
rules: {
'ng-testid/require-data-testid': ['error', {
pattern: '^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$',
allowDynamic: false,
}],
}<!-- ✅ literal matches pattern -->
<button [attr.data-testid]="'save-btn'">Save</button>
<!-- ❌ literal violates pattern -->
<button [attr.data-testid]="'SaveBtn'">Save</button>
<!-- ✅ non-literal accepted (can't statically validate) -->
<button [attr.data-testid]="buttonId">Save</button>allowList — opt out specific tags
Useful for custom component libraries whose button wrapper already enforces testids internally.
rules: {
'ng-testid/require-data-testid': ['error', {
allowList: ['my-button', 'app-icon-btn'],
}],
}<!-- ✅ tag is on the allowList -->
<my-button>Save</my-button>Preset configs
recommended
Turns the rule on as 'error' with all defaults (allowDynamic: true, no pattern, no allowList).
import ngTestId from "eslint-plugin-ng-testid";
export default [ngTestId.configs.recommended];strict
Applies a kebab-case pattern and disables opaque dynamic bindings.
Pattern: ^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$
import ngTestId from "eslint-plugin-ng-testid";
export default [ngTestId.configs.strict];Full example — eslint.config.js
import angular from "angular-eslint";
import tseslint from "typescript-eslint";
import ngTestId from "eslint-plugin-ng-testid";
export default tseslint.config(
{
files: ["**/*.ts"],
extends: [...angular.configs.tsRecommended],
},
{
files: ["**/*.html"],
extends: [...angular.configs.templateRecommended],
plugins: { "ng-testid": ngTestId },
rules: {
"ng-testid/require-data-testid": [
"error",
{
pattern: "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$",
allowDynamic: true,
allowList: ["app-submit-button"],
},
],
},
},
);Error messages
| Message ID | When |
| --------------------- | --------------------------------------------------------------- |
| missingTestId | No data-testid found, or the value is empty / whitespace-only |
| invalidTestIdFormat | data-testid present but fails the pattern check |
Changelog
1.0.0
- Initial release
License
MIT © Mihai Ro
