cypress-context-aware
Version:
A context-aware command system for Cypress that enables component-based test interactions
364 lines (259 loc) • 9.18 kB
Markdown
# 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](#installation)
- [Quick Start](#quick-start)
- [Core Concepts](#core-concepts)
- [API Reference](#api-reference)
- [Examples](#examples)
- [Migration Guide](#migration-guide)
- [Contributing](#contributing)
## Installation
```bash
npm install cypress-context-aware
```
## Quick Start
```javascript
// 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:
```javascript
// 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.
```javascript
// 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.
```javascript
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
```javascript
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
```javascript
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('user@example.com') // Automatically waits 500ms
.table().rows().should('have.length', 1);
```
### Form Component
```javascript
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('user@example.com')
.field('password').type('secret123')
.shouldBeValid()
.submit();
```
## Migration Guide
### From Inline Implementation
If you're migrating from an inline context-aware implementation:
1. **Install the package**:
```bash
npm install cypress-context-aware
```
2. **Update imports**:
```javascript
// Before
import { ChainContext } from '../context-aware';
// After
import { ChainContext } from 'cypress-context-aware';
```
3. **Remove inline files**:
- Delete your local `context-aware.js` file
- Update your support file imports
4. **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:
```javascript
// 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: someContext
```
### Command Chain Introspection
```javascript
// 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
```javascript
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
1. **Root Commands**: Always make your root command return a chainable Cypress element
2. **Subject Handling**: Use the `s()` helper for consistent subject handling
3. **Naming**: Use descriptive names that reflect the component structure
4. **Validation**: Add custom validation for command parameters when needed
5. **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**