cypress-context-aware
v1.0.0
Published
A context-aware command system for Cypress that enables component-based test interactions
Maintainers
Readme
Cypress Context-Aware Commands
A powerful context-aware command system for Cypress that enables component-based test interactions with automatic command scoping and validation.
Table of Contents
Installation
npm install cypress-context-awareQuick Start
// cypress/support/commands.js
import { ChainContext } from 'cypress-context-aware';
// Define your component commands
const Modal = {
// Root command that establishes context
modal(_, { waitForInteractive = true } = {}) {
const modal = () => cy.get('[data-testid="modal"]');
if (waitForInteractive) {
modal().should('be.visible');
}
return modal();
},
// Context-aware commands
header($subject) {
return $subject.find('[data-testid="modal-header"]');
},
body($subject) {
return $subject.find('[data-testid="modal-body"]');
},
footer($subject) {
return $subject.find('[data-testid="modal-footer"]');
}
};
// Register the commands
ChainContext.register('modal', Modal, { prevSubject: 'optional' });Now you can use context-aware commands in your tests:
// These commands are scoped to the modal context
cy.modal()
.header().should('contain', 'Welcome')
.body().should('be.visible')
.footer().find('button').click();Core Concepts
Context-Aware Commands
Context-aware commands automatically understand their execution context based on the command chain. This enables:
- Automatic Scoping: Commands know which component they're operating within
- Command Validation: Prevents invalid command combinations
- Custom Behavior: Commands can behave differently based on context
- Better Error Messages: Clear errors when commands are used incorrectly
Root Commands vs Child Commands
- Root Commands: Establish a new context (e.g.,
modal(),table()) - Child Commands: Operate within an established context (e.g.,
header(),body())
Command Chain Tracking
The system automatically tracks the command chain using Cypress internals, eliminating the need for manual state management.
API Reference
ChainContext
The main export that provides the context-aware functionality.
ChainContext.register(name, commands, options)
Registers a set of context-aware commands.
- name (string): The root command name
- commands (object): Object containing command implementations
- options (object): Cypress command options (e.g.,
{ prevSubject: 'optional' })
ChainContext.preceedsCommand(commandName)
Checks if a specific command appears earlier in the current command chain.
// Example: Add debouncing when typing in search
type(originalFn, $subject, text, options) {
if (ChainContext.preceedsCommand('search')) {
originalFn($subject, text, options);
return cy.wait(300); // Debounce search
}
return originalFn($subject, text, options);
}ChainContext.rootCommand(command)
Returns the root command name for a given command in the chain.
ChainContext.validateCommand(commandName, currentCommand)
Validates that a command is allowed in the current context. Throws an error if invalid.
Helper Functions
s(func, defaultRoot)
A helper function for creating commands with optional subject handling.
import { s } from 'cypress-context-aware';
Cypress.Commands.add('customCommand',
{ prevSubject: 'optional' },
s(($subject, arg1, arg2) => {
// $subject will be cy.root() if no subject provided
return $subject.find('.something');
})
);Examples
Modal Component
import { ChainContext } from 'cypress-context-aware';
const Modal = {
modal(_, { waitForInteractive = true } = {}) {
const modal = () => cy.get('[data-testid="modal"]');
if (waitForInteractive) {
modal().should('be.visible');
}
return modal();
},
header($subject) {
return $subject.find('[data-testid="modal-header"]');
},
body($subject) {
return $subject.find('[data-testid="modal-body"]');
},
footer($subject) {
return $subject.find('[data-testid="modal-footer"]');
},
close($subject) {
return $subject.find('[aria-label="Close"]').click();
}
};
ChainContext.register('modal', Modal, { prevSubject: 'optional' });
// Usage
cy.modal()
.header().should('contain', 'Confirmation')
.body().should('contain', 'Are you sure?')
.footer().contains('button', 'OK').click();
cy.modal().close();Table Component with Custom Type Behavior
import { ChainContext } from 'cypress-context-aware';
const Table = {
table($subject = cy.root()) {
return $subject.find('[data-testid="table"]');
},
search($subject) {
return $subject.find('[data-testid="search-input"]');
},
rows($subject) {
return $subject.find('tbody tr');
},
// Custom behavior for type command within search context
type(originalFn, $subject, text, options = {}) {
if (ChainContext.preceedsCommand('search')) {
originalFn($subject, text, options);
// Auto-debounce search queries
return cy.wait(500);
}
return originalFn($subject, text, options);
}
};
ChainContext.register('table', Table, { prevSubject: 'optional' });
// Usage - typing in search will automatically debounce
cy.table()
.search().type('[email protected]') // Automatically waits 500ms
.table().rows().should('have.length', 1);Form Component
const Form = {
form($subject = cy.root()) {
return $subject.find('form');
},
field($subject, name) {
return $subject.find(`[name="${name}"]`);
},
submit($subject) {
return $subject.find('[type="submit"]').click();
},
// Custom validation
shouldBeValid($subject) {
return $subject.should('not.have.class', 'error');
}
};
ChainContext.register('form', Form, { prevSubject: 'optional' });
// Usage
cy.form()
.field('email').type('[email protected]')
.field('password').type('secret123')
.shouldBeValid()
.submit();Migration Guide
From Inline Implementation
If you're migrating from an inline context-aware implementation:
Install the package:
npm install cypress-context-awareUpdate imports:
// Before import { ChainContext } from '../context-aware'; // After import { ChainContext } from 'cypress-context-aware';Remove inline files:
- Delete your local
context-aware.jsfile - Update your support file imports
- Delete your local
Keep your component definitions:
- Your existing component command objects remain unchanged
- Only the import and registration system changes
Breaking Changes
- None for v1.0.0 - this is the initial stable release
Advanced Usage
Error Handling and Validation
The system provides clear error messages when commands are used incorrectly:
// This will throw a clear error if 'header' is not available in current context
cy.someContext().header(); // Error: Command 'header' is not available in current root command: someContextCommand Chain Introspection
// Check what commands precede the current command
if (ChainContext.preceedsCommand('search')) {
// We're in a search context
}
// Get the root command for current context
const root = ChainContext.rootCommand(cy.state('current'));Custom Command Validation
const MyComponent = {
root() {
return cy.get('.my-component');
},
action($subject, actionType) {
// Validate the action type
if (!['create', 'edit', 'delete'].includes(actionType)) {
throw new Error(`Invalid action type: ${actionType}`);
}
return $subject.find(`[data-action="${actionType}"]`).click();
}
};Best Practices
- Root Commands: Always make your root command return a chainable Cypress element
- Subject Handling: Use the
s()helper for consistent subject handling - Naming: Use descriptive names that reflect the component structure
- Validation: Add custom validation for command parameters when needed
- Documentation: Document expected DOM structure and dependencies
Troubleshooting
Common Issues
Q: Commands not working after registration
A: Ensure you're importing the commands in your cypress/support/commands.js file.
Q: "Command not available in context" errors A: Check that you're calling child commands after establishing the root context.
Q: Type/clear commands not working as expected A: These commands have special handling due to Cypress internals. The system automatically handles this.
Contributing
Contributions are welcome! Please read our contributing guidelines and submit pull requests to the main repository.
License
MIT License - see LICENSE file for details.
Made with ❤️ for the Cypress testing community
