swc-plugin-react-data-testid
v0.1.2
Published
SWC plugin to automatically generate data-testid attributes for React components
Maintainers
Readme
swc-plugin-react-data-testid
Automatically adds
data-testidattributes to your React components during the build process, making it easier to write reliable end-to-end tests. Features component-scoped unique counters for predictable, collision-free test IDs.
Installation
npm install --save-dev swc-plugin-react-data-testid
# or
yarn add -D swc-plugin-react-data-testidBasic Setup
Add the plugin to your .swcrc:
{
"jsc": {
"experimental": {
"plugins": [["swc-plugin-react-data-testid", {}]]
}
}
}Framework-Specific Setup
Configure in next.config.js:
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
swcPlugins: [["swc-plugin-react-data-testid", {}]],
},
};
module.exports = nextConfig;Configure in vite.config.js:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
export default defineConfig({
plugins: [
react({
plugins: [["swc-plugin-react-data-testid", {}]],
}),
],
});Configure in jest.config.js:
module.exports = {
transform: {
"^.+\\.[jt]sx?$": [
"@swc/jest",
{
jsc: {
experimental: {
plugins: [["swc-plugin-react-data-testid", {}]],
},
},
},
],
},
};Configure in vite.config.js:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
export default defineConfig({
plugins: [
react({
plugins: [["swc-plugin-react-data-testid", {}]],
}),
],
test: {
environment: "jsdom",
globals: true,
setupFiles: ["./vitest.setup.js"],
},
});Create vitest.setup.js:
import "@testing-library/jest-dom/vitest";With Custom Attributes
{
"jsc": {
"experimental": {
"plugins": [
[
"swc-plugin-react-data-testid",
{ "attributes": ["data-testid", "data-cy"] }
]
]
}
}
}SWC Version Compatibility
SWC plugins are compiled against a specific swc_core version and must match what your consumer uses. This plugin targets swc_core 65.x. Each SWC minor release may require a new plugin version.
Check compatibility before pinning a version: plugins.swc.rs
Features
- Automatic data-testid generation for functional and class components
- Component-scoped unique counters to prevent ID conflicts
- Predictable naming like
ComponentName.element,ComponentName.element2 - Full React support: Functional components, arrow functions, class components
- Customizable attributes (data-testid, data-cy, data-test-id, etc.)
- Zero configuration - works out of the box
- Anonymous default exports — derives component name from filename
- JSX member expressions support (
Modal.Header→ComponentName.Header) - Never overrides existing attributes
Transformations
Functional Components
Before:
function UserCard({ name }) {
return (
<div>
<h3>{name}</h3>
<button>Follow</button>
<button>Message</button>
</div>
);
}After:
function UserCard({ name }) {
return (
<div data-testid="UserCard.div">
<h3 data-testid="UserCard.h3">{name}</h3>
<button data-testid="UserCard.button">Follow</button>
<button data-testid="UserCard.button2">Message</button>
</div>
);
}Unique Counter System
The plugin uses component-scoped counters to ensure uniqueness:
function FormComponent() {
return (
<div>
{" "}
{/* FormComponent.div */}
<div>First</div> {/* FormComponent.div2 */}
<div>Second</div> {/* FormComponent.div3 */}
<button>Save</button> {/* FormComponent.button */}
<button>Cancel</button> {/* FormComponent.button2 */}
</div>
);
}JSX Member Expressions
function ModalComponent() {
return (
<Modal.Container>
{" "}
{/* ModalComponent.Container */}
<Modal.Header>Title</Modal.Header> {/* ModalComponent.Header */}
<Modal.Body>Content</Modal.Body> {/* ModalComponent.Body */}
<Modal.Header>Second</Modal.Header> {/* ModalComponent.Header2 */}
</Modal.Container>
);
}Class Components
class TodoList extends React.Component {
render() {
return (
<div>
{" "}
{/* TodoList.div */}
<h2>My Todos</h2> {/* TodoList.h2 */}
<ul>
{" "}
{/* TodoList.ul */}
<li>Todo 1</li> {/* TodoList.li */}
<li>Todo 2</li> {/* TodoList.li2 */}
</ul>
<button>Add Todo</button> {/* TodoList.button */}
</div>
);
}
}Anonymous Default Exports
When there is no explicit component name, the plugin derives it from the filename:
// File: UserProfile.tsx
export default () => (
<div data-testid="UserProfile.div">
<span data-testid="UserProfile.span">Hello</span>
</div>
);Configuration Options
| Option | Type | Default | Description |
| ------------ | ---------- | ----------------- | ------------------------------------------- |
| attributes | string[] | ["data-testid"] | Array of attribute names to add to elements |
Examples
Multiple testing frameworks:
{
"jsc": {
"experimental": {
"plugins": [
[
"swc-plugin-react-data-testid",
{ "attributes": ["data-testid", "data-cy", "data-test-id"] }
]
]
}
}
}Cypress only:
{
"jsc": {
"experimental": {
"plugins": [
["swc-plugin-react-data-testid", { "attributes": ["data-cy"] }]
]
}
}
}Disable plugin:
{
"jsc": {
"experimental": {
"plugins": [["swc-plugin-react-data-testid", { "attributes": [] }]]
}
}
}Testing Integration
Jest + React Testing Library
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import UserCard from "./UserCard";
test("should interact with generated test ids", async () => {
render(<UserCard name="John" />);
const followButton = screen.getByTestId("UserCard.button");
const messageButton = screen.getByTestId("UserCard.button2");
await userEvent.click(followButton);
expect(screen.getByTestId("UserCard.div")).toBeInTheDocument();
});Cypress
describe("UserCard Component", () => {
it("should interact with elements", () => {
cy.mount(<UserCard name="John" />);
cy.get('[data-cy="UserCard.button"]').click();
cy.get('[data-cy="UserCard.button2"]').should("be.visible");
});
});Playwright
import { test, expect } from "@playwright/test";
test("user card interactions", async ({ page }) => {
await page.goto("/user-profile");
await page.locator('[data-testid="UserCard.button"]').click();
await expect(page.locator('[data-testid="UserCard.div"]')).toBeVisible();
});Supported React Patterns
| Pattern | Supported | Example |
| ---------------------- | --------- | ------------------------------------------------------ |
| Function Components | ✅ | function MyComponent() {} |
| Arrow Functions | ✅ | const MyComponent = () => {} |
| Class Components | ✅ | class MyComponent extends React.Component {} |
| Anonymous Exports | ✅ | export default () => {} (name derived from filename) |
| JSX Member Expressions | ✅ | <Modal.Header> → ComponentName.Header |
| Fragments | ✅ | <> and <React.Fragment> |
| Conditional Rendering | ✅ | Multiple return statements |
| Existing Attributes | ✅ | Never overrides existing data-testid |
| Self-Closing Elements | ✅ | <img />, <input /> |
| Nested Components | ✅ | Deep nesting with unique counters |
Example Applications
Next.js
A complete Next.js example is available in example/nextjs-swc/:
cd example/nextjs-swc
npm install
npm run devOpen http://localhost:3000 and inspect the DOM to see the automatically generated data-testid attributes.
Jest (@swc/jest)
A Jest example is available in example/jest/. It runs @testing-library/react tests that assert the plugin actually injected the expected data-testid attributes:
cd example/jest
npm install
npm testVitest
A Vitest example is available in example/vitest/. Same assertions as the Jest example, using @vitejs/plugin-react-swc to load the plugin:
cd example/vitest
npm install
npm testDevelopment
Prerequisites
- Rust (stable)
wasm32-wasip1target:rustup target add wasm32-wasip1
Commands
cargo test # Run fixture tests
UPDATE=1 cargo test # Regenerate fixture outputs
cargo build --target wasm32-wasip1 --release # Build WASM binary
npm run build # Build WASM and copy to plugin.wasmTest Structure
Tests use Rust fixture tests in tests/fixture.rs. Each fixture is a directory under tests/fixture*/ with an input.tsx and output.tsx. Run UPDATE=1 cargo test to regenerate outputs after algorithm changes.
Test Coverage
Current coverage: 96.78% line coverage across 91 tests (26 unit tests + 65 fixture tests).
To measure coverage locally:
cargo install cargo-llvm-cov
cargo llvm-covWhy 100% is not achievable
The remaining ~5% is the process_transform function — the #[plugin_transform] WASM entry point that SWC calls at runtime. It requires TransformPluginProgramMetadata, a type provided by the SWC WASM runtime that cannot be constructed in a Rust unit test. This function is intentionally thin (it delegates immediately to parse_options and ReactDataTestIdTransform, both of which are fully tested). Covering it would require a full WASM integration test outside of cargo test.
Contributors
Thanks to these wonderful people who have contributed to this project:
How to Contribute
- Report bugs by opening an issue
- Suggest features or improvements
- Improve documentation
- Add test cases (fixture-based)
- Submit pull requests
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes and add fixture tests
- Ensure all tests pass (
cargo test) - Commit your changes
- Push to the branch
- Open a Pull Request
License
MIT © Oakblu
Made with love for better testing
