@nivustec/proteus
v1.0.2
Published
Shape-shifting CLI that auto-injects data-testid into React components (JS/TS).
Downloads
5
Maintainers
Readme
Proteus
Automated data-testid injection for React (JS/TS). Proteus “surfs” your codebase and transforms it on save/commit, enforcing qa_prefixed, unique, and semantic test IDs across your components.
Why Proteus?
- Consistent, predictable, and unique test IDs
- Zero-runtime: IDs are injected at build/dev time
- Works with JSX/TSX and plain JS/TS React projects
- Safe defaults, with flexible strategies
Features
- Two strategies only:
- safe-hash: compact ids like
qa_abc123; in maps:qa_abc123_${item.id} - functional: semantic ids like
qa_products_image_${item.id}
- safe-hash: compact ids like
- Uniqueness enforcement with sibling-indexing for repeated tags
- Map-awareness: automatically appends key/index for dynamic lists
- Watch mode (inject on save)
- Pre-commit integration (inject before commit)
- Default strategy: functional
- Default include: React frontend files only (
src/**/*.tsx,src/**/*.jsx)
Installation
npm i -D @nivustec/proteus
# or
yarn add -D @nivustec/proteusFor local development of Proteus itself:
# in the Proteus repo
npm run build
npm link
# in your app repo
npm link @nivustec/proteusQuick Start
- Initialize a config in your project root:
proteus init- Inject once:
proteus inject
# or specific files
proteus inject src/components/Button.tsx- Watch mode (auto-inject on save):
proteus inject --watch- Git pre-commit (Husky optional):
proteus setup # adds a pre-commit hook
git commit -m "feat: ..." # Proteus injects before commitConfiguration
Create or edit proteus.config.json in your project root.
{
"injectOnCommit": true,
"injectOnSave": true,
"include": ["src/**/*.tsx", "src/**/*.jsx"],
"exclude": ["node_modules/**", "dist/**"],
"strategy": "functional",
"verbose": false,
"detectReusableComponents": true,
"autoExcludePatterns": ["**/ui/**", "**/common/**"]
}- injectOnCommit: enable pre-commit injection
- injectOnSave: enable watch mode injection
- include/exclude: glob patterns for files
- strategy:
safe-hashorfunctional - verbose: enable additional logs in CLI mode
- detectReusableComponents: auto-detect and skip reusable UI components (default:
true) - autoExcludePatterns: glob patterns to auto-exclude (e.g.,
["**/ui/**", "**/common/**"])
🆕 Reusable Components Detection (v1.1.0)
Proteus now automatically detects reusable components (like Button, Input, Card from UI libraries) and skips injecting data-testid directly in them. Instead, it injects at the usage sites, ensuring unique IDs for each instance.
Detection criteria:
- File is in a UI/common/shared folder (e.g.,
/ui/,/common/,/shared/) - Uses
React.forwardReforforwardRef<> - Has
{...props}spread operator
Example:
// src/components/ui/button.tsx (auto-skipped by Proteus)
const Button = React.forwardRef(({ ...props }, ref) => {
return <button {...props} ref={ref} />; // ✅ No fixed data-testid
});
// src/components/Hero.tsx (Proteus injects here)
<Button onClick={handleClick} data-testid="qa_hero_button_1_abc123">
Get Started
</Button>
<Button variant="outline" data-testid="qa_hero_button_2_def456">
View on GitHub
</Button>Benefits:
- ✅ No duplicate data-testid across multiple Button instances
- ✅ Each usage gets a unique, contextual ID
- ✅ Works with any UI library (shadcn, MUI, Ant Design, etc.)
- ✅ Zero configuration required (auto-detection enabled by default)
Strategies
functional (semantic, recommended)
Rules (simplified):
- Prefix:
qa_ - Base:
qa_<component>_<role>_[<siblingIndex>]_[<descriptor>] - Dynamic lists (map): append key/index as the last segment
Examples:
// Map wrapper element
<div className="item" key={item.id} data-testid={`qa_products_item_${item.id}`} />
// Image inside map
<img data-testid={`qa_products_image_${item.id}`} />
// Text containers inside map
<div data-testid={`qa_products_container_name_${item.id}`}>{item.name}</div>
<div data-testid={`qa_products_container_price_${item.id}`}>{formatCurrency(item.price)}</div>
// Duplicate siblings get indexed
<h1 data-testid={`qa_products_h1_1_${item.id}`}>One</h1>
<h1 data-testid={`qa_products_h1_2_${item.id}`}>Two</h1>
// Outside map, unique suffix may be added to avoid collisions
<div className="header" data-testid="qa_products_header_<uniq>" />Before/After (functional)
// Before
export function ProductCard({ item }) {
return (
<div className="card">
<img alt={item.name} />
<h2>{item.name}</h2>
<button className="add-to-cart">Add</button>
</div>
);
}
// After (auto-injected)
export function ProductCard({ item }) {
return (
<div className="card" data-testid="qa_productcard_container_<uniq>">
<img alt={item.name} data-testid="qa_productcard_image_img-item-name_<uniq>" />
<h2 data-testid="qa_productcard_container_1_item-name_<uniq>">{item.name}</h2>
<button className="add-to-cart" data-testid="qa_productcard_button_add-to-cart_<uniq>">Add</button>
</div>
);
}safe-hash (compact)
Rules:
- Prefix:
qa_ - Base:
qa_<hash>(stable per location) - In maps: append key/index:
qa_<hash>_${item.id}
Examples:
// Single element
<div data-testid="qa_ab12cd" />
// Inside map
<div data-testid={`qa_ab12cd_${item.id}`} />CLI
proteus inject [files...] [options]
Options:
-c, --config <path> Path to config file
--include <paths> Files to include
--exclude <paths> Files to exclude
--strategy <type> safe-hash | functional
--verbose Enable verbose logs
--json Enable machine-readable JSON output
-w, --watch Watch mode for development
proteus pre-commit # injects on staged files (used by Husky)
proteus setup # configures Husky pre-commit hook
proteus init # writes default proteus.config.jsonHusky (Git hooks):
Modern Husky (v7+): if
.husky/exists,proteus setupwrites.husky/pre-commitwithproteus pre-commit.Legacy fallback: if
.husky/is not detected,proteus setupconfigurespackage.jsonscripts/hooks.Pre-commit processes only staged React files (
*.jsx,*.tsx).Watch mode observes the configured include patterns; defaults to
src/**/*.tsxandsrc/**/*.jsx.
Agent-Friendly CLI (MCP Support)
Proteus can be used by AI agents and other automated systems via its machine-readable JSON output. Use the --json flag with the inject command to suppress human-readable logs and receive a structured JSON response.
Usage with --json
proteus inject --jsonExample JSON Output
{
"filesProcessed": 5,
"totalInjected": 12,
"errors": 0,
"errorMessages": []
}If errors occur, they will be included in the errorMessages array:
{
"filesProcessed": 1,
"totalInjected": 0,
"errors": 1,
"errorMessages": [
"Error processing src/components/BrokenComponent.tsx: Parser Error: Unexpected token (1:10)"
]
}Universal Agent (Tool Calling) Support
To enable any AI agent (Gemini, OpenAI, Anthropic, etc.) to interact with Proteus, we provide a universal JSON Schema for the proteus_inject_tool and a dedicated executor function. This approach removes any direct dependency on specific AI SDKs within the Proteus CLI itself.
1. Universal Contract (JSON Schema)
The proteus_inject_tool's function declaration is available as a pure JSON Schema file at the root of the project:
proteus-tool-schema.json: This file defines the structure and parameters of theproteus_inject_toolin a standard Open API/JSON Schema format, allowing any agent to understand how to call the tool.
2. Universal Executor
The executeProteusCLI function provides a clean API for executing the Proteus CLI and receiving its output in a machine-readable format.
src/tool-executor.ts: This module exports theexecuteProteusCLIfunction.
Example Consumer Agent Usage
Here's how a consumer agent (using any AI SDK) would typically integrate with Proteus:
- Load the JSON Schema: The agent loads
proteus-tool-schema.jsonto understand the tool's capabilities. - Model Interaction: The agent's model receives a user prompt and, based on the loaded schema, decides to call
proteus_inject_toolwith specific arguments. - Execute the Tool: The agent then calls the
executeProteusCLIfunction fromsrc/tool-executor.tswith the arguments provided by its model. - Process Output: The agent receives the JSON output from
executeProteusCLIand can then use its model to generate a natural language response for the user.
// Example of a consumer agent (conceptual, using a generic AI SDK)
import { readFileSync } from 'fs';
import { executeProteusCLI } from './src/tool-executor.js'; // Adjust path as needed
// 1. Load the Proteus tool schema
const proteusToolSchema = JSON.parse(readFileSync('proteus-tool-schema.json', 'utf-8'));
// Assume your AI SDK has a way to register tools and get model responses
async function runConsumerAgent(userPrompt: string) {
// --- Conceptual: AI Model decides to call the tool ---
// In a real scenario, your AI SDK would handle this based on the userPrompt
// and the registered proteusToolSchema.
// For demonstration, we'll simulate a tool call from the model.
const simulatedToolCallArgs = {
files: ['src/components/MyComponent.tsx'],
strategy: 'functional',
json_output: true, // Always true for tool calling
};
console.log(`Agent: Simulating tool call to proteus_inject_tool with args:`, simulatedToolCallArgs);
try {
// 2. Execute the Proteus CLI via the universal executor
const toolOutput = await executeProteusCLI(simulatedToolCallArgs);
console.log(`Agent: Proteus CLI returned:`, toolOutput);
// --- Conceptual: AI Model processes tool output and generates response ---
// Your AI model would take toolOutput and generate a natural language response.
if (toolOutput.errors > 0) {
console.log(`Agent: I encountered ${toolOutput.errors} errors while injecting test IDs. Please check the logs.`);
} else {
console.log(`Agent: Successfully injected ${toolOutput.totalInjected} test IDs into ${toolOutput.filesProcessed} files.`);
}
} catch (error) {
console.error("Agent: Error executing Proteus CLI:", error);
}
}
// Example usage
runConsumerAgent("Inject test IDs into src/components/MyComponent.tsx using the functional strategy.");Best Practices
- Commit generated IDs; they are part of your source (zero runtime)
- Choose one strategy per repo; we recommend
functional - Ensure
qa_prefix uniqueness across layers and components - Always append a key/index for dynamic maps
Troubleshooting
- npx proteus not found / CLI not running
- Ensure the package is installed in your project
- For local dev: build +
npm linkin Proteus repo, thennpm link @nivustec/proteusin your app
- Duplicate logs in watch mode
- Proteus debounces its own writes; if you still see duplicates, increase the debounce window in
watcher.ts
- Pre-commit too verbose
- Set
verbose: false(default). Pre-commit runs in silent mode by design.
- Pre-commit not injecting on non-React files
- By default, only
*.jsxand*.tsxstaged files are processed. Adjustincludeif you need other patterns.
- IDs are inserted in the wrong place
- We use AST positions; if you find an edge-case, open an issue with a code sample.
- Partial staging lost in pre-commit
- Current hook re-stages full files after injection. If you rely on partial staging (
git add -p), consider disablinginjectOnCommitand runningproteus injectmanually.
License
MIT © Nivustec
