Skip to content

pbassut/cypress-context-aware

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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-aware

Quick 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('user@example.com') // 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('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:

    npm install cypress-context-aware
  2. Update imports:

    // 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:

// 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

// 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

  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

About

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

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors