Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions includes/classes/class-enqueue-frontend.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,17 @@ public static function maybe_enqueue_frontend_highlighter() {
'edac-frontend-highlighter-app',
'edacFrontendHighlighterApp',
[
'postID' => $post_id,
'nonce' => wp_create_nonce( 'ajax-nonce' ),
'restNonce' => wp_create_nonce( 'wp_rest' ),
'userCanFix' => current_user_can( apply_filters( 'edac_filter_settings_capability', 'manage_options' ) ),
'edacUrl' => esc_url_raw( get_site_url() ),
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'loggedIn' => is_user_logged_in(),
'appCssUrl' => EDAC_PLUGIN_URL . 'build/css/frontendHighlighterApp.css?ver=' . EDAC_VERSION,
'widgetPosition' => get_option( 'edac_frontend_highlighter_position', 'right' ),
'editorLink' => get_edit_post_link( $post_id ),
'postID' => $post_id,
'nonce' => wp_create_nonce( 'ajax-nonce' ),
'restNonce' => wp_create_nonce( 'wp_rest' ),
'userCanFix' => current_user_can( apply_filters( 'edac_filter_settings_capability', 'manage_options' ) ),
'edacUrl' => esc_url_raw( get_site_url() ),
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'loggedIn' => is_user_logged_in(),
'appCssUrl' => EDAC_PLUGIN_URL . 'build/css/frontendHighlighterApp.css?ver=' . EDAC_VERSION,
'widgetPosition' => get_option( 'edac_frontend_highlighter_position', 'right' ),
'editorLink' => get_edit_post_link( $post_id ),
'scannerBundleUrl' => plugin_dir_url( EDAC_PLUGIN_FILE ) . 'build/pageScanner.bundle.js',
]
Comment thread
pattonwebz marked this conversation as resolved.
);

Expand Down
129 changes: 124 additions & 5 deletions src/frontendHighlighterApp/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,15 +185,21 @@ class AccessibilityCheckerHighlight {
},
);
}
} else if ( ! self._scanAttempted && response.data && response.data[ 0 ] && response.data[ 0 ].code === -3 ) {
// Only try kickoffScan once per highlightAjax call
self._scanAttempted = true;
self.kickoffScan();
// After kickoffScan, try highlightAjax again, but only once
setTimeout( () => {
self.highlightAjax().then( resolve ).catch( reject );
}, 5000 ); // Wait 5s for scan to complete.
Comment thread
pattonwebz marked this conversation as resolved.
Outdated
} else {
resolve( [] );
//console.log(response);
// Default: resolve with empty issues/fixes
resolve( { issues: [], fixes: [] } );
}
} else {
self.showWait( false );

//console.log( 'Request failed. Returned status of ' + xhr.status );

reject( {
status: xhr.status,
statusText: xhr.statusText,
Expand Down Expand Up @@ -535,7 +541,11 @@ class AccessibilityCheckerHighlight {
}
}
).catch( ( err ) => {
//TODO:
// Output a message that says that there are no issues or that the issues could not be loaded.
const summary = document.querySelector( '.edac-highlight-panel-controls-summary' );
if ( summary ) {
summary.textContent = __( 'An error occurred when loading the issues.', 'accessibility-checker' );
}
} );
}

Expand Down Expand Up @@ -889,6 +899,115 @@ class AccessibilityCheckerHighlight {

div.textContent = textContent;
}

/**
* Kick off the accessibility scan.
*/
kickoffScan() {
const getPageDensity = () => {
const elementCount = document.body.getElementsByTagName( '*' ).length;
const contentLength = document.body.innerText.length;
return { elementCount, contentLength };
};
const densityMetrics = getPageDensity();
const self = this;
const scriptId = 'edac-accessibility-checker-scanner-script';
if ( ! document.getElementById( scriptId ) ) {
const script = document.createElement( 'script' );
script.src = window.edacFrontendHighlighterApp?.scannerBundleUrl || '/wp-content/plugins/accessibility-checker/build/pageScanner.bundle.js';
script.id = scriptId;
script.onload = function() {
setTimeout( () => {
self._runScanOrShowError( densityMetrics );
}, 100 );
};
script.onerror = function() {
self.showWait( false );
self.showScanError( 'Failed to load scanner script.' );
};
document.head.appendChild( script );
} else {
self._runScanOrShowError( densityMetrics );
}
}
Comment thread
pattonwebz marked this conversation as resolved.

