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

reusely-vite-testid

v0.2.0

Published

Auto-inject data-testid attributes into Vue components at compile time for e2e testing

Readme

reusely-vite-testid

Auto-inject data-testid attributes into Vue components at compile time. No manual annotation needed — your QA team gets stable selectors for e2e testing out of the box.

How it works

The plugin has two layers:

  1. Template transform — hooks into Vue's nodeTransforms to inject data-testid on elements in <template> blocks
  2. Render function transform — parses h() calls in <script> blocks and injects data-testid into props objects
src/components/Auth/LoginForm.vue
┌─────────────────────────────────────────┐
│ <input v-model="form.email" />          │  → data-testid="login-form--email-input"
│ <input v-model="form.password" />       │  → data-testid="login-form--password-input"
│ <button @click="handleSubmit">Login</button> │  → data-testid="login-form--submit-btn"
└─────────────────────────────────────────┘

Format: {namespace}--{identifier}

  • Namespace: derived from file path (LoginForm.vuelogin-form)
  • Identifier: derived from element semantics (v-model, name, id, event handler, etc.)

Install

npm install reusely-vite-testid --save-dev

Setup

Vite

// vite.config.js
import { createTestIdPlugin } from 'reusely-vite-testid'

const { plugin, nodeTransform } = createTestIdPlugin({
  enabled: process.env.NODE_ENV !== 'production',
})

export default defineConfig({
  plugins: [
    plugin,
    vue({
      template: {
        compilerOptions: {
          nodeTransforms: [nodeTransform],
        },
      },
    }),
  ],
})

Vitest

// vitest.config.js
import { createTestIdPlugin } from 'reusely-vite-testid'

const { plugin, nodeTransform } = createTestIdPlugin({ enabled: true })

export default defineConfig({
  plugins: [
    plugin,
    vue({
      template: {
        compilerOptions: {
          nodeTransforms: [nodeTransform],
        },
      },
    }),
  ],
})

Identifier Priority Chain

The plugin resolves identifiers in this order. First match wins:

| Priority | Source | Example Input | Example Output | |----------|--------|---------------|----------------| | 1 | Explicit data-testid | data-testid="custom" | custom (preserved, no override) | | 2 | id attribute | id="email-field" | email-field | | 3 | name attribute | name="username" | username-input | | 4 | v-model binding | v-model="form.email" | email-input | | 5 | aria-label | aria-label="Search" | search | | 6 | Event handler | @click="handleSave" | save-btn | | 7 | type attribute | <input type="email"> | email-input | | 8 | Component tag | <DatePicker /> | date-picker (only in componentMode: 'all') | | 9 | placeholder | placeholder="Search..." | search | | 10 | Hash fallback | (no semantic info) | div-a3f2 |

Component Mode

Controls when data-testid is injected on Vue components (custom elements like <DatePicker />, <Tooltip />, etc.).

| Mode | Behavior | |------|----------| | 'interactive' (default) | Only inject on components that resolved via semantic priorities 2-6 (id, name, v-model, aria-label, event handler). Components with no semantic info are skipped — this avoids Vue fragment warnings. | | 'all' | Inject on all components, including those without semantic attributes. Uses kebab-cased tag name as fallback (priority 8). Recommended when combined with excludeComponents for known fragment components. |

createTestIdPlugin({
  filter: {
    componentMode: 'all',
    excludeComponents: [
      'RouterView', 'TransitionGroup', 'Transition',
    ],
  },
})

Why this matters: Vue 3 components that render fragment or text root nodes cannot inherit non-prop attributes like data-testid. Injecting on these components produces console warnings. Use excludeComponents to skip them when using componentMode: 'all'.

Render Function Transform

For components that use h() render functions instead of <template>, the plugin provides a Vite transform hook that parses h() calls and injects data-testid into props.

Setup

createTestIdPlugin({
  renderFn: {
    enabled: true,
    paths: ['system-design/src/components/'],
  },
})

How it works

The transform finds h() calls with string-literal tags and extracts semantic info from the props object:

// Before
h('input', { type: 'checkbox', onInput: this.changeValue })

// After
h('input', { 'data-testid': 'bbui-checkbox--checkbox-input', type: 'checkbox', onInput: this.changeValue })

What gets processed

  • h('div', { onClick: this.handleToggle }) — structural element with event handler
  • h('input', { type: 'email' }) — interactive element
  • h('button', { onClick: this.handleSave }) — interactive element with event

What gets skipped

  • h(MyComponent) — component references (not string literal)
  • h(cond ? A : B, ...) — dynamic/conditional tags
  • h('span', ...) — tags in skipTags
  • h('div', { class: 'wrapper' }) — structural element without events
  • h() calls with existing data-testid in props

Extractable props

Only string-literal values and method references can be extracted at compile time:

| Pattern | Extracted | |---------|-----------| | type: 'checkbox' | attrs.type = 'checkbox' | | name: 'email' | attrs.name = 'email' | | id: 'my-field' | attrs.id = 'my-field' | | placeholder: 'Search' | attrs.placeholder = 'Search' | | onClick: this.handleSave | events.click = 'handleSave' | | onInput: this.changeValue | events.input = 'changeValue' | | name: this.$props.name | skipped (dynamic) | | type: type \|\| 'text' | skipped (expression) | | onClick: (e) => emit() | skipped (arrow function) |

