UNPKG

cypress-context-aware

Version:

A context-aware command system for Cypress that enables component-based test interactions

364 lines (259 loc) 9.18 kB
# 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**