_runScanOrShowError( densityMetrics ) {
if ( window.runAccessibilityScan ) {
this.runAccessibilityScanAndSave( densityMetrics );
} else {
this.showWait( false );
this.showScanError( __( 'Scanner function not found.', 'accessibility-checker' ) );
}
}

runAccessibilityScanAndSave( densityMetrics ) {
const self = this;
const summary = document.querySelector( '.edac-highlight-panel-controls-summary' );
if ( summary ) {
summary.textContent = __( 'Scanning...', 'accessibility-checker' );
summary.classList.remove( 'edac-error' );
}
window.runAccessibilityScan().then( ( result ) => {
const postId = window.edacFrontendHighlighterApp && window.edacFrontendHighlighterApp.postID;
const nonce = window.edacFrontendHighlighterApp && window.edacFrontendHighlighterApp.restNonce;
if ( ! postId || ! nonce ) {
self.showWait( false );
self.showScanError( __( 'Missing postId or nonce.', 'accessibility-checker' ) );
return;
}
if ( ! result || ! result.violations || result.violations.length === 0 ) {
self.showWait( false );
self.showScanError( __( 'No violations found, skipping save.', 'accessibility-checker' ) );
return;
}
self.saveScanResults( postId, nonce, result.violations, densityMetrics );
} ).catch( () => {
self.showWait( false );
self.showScanError( __( 'Accessibility scan error.', 'accessibility-checker' ) );
} );
}

saveScanResults( postId, nonce, violations, densityMetrics ) {
const self = this;
fetch( '/wp-json/accessibility-checker/v1/post-scan-results/' + postId, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': nonce,
},
body: JSON.stringify( {
violations,
isSkipped: false,
isFailure: false,
densityMetrics,
} ),
} )
.then( ( response ) => response.json() )
.then( ( data ) => {
self.showWait( false );
if ( data && data.success ) {
// Optionally show a success message or update UI
} else {
self.showScanError( __( 'Saving failed.', 'accessibility-checker' ) );
}
} )
.catch( () => {
self.showWait( false );
self.showScanError( __( 'Error saving scan results.', 'accessibility-checker' ) );
} );
}
Comment thread
pattonwebz marked this conversation as resolved.

/**
* Show an error message in the scan panel or as an alert fallback.
* @param {string} message
*/
showScanError( message ) {
const summary = document.querySelector( '.edac-highlight-panel-controls-summary' );
if ( summary ) {
summary.textContent = message;
summary.classList.add( 'edac-error' );
}
}
}

// Some systems (Cloudflare Rocket Loader) defers scripts for performance but that can
Expand Down
94 changes: 70 additions & 24 deletions src/pageScanner/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,39 @@ import { getPageDensity } from './helpers/density';

const SCAN_TIMEOUT_IN_SECONDS = 30;

// Hold the timeout for the scan so it can bail on long-running scans.
let tooLongTimeout;

// Read the data passed from the parent document.
const body = document.querySelector( 'body' );
const iframeId = body.getAttribute( 'data-iframe-id' );
const eventName = body.getAttribute( 'data-iframe-event-name' );
const postId = body.getAttribute( 'data-iframe-post-id' );

/**
* Check if the current context the script is loaded in is a scanner iframe.
*
* @return {boolean} True if in iframe context, false otherwise.
*/
function isIframeContext() {
return !! ( body && body.hasAttribute( 'data-iframe-id' ) && body.hasAttribute( 'data-iframe-event-name' ) );
}

/**
* Get the iframe options from the body attributes/
*
* @return {Object} {{configOptions: {}, runOptions: {}, iframeId: string | Attribute, eventName: string | Attribute, postId: string | Attribute}}
*/
function getIframeOptions() {
return {
configOptions: {},
runOptions: {},
iframeId: body.getAttribute( 'data-iframe-id' ),
eventName: body.getAttribute( 'data-iframe-event-name' ),
postId: body.getAttribute( 'data-iframe-post-id' ),
};
}