Configuration

All options are optional. Defaults are designed to work out of the box for most Vue projects.

createTestIdPlugin({
  // Master switch
  enabled: true,

  // Which attributes to inject
  attributes: {
    testId: { enabled: true, name: 'data-testid' },
    component: { enabled: false, name: 'data-component' },
    feature: { enabled: false, name: 'data-feature' },
  },

  namespace: {
    separator: '--',           // between namespace and identifier
    prefix: '',                // global prefix for all testids
    maxSegments: 4,            // max path segments in namespace
    stripPaths: [              // path prefixes to remove
      'src/components/',
      'src/views/',
    ],
    stripLayers: [],           // folder names to skip (e.g. ['Features'])
    mappings: {},              // manual overrides: { 'path/file.vue': 'custom-namespace' }
    libraryPrefixes: [],       // UI library path → prefix mapping
  },

  identifier: {
    hashFallback: true,        // generate hash for elements without semantic info
    tagSuffixMap: {},          // extend/override tag → suffix mapping
    eventTagSuffixMap: {},     // extend/override event tag → suffix mapping
    eventHandlerPrefixes: ['handle', 'on'],
  },

  filter: {
    componentMode: 'interactive', // 'interactive' | 'all'
    skipTags: [                // HTML tags to never inject on
      'span', 'br', 'hr', 'img', 'svg', 'path',
      'template', 'slot', 'transition', 'keep-alive',
      'teleport',
    ],
    skipPatterns: [],          // regex patterns to skip (e.g. [/^Icon/])
    excludeComponents: [],     // component names to skip (supports PascalCase and kebab-case)
    excludeComponentPatterns: [],  // regex patterns to skip components
  },

  // Render function transform (for h() calls)
  renderFn: {
    enabled: false,            // enable render function transform
    paths: [],                 // file path prefixes to process (e.g. ['system-design/src/'])
  },
})

Recipes

Full setup with render function support

createTestIdPlugin({
  enabled: true,
  namespace: {
    stripPaths: [
      'src/components/',
      'src/views/',
      'system-design/src/components/',
    ],
    stripLayers: ['Features'],
    libraryPrefixes: [
      { path: 'system-design/src/components/', prefix: 'bbui' },
    ],
  },
  filter: {
    componentMode: 'all',
    excludeComponents: [
      'RouterView', 'RouterLink',
      'TransitionGroup', 'Transition',
    ],
    excludeComponentPatterns: [/^Icon/],
  },
  renderFn: {
    enabled: true,
    paths: ['system-design/src/components/'],
  },
})

Skip specific components

Component names are matched in both PascalCase and kebab-case:

createTestIdPlugin({
  filter: {
    excludeComponents: [
      'RouterView',      // matches <RouterView> and <router-view>
      'TransitionGroup', // matches <TransitionGroup> and <transition-group>
    ],
  },
})

Skip Icon components

createTestIdPlugin({
  filter: {
    excludeComponentPatterns: [/^Icon/],
  },
})

UI library prefix

If you have a design system in a separate folder:

createTestIdPlugin({
  namespace: {
    stripPaths: [
      'src/components/',
      'packages/ui/src/components/',
    ],
    libraryPrefixes: [
      { path: 'packages/ui/src/components/', prefix: 'ui' },
    ],
  },
})

This turns packages/ui/src/components/Button.vueui-button.

Custom tag suffixes

createTestIdPlugin({
  identifier: {
    tagSuffixMap: {
      'bselect': '-select',
      'datepickerpanel': '-picker',
      'combobox': '-select',
    },
  },
})

Organizational layers

If your folder structure has grouping layers you want to ignore:

src/components/Features/Buyback/BuybackOffer/BuybackOfferSearch.vue
createTestIdPlugin({
  namespace: {
    stripLayers: ['Features'],
  },
})

Result: buyback-offer-search (instead of features-buyback-offer-search).

Built-in Suffix Defaults

Tag suffix map

| Tag | Suffix | |-----|--------| | input | -input | | textarea | -input | | select | -select | | checkbox | -checkbox | | datepicker | -picker |

Event tag suffix map

| Tag | Suffix | |-----|--------| | button | -btn | | a | -link | | form | -form | | div | -action | | li | -item | | td | -cell | | (other) | -action |

Duplicate Detection

When multiple elements in the same file resolve to the same identifier, the plugin automatically appends -2, -3, etc:

<button @click="handleDelete">Delete</button>  → delete-btn
<button @click="handleDelete">Delete</button>  → delete-btn-2

Manual Override

Existing data-testid attributes are always preserved. The plugin never overwrites manual annotations:

<input data-testid="my-custom-id" v-model="form.email" />
<!-- Keeps "my-custom-id", does NOT generate "email-input" -->

v-for Elements

Elements inside v-for get the same static testid. Use the index or item id to query:

// Cypress
cy.get('[data-testid="order-item--card"]').eq(2)

// Playwright
page.locator('[data-testid="order-item--card"]').nth(2)

Or override manually for unique IDs:

<OrderItem v-for="item in items" :data-testid="'order-' + item.id" />

License

MIT