or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

configuration.mdcore-testing.mdindex.mdplugins-frames.mdrules-reporting.mdutilities.md
tile.json

plugins-frames.mddocs/

Plugins and Frames

Plugin system and frame communication for extending axe-core functionality and testing across iframe boundaries. This includes registering custom plugins, managing frame communication, and handling cross-origin testing scenarios.

Capabilities

Plugin System

Functions for registering and managing plugins that extend axe-core functionality.

/**
 * Register a plugin configuration in document and its subframes
 * @param plugin - Plugin configuration object
 */
function registerPlugin(plugin: AxePlugin): void;

/**
 * Clean up plugin configuration in document and its subframes
 */
function cleanup(): void;

Usage Examples:

// Register a simple plugin
axe.registerPlugin({
  id: 'my-plugin',
  run: function(context, options, resolve, reject) {
    // Custom plugin logic
    const results = [];
    // ... analyze elements
    resolve(results);
  },
  commands: [{
    id: 'custom-command',
    callback: function(data, callback) {
      // Handle custom command
      callback({ success: true });
    }
  }],
  cleanup: function(resolve) {
    // Cleanup plugin resources
    resolve();
  }
});

// Register plugin with multiple commands
axe.registerPlugin({
  id: 'advanced-plugin',
  run: function(context, options, resolve, reject) {
    try {
      // Plugin implementation
      const customResults = this.analyzeElements(context);
      resolve(customResults);
    } catch (error) {
      reject(error);
    }
  },
  commands: [
    {
      id: 'validate',
      callback: function(data, callback) {
        const isValid = this.validateData(data);
        callback({ valid: isValid });
      }
    },
    {
      id: 'transform',
      callback: function(data, callback) {
        const transformed = this.transformData(data);
        callback(transformed);
      }
    }
  ],
  analyzeElements: function(context) {
    // Custom analysis logic
    return [];
  },
  validateData: function(data) {
    return data && typeof data === 'object';
  },
  transformData: function(data) {
    return { ...data, processed: true };
  },
  cleanup: function(resolve) {
    // Clean up any resources
    this.cache = null;
    this.listeners = [];
    resolve();
  }
});

// Later, clean up all plugins
axe.cleanup();

Frame Communication

Setup alternative frame communication for cross-origin iframe testing.

/**
 * Set up alternative frame communication
 * @param frameMessenger - Custom frame messenger implementation
 */
function frameMessenger(frameMessenger: FrameMessenger): void;

Usage Examples:

// Implement custom frame messenger for cross-origin scenarios
const customFrameMessenger = {
  open: function(topicHandler) {
    // Set up communication channel
    window.addEventListener('message', function(event) {
      if (event.data && event.data.topic) {
        const responder = function(message, keepalive, replyHandler) {
          event.source.postMessage({
            channelId: event.data.channelId,
            message: message,
            keepalive: keepalive || false
          }, event.origin);
        };
        topicHandler(event.data, responder);
      }
    });

    // Return cleanup function
    return function() {
      // Cleanup listeners
    };
  },
  
  post: function(frameWindow, data, replyHandler) {
    if (!frameWindow) return false;
    
    try {
      frameWindow.postMessage(data, '*');
      
      // Set up reply listener
      const handleReply = function(event) {
        if (event.data && event.data.channelId === data.channelId) {
          window.removeEventListener('message', handleReply);
          
          const responder = function(message, keepalive, nextReplyHandler) {
            // Handle follow-up communications
          };
          
          replyHandler(event.data.message, event.data.keepalive, responder);
        }
      };
      
      window.addEventListener('message', handleReply);
      return true;
    } catch (error) {
      console.error('Failed to post message:', error);
      return false;
    }
  }
};

// Register the custom frame messenger
axe.frameMessenger(customFrameMessenger);

// Now run tests that will use custom frame communication
axe.run(document, { iframes: true }).then(results => {
  console.log('Cross-frame results:', results);
});

Advanced Frame Communication

For complex applications with multiple nested frames:

