npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

eslint-plugin-ng-testid

v1.0.1

Published

ESLint plugin that enforces data-testid attributes on interactive Angular template elements

Downloads

213

Readme

eslint-plugin-ng-testid

ESLint plugin that enforces test attributes on interactive elements in Angular templates.

Defaults to data-testid — configurable to data-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
  • allowList to 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-testid

Usage

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