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.
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();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);
});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);// 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();
}
});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;
}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:
// 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);
});