const scan = async (
options = { configOptions: {}, runOptions: {} }
) => {
Expand Down Expand Up @@ -58,9 +85,10 @@ const scan = async (
//Build an array of the dom selectors and ruleIDs for violations/failed tests
item.violations.forEach( ( violation ) => {
if ( violation.result === 'failed' ) {
const el = document.querySelector( violation.node.selector );
violations.push( {
selector: violation.node.selector,
html: document.querySelector( violation.node.selector ).outerHTML,
html: el ? el.outerHTML : null,
ruleId: item.id,
impact: item.impact,
tags: item.tags,
Expand All @@ -71,9 +99,10 @@ const scan = async (
// Handle incomplete results for form-field-multiple-labels only.
if ( item.id === 'form-field-multiple-labels' ) { // Allow incomplete results for this rule.
item.incomplete.forEach( ( incompleteItem ) => {
const el = document.querySelector( incompleteItem.node.selector );
violations.push( {
selector: incompleteItem.node.selector,
html: document.querySelector( incompleteItem.node.selector ).outerHTML,
html: el ? el.outerHTML : null,
ruleId: item.id,
impact: item.impact,
tags: item.tags,
Expand Down Expand Up @@ -143,6 +172,7 @@ function dispatchDoneEvent( violations, errorMsgs, error ) {
top.dispatchEvent( customEvent );
}

// eslint-disable-next-line no-unused-vars
const onDone = ( violations = [], errorMsgs = [], error = false ) => {
// cleanup the timeout.
clearTimeout( tooLongTimeout );
Expand All @@ -153,38 +183,54 @@ const onDone = ( violations = [], errorMsgs = [], error = false ) => {
function() {
axe.teardown();
axe = null;

dispatchDoneEvent( violations, errorMsgs, error );
dispatchDoneEvent( violations, errorMsgs, '' );
},
function() {
axe.teardown();
axe = null;

// Create a custom event
errorMsgs.push( '***** axe.cleanup() failed.' );

dispatchDoneEvent( violations, errorMsgs, error );
dispatchDoneEvent( violations, errorMsgs, 'cleanup-failed' );
}
);
} else {
error = true;

errorMsgs.push( '***** axe.cleanup() does not exist.' );
axe = null;

dispatchDoneEvent( violations, errorMsgs, error );
dispatchDoneEvent( violations, errorMsgs, 'cleanup-not-exists' );
}
};

// Fire a failed event if the scan doesn't complete on time.
const tooLongTimeout = setTimeout( function() {
onDone( [], [ '***** axe scan took too long.' ], true );
}, SCAN_TIMEOUT_IN_SECONDS * 1000 );

// Start the scan.
scan().then( ( results ) => {
const violations = JSON.parse( JSON.stringify( results.violations ) );
onDone( violations );
} ).catch( ( err ) => {
onDone( [], [ err.message ], true );
} );
/**
* Attach an axe runner to the window object to allow for running the scan from
* the active document.
*
* @param {Object} options Options for the accessibility scan.
* @return {Promise<Object>} Promise resolving to the scan result.
*/
window.runAccessibilityScan = async function( options = {} ) {
return scan( options )
.then( ( result ) => {
if ( typeof options.onComplete === 'function' ) {
options.onComplete( result );
}
return result;
} )
.catch( ( err ) => {
if ( typeof options.onComplete === 'function' ) {
options.onComplete( null, err );
}
throw err;
Comment thread
pattonwebz marked this conversation as resolved.
} );
};

// Auto-run scan and dispatch event to parent frame if in iframe context
if ( isIframeContext() ) {
const iframeOptions = getIframeOptions();

tooLongTimeout = setTimeout( () => {
dispatchDoneEvent( [], [ 'Scan timed out' ], 'timeout' );
}, SCAN_TIMEOUT_IN_SECONDS * 1000 );

scan( iframeOptions )
.then( ( result ) => onDone( result.violations, [], null ) )
.catch( ( err ) => onDone( [], [ err.message || 'Unknown error' ], err.message ) );
}