Introduction
Just wanna get the code? All of the code for this tutorial is available at an open repository on BitBucket
Tenon.io is an API that facilitates quick and easy JavaScript-aware accessibility testing. The API accepts a large number of request parameters that allow you to customize how Tenon does its testing and returns its results. Full documentation for client developers is available in a public repository on Bitbucket. As an API, getting Tenon to do your accessibility testing requires a little bit of work. Tenon users have to do relatively minimal work to submit their request and deal with the response. This blog post shows an example of how to do that with a simple PHP class and also provides a method of generating a CSV file of results.
Despite the fact that it is an API, you can create a simple app very easily. First thing, of course, is that you need a Tenon.io API key. Go to Tenon.io to get one. Right now, Tenon is in Private Beta. If you’re interested in getting started right away, email karl@tenon.io to get your key. The second thing is you need a PHP-enabled server with cURL. Most default installs of PHP on web hosts will have it. If not, installation is easy.
How to use this class
Using this class is super easy. In the code chunk below, we’re merely going to pass some variables to the class and get the response. This is not production-ready code. There are a lot of areas where this can be improved. Use this as a starting point, not an end point.
<?php
require('tenon.class.php');
define('TENON_API_KEY', 'this is where you enter your api key');
define('TENON_API_URL', 'http://www.tenon.io/api/');
define('DEBUG', false);
$opts['key'] = TENON_API_KEY;
$opts['url'] = 'http://www.example.com'; // enter a real URL here, of course
$tenon = new tenon(TENON_API_URL, $opts);
$tenon->submit(DEBUG);
Using the code chunk above, you now have a variable, $tenon->tenonResponse
, formatted according to the Tenon response format (read the docs for full details.)
That’s it! From there, all you need to do is massage that JSON response into something useful for your purposes.
Let’s walk through a class that can help us do that.
Give it a name
First, create a file, called tenon.class.php
. Then start your file like so.
<?php
class tenon
{
...
Declare some variables
Now, at the top of the file we want to declare some variables:
$url
– this will be the URL to the Tenon.io API itself.$opts
– this will be an array of your request parameters$tenonResponse
– this will be populated by the JSON response from Tenon$rspArray
– this will be a multidimensional array of the decoded response.
protected $url, $opts;
public $tenonResponse, $rspArray;
Class Constructor
Time to get to our actual class methods. First up is our class constructor. Since constructors in PHP cannot return a value, we just set up some instance variables to be used by other methods. The arguments are the $url
and $opts
variables discussed above.
/**
* Class constructor
*
* @param string $url the API url to post your request to
* @param array $opts options for the request
*/
public function __construct($url, $opts)
{
$this->url = $url;
$this->opts = $opts;
$this->rspArray = null;
}
Submit your request to Tenon
Next up is the method that actually fires the request to the API. This function is nothing more than a wrapper around some cURL stuff. PHP’s functionality around cURL is excellent and makes it perfect for this type of purpose.
This method passes through our request parameters (from the $tenon->opts
array) to the API as a POST request and returns a variable, $tenon->tenonResponse
, populated with the JSON response from Tenon.
/**
* Submits the request to Tenon
*
* @param bool $printInfo whether or not to print the output from curl_getinfo (usually for debugging only)
*
* @return string the results, formatted as JSON
*/
public function submit($printInfo = false)
{
if (true === $printInfo) {
echo '<h2>Options Passed To TenonTest</h2><pre><br>';
var_dump($this->opts);
echo '</pre>';
}
//open connection
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_FAILONERROR, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $this->opts);
//execute post and get results
$result = curl_exec($ch);
if (true === $printInfo) {
echo 'ERROR INFO (if any): ' . curl_error($ch) . '<br>';
echo '<h2>Curl Info </h2><pre><br>';
print_r(curl_getinfo($ch));
echo '</pre>';
}
//close connection
curl_close($ch);
//the test results
$this->tenonResponse = $result;
}
Decode the response
From here, how you deal with the JSON is up to you. Most programming languages have ways to deal with JSON. PHP has some native functionality, albeit simple, to decode and encode JSON. Below, we use json_decode
to turn the JSON into a multidimensional array. This gives us the $tenon->rspArray
to use in other methods later.
/**
* @return mixed
*/
public function decodeResponse()
{
if ((false !== $this->tenonResponse) && (!is_null($this->tenonResponse))) {
$result = json_decode($this->tenonResponse, true);
if (!is_null($result)) {
$this->rspArray = $result;
} else {
return false;
}
} else {
return false;
}
}
Make some sense of booleans
Tenon returns some of its information as ‘1’ or ‘0’. We’re going to want that to be more useful for human consumption, so we convert those to ‘Yes’ and ‘No’. Because of some weirdness with json_decode and PHP’s loose typing, sometimes digits are actually strings, so that’s why we’re not using strict comparison.
/**
* @param $val
*
* @return string
*/
public static function boolToString($val){
if($val == '1'){
return 'Yes';
}
else{
return 'No';
}
}
Create a summary
OK, now it is time to start doing something useful with the response array. The first thing we need is a summary of how our request went and the status of our document. This method creates a string of HTML showing the following details:
- Your Request – Tenon echoes back your request to you. This section reports the request that Tenon uses, which may include items set to their defaults.
- Response Summary – This section gives a summary of the response, such as response code, response type, execution time, and document size.
- Global Stats – This section gives some high level stats on error rates across all tests run by Tenon. When compared against your document’s density (below), this is useful for getting an at-a-glance idea of your document’s accessibility
- Density – Tenon calculates a statistic called ‘Density’ which is, basically, how many errors you have, compared to how big the document is. In other words how dense are the issues on the page?
- Issue Counts – This section gives raw issue counts for your document
- Issues By Level – This section provides issue counts according to WCAG Level
- Client Script Errors – one of the things that may reduce the ability of Tenon to test your site is JavaScript errors and uncaught exceptions. A cool feature of Tenon is that it reports these to you.
/**
*
* @return mixed
*/
public function processResponseSummary()
{
if ((false === $this->rspArray) || (is_null($this->rspArray))) {
return false;
}
$output = '';
$output .= '<h2>Your Request</h2>';
$output .= '<ul>';
$output .= '<li>DocID: ' . $this->rspArray['request']['docID'] . '</li>';
$output .= '<li>Certainty: ' . $this->rspArray['request']['certainty'] . '</li>';
$output .= '<li>Level: ' . $this->rspArray['request']['level'] . '</li>';
$output .= '<li>Priority: ' . $this->rspArray['request']['priority'] . '</li>';
$output .= '<li>Importance: ' . $this->rspArray['request']['importance'] . '</li>';
$output .= '<li>Report ID: ' . $this->rspArray['request']['reportID'] . '</li>';
$output .= '<li>System ID: ' . $this->rspArray['request']['systemID'] . '</li>';
$output .= '<li>User-Agent String: ' . $this->rspArray['request']['uaString'] . '</li>';
$output .= '<li>URL: ' . $this->rspArray['request']['url'] . '</li>';
$output .= '<li>Viewport: ' . $this->rspArray['request']['viewport']['width'] . ' x ' . $this->rspArray['request']['viewport']['height'] . '</li>';
$output .= '<li>Fragment? ' . self::boolToString($this->rspArray['request']['fragment']) . '</li>';
$output .= '<li>Store Results? ' . self::boolToString($this->rspArray['request']['store']) . '</li>';
$output .= '</ul>';
$output .= '<h2>Response</h2>';
$output .= '<ul>';
$output .= '<li>Document Size: ' . $this->rspArray['documentSize'] . ' bytes </li>';
$output .= '<li>Response Code: ' . $this->rspArray['status'] . '</li>';
$output .= '<li>Response Type: ' . $this->rspArray['message'] . '</li>';
$output .= '<li>Response Time: ' . date("F j, Y, g:i a", strtotime($this->rspArray['responseTime'])) . '</li>';
$output .= '<li>Response Execution Time: ' . $this->rspArray['responseExecTime'] . ' seconds</li>';
$output .= '</ul>';
$output .= '<h2>Global Stats</h2>';
$output .= '<ul>';
$output .= '<li>Global Density, overall: ' . $this->rspArray['globalStats']['allDensity'] . '</li>';
$output .= '<li>Global Error Density: ' . $this->rspArray['globalStats']['errorDensity'] . '</li>';
$output .= '<li>Global Warning Density: ' . $this->rspArray['globalStats']['warningDensity'] . '</li>';
$output .= '</ul>';
$output .= '<h3>Density</h3>';
$output .= '<ul>';
$output .= '<li>Overall Density: ' . $this->rspArray['resultSummary']['density']['allDensity'] . '%</li>';
$output .= '<li>Error Density: ' . $this->rspArray['resultSummary']['density']['errorDensity'] . '%</li>';
$output .= '<li>Warning Density: ' . $this->rspArray['resultSummary']['density']['warningDensity'] . '%</li>';
$output .= '</ul>';
$output .= '<h3>Issue Counts</h3>';
$output .= '<ul>';
$output .= '<li>Total Issues: ' . $this->rspArray['resultSummary']['issues']['totalIssues'] . '</li>';
$output .= '<li>Total Errors: ' . $this->rspArray['resultSummary']['issues']['totalErrors'] . '</li>';
$output .= '<li>Total Warnings: ' . $this->rspArray['resultSummary']['issues']['totalWarnings'] . '</li>';
$output .= '</ul>';
$output .= '<h3>Issues By WCAG Level</h3>';
$output .= '<ul>';
$output .= '<li>Level A: ' . $this->rspArray['resultSummary']['issuesByLevel']['A']['count'];
$output .= ' (' . $this->rspArray['resultSummary']['issuesByLevel']['A']['pct'] . '%)</li>';
$output .= '<li>Level AA: ' . $this->rspArray['resultSummary']['issuesByLevel']['AA']['count'];
$output .= ' (' . $this->rspArray['resultSummary']['issuesByLevel']['AA']['pct'] . '%)</li>';
$output .= '<li>Level AAA: ' . $this->rspArray['resultSummary']['issuesByLevel']['AAA']['count'];
$output .= ' (' . $this->rspArray['resultSummary']['issueSummary']['AAA']['pct'] . '%)</li>';
$output .= '</ul>';
$output .= '<h3>Client Script Errors, if any</h3>';
$output .= '<p>(Note: "NULL" or empty array here means there were no errors.)</p>';
$output .= '<pre>' . var_export($this->rspArray['clientScriptErrors'], true) . '</pre>';
return $output;
}
Output the issues
The most important part of Tenon is obviously the issues. The below method gets the issues and loops through them to print them out in a human-readable format. Each issue is presented to show what the issue is and where the issue is. For a full description of Tenon’s issue reports, read the Tenon.io Documentation
/**
*
* @return string
*/
function processIssues()
{
$issues = $this->rspArray['resultSet'];
$count = count($issues);
if ($count > 0) {
$i = 0;
for ($x = 0; $x < $count; $x++) {
$i++;
$output .= '<div class="issue">';
$output .= '<div>' . $i .': ' . $issues[$x]['errorTitle'] . '</div>';
$output .= '<div>' . $issues[$x]['errorDescription'] . '</div>';
$output .= '<div><pre><code>' . trim($issues[$x]['errorSnippet']) . '</code></pre></div>';
$output .= '<div>Line: ' . $issues[$x]['position']['line'] . '</div>';
$output .= '<div>Column: ' . $issues[$x]['position']['column'] . '</div>';
$output .= '<div>xPath: <pre><code>' . $issues[$x]['xpath'] . '</code></pre></div>';
$output .= '<div>Certainty: ' . $issues[$x]['certainty'] . '</div>';
$output .= '<div>Priority: ' . $issues[$x]['priority'] . '</div>';
$output .= '<div>Best Practice: ' . $issues[$x]['resultTitle'] . '</div>';
$output .= '<div>Reference: ' . $issues[$x]['ref'] . '</div>';
$output .= '<div>Standards: ' . implode(', ', $issues[$x]['standards']) . '</div>';
$output .= '<div>Issue Signature: ' . $issues[$x]['signature'] . '</div>';
$output .= '<div>Test ID: ' . $issues[$x]['tID'] . '</div>';
$output .= '<div>Best Practice ID: ' . $issues[$x]['bpID'] . '</div>';
$output .= '</div>';
}
}
return $output;
}
Full Usage Example
So now that we have the full class in place, let’s put it all together. In the example below, we’re taking our request parameters from a $_POST
array, such as that which we’d get from a form submission.
<?php
define('TENON_API_KEY', 'this is where you enter your api key');
define('TENON_API_URL', 'http://www.tenon.io/api/');
define('DEBUG', false);
$expectedPost = array('src', 'url', 'level', 'certainty', 'priority',
'docID', 'systemID', 'reportID', 'viewport',
'uaString', 'importance', 'ref', 'importance',
'fragment', 'store', 'csv');
foreach ($_POST AS $k => $v) {
if (in_array($k, $expectedPost)) {
if (strlen(trim($v)) > 0) {
$opts[$k] = $v;
}
}
}
$opts['key'] = TENON_API_KEY;
$tenon = new tenon(TENON_API_URL, $opts);
$tenon->submit(DEBUG);
if (false === $tenon->decodeResponse()) {
$content = '<h1>Error</h1><p>No Response From Tenon API, or JSON malformed.</p>';
$content .= '<pre>' . var_export($tenon->tenonResponse, true) . '</pre>';
} else {
$summary = $tenon->processResponseSummary();
$content .= '<h2>Issues</h2>';
$content .= $tenon->processIssues();
$content .= $tenon->rawResponse();
}
echo $content;
?>
That’s it! You now have an HTML output of Tenon’s response summary and issue details!
Screw it, just gimme the issues
OK, what if you just want the issues and none of that output-to-HTML stuff? Getting the issues into a CSV file is ridiculously easy with PHP. Add this method to your PHP class:
/**
* @param $pathToFolder
*
* @return bool
*/
public function writeResultsToCSV($pathToFolder)
{
$url = $this->rspArray['request']['url'];
$issues = $this->rspArray['resultSet'];
$name = htmlspecialchars($this->rspArray['request']['docID']);
$count = count($issues);
if ($count < 1) {
return false;
}
for ($x = 0; $x < $count; $x++) {
$rows[$x] = array(
$url,
$issues[$x]['tID'],
$issues[$x]['resultTitle'],
$issues[$x]['errorTitle'],
$issues[$x]['errorDescription'],
implode(', ', $issues[$x]['standards']),
html_entity_decode($issues[$x]['errorSnippet']),
$issues[$x]['position']['line'],
$issues[$x]['position']['column'],
$issues[$x]['xpath'],
$issues[$x]['certainty'],
$issues[$x]['priority'],
$issues[$x]['ref'],
$issues[$x]['signature']
);
}
// Put a row of headers up on the beginning
array_unshift($rows, array('URL', 'testID', 'Best Practice', 'Issue Title', 'Description',
'WCAG SC', 'Issue Code', 'Line', 'Column', 'xPath', 'Certainty', 'Priority', 'Reference', 'Signature'));
// MAKE SURE THE FILE DOES NOT ALREADY EXIST
if (!file_exists($pathToFolder . $name . '.csv')) {
$fp = fopen($pathToFolder . $name . '.csv', 'w');
foreach ($rows as $fields) {
fputcsv($fp, $fields);
}
fclose($fp);
return true;
}
return false;
}
Then all you need to do is call it like this:
<?php
define('TENON_API_KEY', 'this is where you'd enter your api key');
define('TENON_API_URL', 'http://www.tenon.io/api/');
define('DEBUG', false);
define('CSV_FILE_PATH', $_SERVER['DOCUMENT_ROOT'] . '/csv/');
$expectedPost = array('src', 'url', 'level', 'certainty', 'priority',
'docID', 'systemID', 'reportID', 'viewport',
'uaString', 'importance', 'ref', 'importance',
'fragment', 'store', 'csv');
foreach ($_POST AS $k => $v) {
if (in_array($k, $expectedPost)) {
if (strlen(trim($v)) > 0) {
$opts[$k] = $v;
}
}
}
$opts['key'] = TENON_API_KEY;
$tenon = new tenonTest(TENON_API_URL, $opts);
$tenon->submit(DEBUG);
if (false === $tenon->decodeResponse()) {
$content = '<h1>Error</h1><p>No Response From Tenon API, or JSON malformed.</p>';
$content .= '<pre>' . var_export($tenon->tenonResponse, true) . '</pre>';
echo $content;
} else {
if(false !== $tenon->writeResultsToCSV(CSV_FILE_PATH)){
echo 'CSV file written!';
}
}
?>
Now what?
This blog post shows how easy it is to create a PHP implementation that will submit a request to Tenon, do some testing, and return results. We want to see what you can do with it. Register at Tenon.io and get started!