diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/wai-aria/scripts | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/wai-aria/scripts')
3 files changed, 1213 insertions, 0 deletions
diff --git a/testing/web-platform/tests/wai-aria/scripts/ATTAcomm.js b/testing/web-platform/tests/wai-aria/scripts/ATTAcomm.js new file mode 100644 index 0000000000..b748233fc2 --- /dev/null +++ b/testing/web-platform/tests/wai-aria/scripts/ATTAcomm.js @@ -0,0 +1,947 @@ +/* globals Promise, window, done, assert_true, on_event, promise_test */ + +/** + * Creates an ATTAcomm object. If the parameters are supplied + * it sets up event listeners to send the test data to an ATTA if one + * is available. If the ATTA does not respond, it will assume the test + * is being done manually and the results are being entered in the + * parent test window. + * + * @constructor + * @param {object} params + * @param {string} [params.test] - object containing JSON test definition + * @param {string} [params.testFile] - URI of a file with JSON test definition + * @param {string} params.ATTAuri - URI to use to exercise the window + * @event DOMContentLoaded Calls go once DOM is fully loaded + * @returns {object} Reference to the new object + * + */ + +function ATTAcomm(params) { + 'use strict'; + + this.Params = null; // parameters passed in + this.Promise = null; // master Promise that resolves when intialization is complete + this.Properties = null; // testharness_properties from the opening window + this.Tests = null; // test object being processed + this.testName = ""; // name of test being run + this.log = ""; // a buffer to capture log information for debugging + this.startReponse = {}; // startTest response will go in here for debugging + + this.loading = true; + + this.timeout = 5000; + + var pending = [] ; + + // set up in case DOM finishes loading early + pending.push(new Promise(function(resolve) { + on_event(document, "DOMContentLoaded", function() { + resolve(true); + }.bind(this)); + }.bind(this))); + + // if we are under runner, then there are props in the parent window + // + // if "output" is set in that, then pause at the end of running so the output + // can be analyzed. @@@TODO@@@ + if (window && window.opener && window.opener.testharness_properties) { + this.Properties = window.opener.testharness_properties; + } + + this.Params = params; + + if (this.Params.hasOwnProperty("ATTAuri")) { + this.ATTAuri = this.Params.ATTAuri; + } else { + this.ATTAuri = "http://localhost:4119"; + } + + if (this.Params.hasOwnProperty("title")) { + this.testName = this.Params.title; + } + + // start by loading the test (it might be inline, but + // loadTest deals with that + pending.push(this.loadTest(params) + .then(function(tests) { + // if the test is NOT an object, turn it into one + if (typeof tests === 'string') { + tests = JSON.parse(tests) ; + } + + this.Tests = tests; + + }.bind(this))); + + this.Promise = new Promise(function(resolve, reject) { + // once the DOM and the test is loaded... set us up + Promise.all(pending) + .then(function() { + // Everything is loaded + this.loading = false ; + // run the automated tests (or setup for manual testing) + this.go(); + resolve(this); + }.bind(this)) + .catch(function(err) { + // loading the components failed somehow - report the errors and mark the test failed + test( function() { + assert_true(false, "Loading of test components failed: " +JSON.stringify(err)) ; + }, "Loading test components"); + this.dumpLog(); + done() ; + reject("Loading of test components failed: "+JSON.stringify(err)); + return ; + }.bind(this)); + }.bind(this)); + + return this; +} + +ATTAcomm.prototype = { + + /** + * go sets up the connection to the ATTA + * + * If that succeeds and the tests in this test file have methods for + * the API supported by the ATTA, then it automatically runs those tests. + * + * Otherwise it sets up for manualt testing. + */ + go: function() { + 'use strict'; + // everything is ready. Let's talk to the ATTA + this.startTest().then(function(res) { + + // start was successful - iterate over steps + var API = res.body.API; + + var subtestsForAPI = false; + + // check main and potentially nested lists of tests for + // tests with this API. If any step is missing this API + // mapping, then we need to be manual + this.Tests.forEach(function(subtest) { + if (subtest.hasOwnProperty("test") && + subtest.test.hasOwnProperty(API)) { + // there is at least one subtest for this API so + // this is a test that needs to be looked at by an atta + subtestsForAPI = true; + } else if (Array.isArray(subtest)) { + subtest.forEach(function(st) { + if (st.hasOwnProperty("test") && + st.test.hasOwnProperty(API)) { + subtestsForAPI = true; + } + }); + } + }); + + if (subtestsForAPI) { + this.runTests(API, this.Tests) + .then(function() { + // the tests all ran; close it out + this.endTest().then(function() { + this.dumpLog(); + done(); + }.bind(this)); + }.bind(this)) + .catch(function(err) { + this.endTest().then(function() { + this.dumpLog(); + done(); + }.bind(this)); + }.bind(this)); + } else { + // we don't know this API for this test + // but we ARE talking to an ATTA; skip this test + this.dumpLog(); + if (window.opener && window.opener.completion_callback) { + window.opener.completion_callback([], { status: 3, message: "No steps for AT API " + API } ); + } else { + done(); + } + // this.setupManualTest("Unknown AT API: " + API); + } + }.bind(this)) + .catch(function(res) { + // startTest failed so just sit and wait for a manual test to occur + if (res.timeout || res.status === 102) { + this.setupManualTest("No response from ATTA at " + this.ATTAuri); + } else if (res.status === 200 ) { + this.setupManualTest(res.message); + } else if (res.statusText === "No response from ATTA") { + this.setupManualTest(""); + } else { + this.setupManualTest("Error from ATTA: " + res.status + ": " + res.statusText); + } + }.bind(this)); + }, + + runTests: function(API, collection) { + // this method returns a promise + + return new Promise(function(resolve, reject) { + // accumulate promises; complete when done + var pending = []; + var testCount = 0; + + this.sendEvents(API, collection) + .then(function(eventStatus) { + + /* Loop strategy... + * + * If the the step is a 'test' then push it into the pending queue as a promise + * + * If the step is anything else, then if there is anything in pending, wait on it + * Once it resolves, clear the queue and then execute the other step. + * + */ + collection.forEach(function(subtest) { + // what "type" of step in the sequence is this? + var theType = "test" ; + if (Array.isArray(subtest)) { + // it is a group + Promise.all(pending).then(function() { + pending = []; + // recursively run the tests + pending.push(this.runTests(API, subtest)); + }.bind(this)); + } else if (subtest.hasOwnProperty("type")) { + theType = subtest.type; + } + testCount++; + if (theType === "test") { + // this is a set of assertions that should be evaluated + pending.push(this.runTest(testCount, API, subtest)); + } else if (theType === "script") { + Promise.all(pending).then(function() { + pending = []; + // execute the script + this.runScript(testCount, subtest); + }.bind(this)); + } else if (theType === "attribute") { + Promise.all(pending).then(function() { + pending = []; + // raise the event + this.handleAttribute(testCount, subtest); + }.bind(this)); + // } else { + } else if (theType === "event") { + Promise.all(pending).then(function() { + pending = []; + // raise the event + this.raiseEvent(testCount, subtest); + }.bind(this)); + // } else { + } + }.bind(this)); + + Promise.all(pending) + .then(function() { + // this collection all ran + if (eventStatus !== "NOEVENTS") { + // there were some events at the beginning + this.sendStopListen().then(function() { + resolve(true); + }); + } else { + resolve(true); + } + }.bind(this)); + }.bind(this)); + }.bind(this)); + }, + + setupManualTest: function(message) { + // if we determine the test should run manually, then expose all of the conditions that are + // in the TEST data structure so that a human can to the inspection and calculate the result + // + 'use strict'; + + var ref = document.getElementById("manualMode"); + if (ref) { + // we have a manualMode block. Populate it + var content = "<h2>Manual Mode Enabled</h2><p>"+message+"</p>"; + if (this.Tests.hasOwnProperty("description")) { + content += "<p>" + this.Tests.description + "</p>"; + } + var theTable = "<table id='steps'><tr><th>Step</th><th>Type</th><th>Element ID</th><th>Assertions</th></tr>"; + this.Tests.forEach(function(subtest) { + var type = "test"; + if (subtest.hasOwnProperty("type")) { + type = subtest.type; + } + var id = "" ; + if (subtest.hasOwnProperty("element")) { + id = subtest.element; + } + theTable += "<tr><td class='step'>" + subtest.title +"</td>"; + theTable += "<td class='type'>" + type + "</td>"; + theTable += "<td class='element'>" + id +"</td>"; + + // now what do we put over here? depends on the type + if (type === "test") { + // it is a test; dump the assertions + theTable += "<td>" + this.buildAssertionTable(subtest.test) + "</td>"; + } else if (type === "attribute" ) { + if (subtest.hasOwnProperty("attribute") && subtest.hasOwnProperty("value") && subtest.hasOwnProperty("element")) { + if (subtest.value === "none") { + theTable += "<td>Remove attribute <code>" + subtest.attribute + "</code> from the element with ID <code>" + subtest.element + "</code></td>"; + } else { + theTable += "<td>Set attribute <code>" + subtest.attribute + "</code> on the element with ID <code>" + subtest.element + "</code> to the value <code>" + subtest.value + "</code></td>"; + } + } + } else if (type === "event" ) { + // it is some events + if (subtest.hasOwnProperty("event") && subtest.hasOwnProperty("element")) { + theTable += "<td>Send event <code>" + subtest.event + "</code> to the element with ID <code>" + subtest.element + "</code></td>"; + } + } else if (type === "script" ) { + // it is a script fragment + theTable += "<td>Script: " + subtest.script + "</td>"; + } else { + theTable += "<td>Unknown type: " + type + "</td>"; + } + theTable += "</tr>"; + + + }.bind(this)); + + theTable += "</table>"; + ref.innerHTML = content + theTable ; + } + }, + + buildAssertionTable: function(asserts) { + "use strict"; + var output = "<table class='api'><tr><th>API Name</th><th colspan='4'>Assertions</th></tr>"; + var APIs = [] ; + for (var k in asserts) { + if (asserts.hasOwnProperty(k)) { + APIs.push(k); + } + } + + APIs.sort().forEach(function(theAPI) { + var rows = asserts[theAPI] ; + var height = rows.length; + output += "<tr><td rowspan='" + height + "' class='apiName'>"+theAPI+"</td>"; + var lastRow = rows.length - 1; + rows.forEach(function(theRow, index) { + var span = 4 - theRow.length; + var colspan = span ? " colspan='"+span+"'" : ""; + theRow.forEach(function(item) { + output += "<td" + colspan + ">" + item + "</td>"; + }); + output += "</tr>"; + if (index < lastRow) { + output += "<tr>"; + } + }); + }); + + output += "</table>"; + return output; + }, + + // eventList - find the events for an API + // + // @param {string} API + // @param {array} collection - a collection of tests + // @returns {array} list of event names + + eventList: function(API, collection) { + var eventHash = {}; + + if (!API || API === "") { + return []; + } + + collection.forEach(function(subtest) { + if (subtest.hasOwnProperty("test") && + subtest.test.hasOwnProperty(API)) { + // this is a subtest for this API; look at the events + subtest.test[API].forEach(function(assert) { + // look for event names + if (assert[0] === "event" && assert[1] === "type" && assert[2] === "is") { + eventHash[assert[3]] = 1; + } + }); + } + }); + + return Object.keys(eventHash); + }, + + // handleAttribute - set or clear an attribute + /** + * @param {integer} testNum - The subtest number + * @param {object} subtest - attribute information to set + */ + handleAttribute: function(testNum, subtest) { + "use strict"; + if (subtest) { + if (subtest.hasOwnProperty("attribute") && subtest.hasOwnProperty("element") && subtest.hasOwnProperty("value")) { + // update an attribute + try { + var node = document.getElementById(subtest.element); + if (node) { + if (subtest.value === "none") { + // remove this attribute + node.removeAttribute(subtest.attribute); + } else if (subtest.value === '""') { + node.setAttribute(subtest.attribute, ""); + } else if (subtest.value.match(/^"/) ) { + var v = subtest.value; + v = v.replace(/^"/, ''); + v = v.replace(/"$/, ''); + node.setAttribute(subtest.attribute, v); + } else { + node.setAttribute(subtest.attribute, subtest.value); + } + } + } + catch (e) { + test(function() { + assert_true(false, "Subtest attribute failed to update: " +e); + }, "Attribute subtest " + testNum); + } + } else { + test(function() { + var err = ""; + if (!subtest.hasOwnProperty("attribute")) { + err += "Attribute subtest has no attribute property; "; + } else if (!subtest.hasOwnProperty("value")) { + err += "Attribute subtest has no value property; "; + } else if (!subtest.hasOwnProperty("element")) { + err += "Attribute subtest has no element property; "; + } + assert_true(false, err); + }, "Attribute subtest " + testNum ); + } + } + return; + }, + + + + // raiseEvent - throw an event at an item + /** + * @param {integer} testNum - The subtest number + * @param {object} subtest - event information to throw + */ + raiseEvent: function(testNum, subtest) { + "use strict"; + var evt; + if (subtest) { + var kp = function(target, key) { + evt = document.createEvent("KeyboardEvent"); + evt.initKeyEvent ("keypress", true, true, window, + 0, 0, 0, 0, 0, "e".charCodeAt(0)); + target.dispatchEvent(evt); + }; + if (subtest.hasOwnProperty("event") && subtest.hasOwnProperty("element")) { + // throw an event + try { + var node = document.getElementById(subtest.element); + if (node) { + if (subtest.event === "focus") { + node.focus(); + } else if (subtest.event === "select") { + node.click(); + } else if (subtest.event.startsWith('key:')) { + var key = subtest.event.replace('key:', ''); + evt = new KeyboardEvent("keypress", { "key": key}); + node.dispatchEvent(evt); + } else { + evt = new Event(subtest.element); + node.dispatchEvent(evt); + } + } + } + catch (e) { + test(function() { + assert_true(false, "Subtest event failed to dispatch: " +e); + }, "Event subtest " + testNum); + } + } else { + test(function() { + var err = ""; + if (!subtest.hasOwnProperty("event")) { + err += "Event subtest has no event property; "; + } else if (!subtest.hasOwnProperty("element")) { + err += "Event subtest has no element property; "; + } + assert_true(false, err); + }, "Event subtest " + testNum ); + } + } + return; + }, + + // runScript - run a script in the context of the window + /** + * @param {integer} testNum - The subtest number + * @param {object} subtest - script and related information + */ + runScript: function(testNum, subtest) { + "use strict"; + if (subtest) { + if (subtest.hasOwnProperty("script") && typeof subtest.script === "string") { + try { + /* jshint evil:true */ + eval(subtest.script); + } + catch (e) { + test(function() { + assert_true(false, "Subtest script " + subtest.script + " failed to evaluate: " +e); + }, "Event subtest " + testNum); + } + } else { + test(function() { + assert_true(false, "Event subtest has no script property"); + }, "Event subtest " + testNum ); + } + } + return; + }, + + // runTest - process subtest + /** + * @param {integer} testNum - The subtest number + * @param {string} API - name of the API being tested + * @param {object} subtest - a subtest to run; contains 'title', 'element', and + * 'test array' + * @returns {Promise} - a Promise that resolves when the test completes + */ + runTest: function(testNum, API, subtest) { + 'use strict'; + + var data = { + "title" : subtest.title, + "id" : subtest.element, + "data": this.normalize(subtest.test[API]) + }; + + return new Promise(function(resolve) { + var ANNO = this; + if (subtest.test[API]) { + // we actually have a test to run + promise_test(function() { + // force a resolve of the promise regardless + this.add_cleanup(function() { resolve(true); }); + return ANNO.sendTest(data) + .then(function(res) { + if (typeof res.body === "object" && res.body.hasOwnProperty("status")) { + // we got some sort of response + if (res.body.status === "OK") { + // the test ran - yay! + var messages = ""; + var thisResult = null; + var theLog = ""; + var assertionCount = 0; + res.body.results.forEach( function (a) { + if (typeof a === "object") { + // we have a result for this assertion + // first, what is the assertion? + var aRef = data.data[assertionCount]; + var assertionText = '"' + aRef.join(" ") +'"'; + + if (a.hasOwnProperty("log") && a.log !== null && a.log !== '' ) { + // there is log data - save it + theLog += "\n--- Assertion " + assertionCount + " ---"; + theLog += "\nAssertion: " + assertionText + "\nLog data: "+a.log ; + } + + // is there a message? + var theMessage = ""; + if (a.hasOwnProperty("message")) { + theMessage = a.message; + } + if (!a.hasOwnProperty("result")) { + messages += "ATTA did not report a result " + theMessage + "; "; + } else if (a.result === "ERROR") { + messages += "ATTA reported ERROR with message: " + theMessage + "; "; + } else if (a.result === "FAIL") { + thisResult = false; + messages += assertionText + " failed " + theMessage + "; "; + } else if (a.result === "PASS" && thisResult === null) { + // if we got a pass and there was no other result thus far + // then we are passing + thisResult = true; + } + } + assertionCount++; + }); + if (theLog !== "") { + ANNO.saveLog("runTest", theLog, subtest); + } + if (thisResult !== null) { + assert_true(thisResult, messages); + } else { + assert_true(false, "ERROR: No results reported from ATTA; " + messages); + } + } else if (res.body.status === "ERROR") { + assert_true(false, "ATTA returned ERROR with message: " + res.body.statusText); + } else { + assert_true(false, "ATTA returned unknown status " + res.body.status + " with message: " + res.body.statusText); + } + } else { + // the return wasn't an object! + assert_true(false, "ATTA failed to return a result object: returned: "+JSON.stringify(res)); + } + }); + }, subtest.name ); + } else { + // there are no test steps for this API. fake a subtest result + promise_test(function() { + // force a resolve of the promise regardless + this.add_cleanup(function() { resolve(true); }); + return new Promise(function(innerResolve) { + innerResolve(true); + }) + .then(function(res) { + var theLog = "\nSUBTEST NOTRUN: No assertions for API " + API + "\n"; + if (theLog !== "") { + ANNO.saveLog("runTest", theLog, subtest); + } + assert_false(true, "NOTRUN: No assertion for API " + API); + }); + }, subtest.name ); + } + }.bind(this)); + }, + + // loadTest - load a test from an external JSON file + // + // returns a promise that resolves with the contents of the + // test + + loadTest: function(params) { + 'use strict'; + + if (params.hasOwnProperty('stepFile')) { + // the test is referred to by a file name + return this._fetch("GET", params.stepFile); + } // else + return new Promise(function(resolve, reject) { + if (params.hasOwnProperty('steps')) { + resolve(params.steps); + } else { + reject("Must supply a 'steps' or 'stepFile' parameter"); + } + }); + }, + + /* dumpLog - put log information into the log div on the page if it exists + */ + + dumpLog: function() { + 'use strict'; + if (this.log !== "") { + var ref = document.getElementById("ATTAmessages"); + if (ref) { + // we have a manualMode block. Populate it + var content = "<h2>Logging information recorded</h2>"; + if (this.startResponse && this.startResponse.hasOwnProperty("API")) { + content += "<h3>ATTA Information</h3>"; + content += "<pre>"+JSON.stringify(this.startResponse, null, " ")+"</pre>"; + } + content += "<textarea rows='50' style='width:100%'>"+this.log+"</textarea>"; + ref.innerHTML = content ; + } + } + }, + + /* saveLog - capture logging information so that it can be displayed on the page after testing is complete + * + * @param {string} caller name + * @param {string} log message + * @param {object} subtest + */ + + saveLog: function(caller, message, subtest) { + 'use strict'; + + if (typeof message === "string" && message !== "") { + this.log += "============================================================\n"; + this.log += "Message from " + caller + "\n"; + if (subtest && typeof subtest === "object") { + var API = this.startResponse.API; + this.log += "\n SUBTEST TITLE: " + subtest.title; + this.log += "\n SUBTEST ELEMENT: " + subtest.element; + this.log += "\n SUBTEST DATA: " + JSON.stringify(subtest.test[API]); + this.log += "\n\n"; + } + this.log += message; + } + return; + }, + + // startTest - send the test start message + // + // @returns {Promise} resolves if the start is successful, or rejects with + + startTest: function() { + 'use strict'; + + return new Promise(function(resolve, reject) { + var params = { + test: this.testName || window.title, + url: document.location.href + }; + + this._fetch("POST", this.ATTAuri + "/start", null, params) + .then(function(res) { + if (res.body.hasOwnProperty("status")) { + if (res.body.status === "READY") { + this.startResponse = res.body; + if (res.body.hasOwnProperty("log")) { + // there is some logging data - capture it + this.saveLog("startTest", res.body.log); + } + // the system is ready for us - is it really? + if (res.body.hasOwnProperty("API")) { + resolve(res); + } else { + res.message = "No API in response from ATTA"; + reject(res); + } + } else { + // the system reported something else - fail out with the statusText as a result + res.message = "ATTA reported an error: " + res.body.statusText; + reject(res); + } + } else { + res.message = "ATTA did not report a status"; + reject(res); + } + }.bind(this)) + .catch(function(res) { + reject(res); + }); + }.bind(this)); + }, + + // sendEvents - send the list of events the ATTA needs to listen for + // + // @param {string} API + // @param {array} collection - a list of tests + // @returns {Promise} resolves if the message is successful, or rejects with + + sendEvents: function(API, collection) { + 'use strict'; + + return new Promise(function(resolve, reject) { + var eList = this.eventList(API, collection) ; + if (eList && eList.length) { + var params = { + events: eList + }; + + this._fetch("POST", this.ATTAuri + "/startlisten", null, params) + .then(function(res) { + if (res.body.hasOwnProperty("status")) { + if (res.body.status === "READY") { + if (res.body.hasOwnProperty("log")) { + // there is some logging data - capture it + this.saveLog("sendEvents", res.body.log); + } + resolve(res.body.status); + } else { + // the system reported something else - fail out with the statusText as a result + res.message = "ATTA reported an error: " + res.body.statusText; + reject(res); + } + } else { + res.message = "ATTA did not report a status"; + reject(res); + } + }.bind(this)) + .catch(function(res) { + reject(res); + }); + } else { + // there are no events + resolve("NOEVENTS"); + } + }.bind(this)); + }, + + sendStopListen: function() { + 'use strict'; + + return this._fetch("POST", this.ATTAuri + "/stoplisten", null, null); + }, + + // sendTest - send test data to an ATTA and wait for a response + // + // returns a promise that resolves with the results of the test + + sendTest: function(testData) { + 'use strict'; + + if (typeof testData !== "string") { + testData = JSON.stringify(testData); + } + var ret = this._fetch("POST", this.ATTAuri + "/test", null, testData, true); + ret.then(function(res) { + if (res.body.hasOwnProperty("log")) { + // there is some logging data - capture it + this.saveLog("sendTest", res.body.log); + } + }.bind(this)); + return ret; + }, + + endTest: function() { + 'use strict'; + + return this._fetch("GET", this.ATTAuri + "/end"); + }, + + /* normalize - ensure subtest data conforms to ATTA spec + */ + + normalize: function( data ) { + 'use strict'; + + var ret = [] ; + + if (data) { + data.forEach(function(assert) { + var normal = [] ; + // ensure if there is a value list it is compressed + if (Array.isArray(assert)) { + // we have an array + normal[0] = assert[0]; + normal[1] = assert[1]; + normal[2] = assert[2]; + if ("string" === typeof assert[3] && assert[3].match(/^\[.*\]$/)) { + // it is a string and matches the valuelist pattern + normal[3] = assert[3].replace(/, +/, ','); + } else { + normal[3] = assert[3]; + } + ret.push(normal); + } else { + ret.push(assert); + } + }); + } + return ret; + }, + + // _fetch - return a promise after sending data + // + // Resolves with the returned information in a structure + // including: + // + // xhr - a raw xhr object + // headers - an array of headers sent in the request + // status - the status code + // statusText - the text of the return status + // text - raw returned data + // body - an object parsed from the returned content + // + + _fetch: function (method, url, headers, content, parse) { + 'use strict'; + if (method === null || method === undefined) { + method = "GET"; + } + if (parse === null || parse === undefined) { + parse = true; + } + if (headers === null || headers === undefined) { + headers = []; + } + + + // note that this Promise always resolves - there is no reject + // condition + + return new Promise(function (resolve, reject) { + var xhr = new XMLHttpRequest(); + + // this gets returned when the request completes + var resp = { + xhr: xhr, + headers: null, + status: 0, + statusText: "", + body: null, + text: "" + }; + + xhr.open(method, url); + + // headers? + headers.forEach(function(ref) { + xhr.setRequestHeader(ref[0], ref[1]); + }); + + //if (this.timeout) { + // xhr.timeout = this.timeout; + //} + + xhr.ontimeout = function() { + resp.timeout = this.timeout; + resolve(resp); + }; + + xhr.onerror = function() { + if (this.status) { + resp.status = this.status; + resp.statusText = xhr.statusText; + } else if (this.status === 0) { + resp.status = 0; + resp.statusText = "No response from ATTA"; + } + reject(resp); + }; + + xhr.onload = function () { + resp.status = this.status; + if (this.status >= 200 && this.status < 300) { + var d = xhr.response; + // return the raw text of the response + resp.text = d; + // we have it; what is it? + if (parse) { + try { + d = JSON.parse(d); + resp.body = d; + } + catch(err) { + resp.body = null; + } + } + resolve(resp); + } else { + reject({ + status: this.status, + statusText: xhr.statusText + }); + } + }; + + if (content !== null && content !== undefined) { + if ("object" === typeof(content)) { + xhr.send(JSON.stringify(content)); + } else if ("function" === typeof(content)) { + xhr.send(content()); + } else if ("string" === typeof(content)) { + xhr.send(content); + } + } else { + xhr.send(); + } + }); + }, + +}; + +// vim: set ts=2 sw=2: diff --git a/testing/web-platform/tests/wai-aria/scripts/aria-utils.js b/testing/web-platform/tests/wai-aria/scripts/aria-utils.js new file mode 100644 index 0000000000..ec53464c18 --- /dev/null +++ b/testing/web-platform/tests/wai-aria/scripts/aria-utils.js @@ -0,0 +1,196 @@ +/* Utilities related to WAI-ARIA */ + +const AriaUtils = { + + /* + Tests simple role assignment: <div role="alert">x</div> + Not intended for nested, context-dependent, or other complex role tests. + + Ex: AriaUtils.assignAndVerifyRolesByRoleNames(["group", "main", "button"]) + + */ + assignAndVerifyRolesByRoleNames: function(roleNames) { + if (!Array.isArray(roleNames) || !roleNames.length) { + throw `Param roleNames of assignAndVerifyRolesByRoleNames("${roleNames}") should be an array containing at least one role string.`; + } + for (const role of roleNames) { + promise_test(async t => { + let el = document.createElement("div"); + el.appendChild(document.createTextNode("x")); + el.setAttribute("role", role); // el.role not yet supported by Gecko. + document.body.appendChild(el); + const computedRole = await test_driver.get_computed_role(el); + assert_equals(computedRole, role, el.outerHTML); + }, `role: ${role}`); + } + }, + + + /* + Tests computed ROLE of all elements matching selector + against the string value of their data-expectedrole attribute. + + Ex: <div role="list" + data-testname="optional unique test name" + data-expectedrole="list" + class="ex"> + + AriaUtils.verifyRolesBySelector(".ex") + + */ + verifyRolesBySelector: function(selector, roleTestNamePrefix) { + const els = document.querySelectorAll(selector); + if (!els.length) { + throw `Selector passed in verifyRolesBySelector("${selector}") should match at least one element.`; + } + for (const el of els) { + let role = el.getAttribute("data-expectedrole"); + let testName = el.getAttribute("data-testname") || role; // data-testname optional if role is unique per test file + if (typeof roleTestNamePrefix !== "undefined") { + testName = roleTestNamePrefix + testName; + } + promise_test(async t => { + const expectedRole = el.getAttribute("data-expectedrole"); + const computedRole = await test_driver.get_computed_role(el); + assert_equals(computedRole, expectedRole, el.outerHTML); + }, `${testName}`); + } + }, + + + /* + Tests computed ROLE of selected elements matching selector + against the string value of provided roles array. + + Ex: <foo + data-testname="verify fooRole or barRole role on span" + class="ex-foo-or-bar"> + + AriaUtils.verifyRoleOrVariantRolesBySelector(".ex-foo-or-bar", ["fooRole", "barRole"]); + + See also helper function verifyGenericRolesBySelector shorthand of the above using ["generic", "", "none"]. + + Note: This function should not be used to circumvent unexpected interop differences in implementations. + It should only be used in specific cases (like "generic") determined by ARIA WG or other spec maintainers to be acceptable for the purposes of testing. + + */ + verifyRoleOrVariantRolesBySelector: function(selector, roles) { + const els = document.querySelectorAll(selector); + if (!els.length) { + throw `Selector "${selector}" should match at least one element.`; + } + if (!roles.length || roles.length < 2) { + throw `Roles array ["${roles.join('", "')}"] should include at least two strings, a primary role and at least one acceptable implementation-specific variant. E.g. ["generic", "", "none"]…`; + } + for (const el of els) { + let testName = el.getAttribute("data-testname"); + promise_test(async t => { + const expectedRoles = roles; + const computedRole = await test_driver.get_computed_role(el); + for (role of roles){ + if (computedRole === role) { + return assert_equals(computedRole, role, `Computed Role: "${computedRole}" matches one of the acceptable role strings in ["${roles.join('", "')}"]: ${el.outerHTML}`); + } + } + return assert_false(true, `Computed Role: "${computedRole}" does not match any of the acceptable role strings in ["${roles.join('", "')}"]: ${el.outerHTML}`); + }, `${testName}`); + } + }, + + + /* + Helper function for "generic" ROLE tests. + + Ex: <span + data-testname="verify generic, none, or empty computed role on span" + class="ex-generic"> + + AriaUtils.verifyGenericRolesBySelector(".ex-generic"); + + This helper function is equivalant to AriaUtils.verifyRoleOrVariantRolesBySelector(".ex-generic", ["generic", "", "none"]); + See various issues and discussions linked from https://github.com/web-platform-tests/interop-accessibility/issues/48 + + */ + verifyGenericRolesBySelector: function(selector) { + // ARIA WG determined implementation variants "none" (Chromium), and the empty string "" (WebKit), are sufficiently equivalent to "generic" for WPT test verification of HTML-AAM. + // See various discussions linked from https://github.com/web-platform-tests/interop-accessibility/issues/48 + this.verifyRoleOrVariantRolesBySelector(selector, ["generic", "", "none"]); + }, + + + /* + Tests computed LABEL of all elements matching selector + against the string value of their data-expectedlabel attribute. + + Ex: <div aria-label="foo" + data-testname="optional unique test name" + data-expectedlabel="foo" + class="ex"> + + AriaUtils.verifyLabelsBySelector(".ex") + + */ + verifyLabelsBySelector: function(selector, labelTestNamePrefix) { + const els = document.querySelectorAll(selector); + if (!els.length) { + throw `Selector passed in verifyLabelsBySelector("${selector}") should match at least one element.`; + } + for (const el of els) { + let label = el.getAttribute("data-expectedlabel"); + let testName = el.getAttribute("data-testname") || label; // data-testname optional if label is unique per test file + if (typeof labelTestNamePrefix !== "undefined") { + testName = labelTestNamePrefix + testName; + } + promise_test(async t => { + const expectedLabel = el.getAttribute("data-expectedlabel"); + let computedLabel = await test_driver.get_computed_label(el); + assert_not_equals(computedLabel, null, `get_computed_label(el) shouldn't return null for ${el.outerHTML}`); + + // See: + // - https://github.com/w3c/accname/pull/165 + // - https://github.com/w3c/accname/issues/192 + // - https://github.com/w3c/accname/issues/208 + // + // AccName references HTML's definition of ASCII Whitespace + // https://infra.spec.whatwg.org/#ascii-whitespace + // which matches tab (\t), newline (\n), formfeed (\f), return (\r), and regular space (\u0020). + // but it does NOT match non-breaking space (\xA0,\u00A0) and others matched by \s + const asciiWhitespace = /[\t\n\f\r\u0020]+/g; + computedLabel = computedLabel.replace(asciiWhitespace, '\u0020').replace(/^\u0020|\u0020$/g, ''); + + assert_equals(computedLabel, expectedLabel, el.outerHTML); + }, `${testName}`); + } + }, + + + /* + Tests computed LABEL and ROLE of all elements matching selector using existing + verifyLabelsBySelector(), verifyRolesBySelector() functions and passes a test name prefix + to ensure uniqueness. + + Ex: <div aria-label="foo" role="button" + data-testname="div with role=button is labelled via aria-label" + data-expectedlabel="foo" + data-expectedrole="button" + class="ex-role-and-label"> + + AriaUtils.verifyRolesAndLabelsBySelector(".ex-role-and-label") + + */ + verifyRolesAndLabelsBySelector: function(selector) { + let labelTestNamePrefix = "Label: "; + let roleTestNamePrefix = "Role: "; + const els = document.querySelectorAll(selector); + if (!els.length) { + throw `Selector passed in verifyRolesAndLabelsBySelector("${selector}") should match at least one element.`; + } + for (const el of els) { + el.classList.add("ex-label-only"); + el.classList.add("ex-role-only"); + } + this.verifyLabelsBySelector(".ex-label-only", labelTestNamePrefix); + this.verifyRolesBySelector(".ex-role-only", roleTestNamePrefix); + }, +}; + diff --git a/testing/web-platform/tests/wai-aria/scripts/manual.css b/testing/web-platform/tests/wai-aria/scripts/manual.css new file mode 100644 index 0000000000..0e4f4a03e4 --- /dev/null +++ b/testing/web-platform/tests/wai-aria/scripts/manual.css @@ -0,0 +1,70 @@ +html { + font-family:DejaVu Sans, Bitstream Vera Sans, Arial, Sans; +} + +table#steps { + border-collapse:collapse; + table-layout:fixed; + width:100%; +} + +table#steps th:last-child, +table#steps td:last-child { + width:70%; +} + +// table#steps.assertions th:last-child, +// table#steps.assertions td:last-child { +// width:35%; +// } + +table#steps th { + padding:0; + padding-bottom:0.25; + border-bottom:medium solid black; +} + +table#steps td { + padding-top:0.25em; + padding-bottom:0.25em; + padding-left:0.5em; + padding-right:0.5em; + border-right:thin solid black; + border-top:thin solid black; + border-bottom:thin solid black; +} + +table#steps td.step, table#steps td.type, table#steps td.element { + vertical-align:top; +} +table#api { + border-collapse:collapse; + table-layout:fixed; + width:100%; +} + +// table#steps.assertions th:last-child, +// table#steps.assertions td:last-child { +// width:35%; +// } + +table#api th { + padding:0; + padding-bottom:0.25; + border-bottom:medium solid black; +} + +table#api td { + padding-top:0.25em; + padding-bottom:0.25em; + padding-left:0.5em; + padding-right:0.5em; + border-right:thin solid black; + border-top:thin solid black; + border-bottom:thin solid black; +} + +table#api td.step, table#api td.type, table#api td.element { + vertical-align:top; +} +} |