// Advanced frame messenger with error handling and timeouts
const advancedFrameMessenger = {
  open: function(topicHandler) {
    const channelId = axe.utils.uuid();
    const messageHandler = function(event) {
      if (event.data && 
          event.data.topic && 
          event.data.channelId === channelId) {
        
        const responder = function(message, keepalive, replyHandler) {
          if (event.source) {
            event.source.postMessage({
              channelId: event.data.channelId,
              message: message,
              keepalive: keepalive || false,
              timestamp: Date.now()
            }, event.origin);
          }
        };
        
        try {
          topicHandler(event.data, responder);
        } catch (error) {
          console.error('Topic handler error:', error);
          responder({ error: error.message }, false);
        }
      }
    };
    
    window.addEventListener('message', messageHandler);
    
    return function cleanup() {
      window.removeEventListener('message', messageHandler);
    };
  },
  
  post: function(frameWindow, data, replyHandler) {
    if (!frameWindow || !frameWindow.postMessage) {
      return false;
    }
    
    const timeout = 5000; // 5 second timeout
    const channelId = data.channelId || axe.utils.uuid();
    const messageData = { ...data, channelId, timestamp: Date.now() };
    
    try {
      frameWindow.postMessage(messageData, '*');
      
      const timeoutId = setTimeout(() => {
        window.removeEventListener('message', replyListener);
        replyHandler(new Error('Frame communication timeout'), false);
      }, timeout);
      
      const replyListener = function(event) {
        if (event.data && 
            event.data.channelId === channelId &&
            event.source === frameWindow) {
          
          clearTimeout(timeoutId);
          window.removeEventListener('message', replyListener);
          
          const responder = function(message, keepalive, nextReplyHandler) {
            if (keepalive && nextReplyHandler) {
              // Set up for continued communication
              const continueData = { 
                channelId: channelId, 
                message: message,
                keepalive: true
              };
              return advancedFrameMessenger.post(frameWindow, continueData, nextReplyHandler);
            }
          };
          
          replyHandler(event.data.message, event.data.keepalive, responder);
        }
      };
      
      window.addEventListener('message', replyListener);
      return true;
    } catch (error) {
      console.error('Frame post error:', error);
      return false;
    }
  }
};

axe.frameMessenger(advancedFrameMessenger);

Plugin Development Best Practices

// Example of a well-structured plugin
axe.registerPlugin({
  id: 'accessibility-checker',
  
  // Main plugin execution
  run: function(context, options, resolve, reject) {
    try {
      const results = [];
      const elements = this.findElements(context);
      
      elements.forEach(element => {
        const analysis = this.analyzeElement(element, options);
        if (analysis.hasIssues) {
          results.push(analysis);
        }
      });
      
      resolve(results);
    } catch (error) {
      reject(error);
    }
  },
  
  // Available commands
  commands: [
    {
      id: 'find-elements',
      callback: function(data, callback) {
        const elements = this.findElements(data.context);
        callback({ elements: elements.map(el => el.tagName) });
      }
    },
    {
      id: 'get-metrics',
      callback: function(data, callback) {
        callback({
          version: this.version,
          rulesCount: this.customRules.length,
          lastRun: this.lastRun
        });
      }
    }
  ],
  
  // Plugin data
  version: '1.0.0',
  customRules: [],
  lastRun: null,
  
  // Helper methods
  findElements: function(context) {
    return Array.from(context.querySelectorAll('[role], [aria-*]'));
  },
  
  analyzeElement: function(element, options) {
    // Custom analysis logic
    return {
      element: element,
      hasIssues: false,
      issues: []
    };
  },
  
  // Cleanup
  cleanup: function(resolve) {
    this.customRules = [];
    this.lastRun = null;
    resolve();
  }
});

Types

interface AxePlugin {
  id: string;
  run(...args: any[]): any;
  commands: PluginCommand[];
  cleanup?(callback: Function): void;
}

interface PluginCommand {
  id: string;
  callback(...args: any[]): void;
}

interface FrameMessenger {
  open: (topicHandler: TopicHandler) => Close | void;
  post: (
    frameWindow: Window,
    data: TopicData,
    replyHandler: ReplyHandler
  ) => boolean | void;
}

type Close = Function;

type TopicHandler = (data: TopicData, responder: Responder) => void;

type ReplyHandler = (
  message: any | Error,
  keepalive: boolean,
  responder: Responder
) => void;

type Responder = (
  message: any | Error,
  keepalive?: boolean,
  replyHandler?: ReplyHandler
) => void;

interface TopicData {
  topic: string;
  channelId: string;
  message: any;
  keepalive: boolean;
  timestamp?: number;
}

interface ReplyData {
  channelId: string;
  message: any;
  keepalive: boolean;
  timestamp?: number;
}

Cross-Origin Frame Testing

When testing applications with cross-origin iframes, the default frame communication may not work due to browser security restrictions. Custom frame messengers enable testing across these boundaries:

Common Cross-Origin Scenarios

  1. Third-party widgets: Embedded maps, videos, or social media widgets
  2. Sandboxed iframes: Iframes with restricted permissions
  3. Different subdomain iframes: Testing across subdomain boundaries
  4. CDN-hosted content: Content served from different domains

Frame Messenger Implementation Tips

  1. Security: Always validate message origins and content
  2. Timeouts: Implement timeouts to handle unresponsive frames
  3. Error Handling: Gracefully handle communication failures
  4. Cleanup: Properly remove event listeners to prevent memory leaks
  5. Unique IDs: Use unique channel IDs to avoid message collision

Testing with Frames

// Test specific to iframe content
axe.run(document, {
  iframes: true,
  frameWaitTime: 10000 // Wait up to 10 seconds for frames
}).then(results => {
  // Results include both main page and iframe violations
  console.log('Main page violations:', results.violations.filter(v => 
    !v.nodes.some(n => n.target.length > 1)
  ));
  
  console.log('Iframe violations:', results.violations.filter(v => 
    v.nodes.some(n => n.target.length > 1)
  ));
});

// Run without iframe testing if needed
axe.run(document, {
  iframes: false
}).then(results => {
  console.log('Main page only results:', results);
});