/* 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 = "

Manual Mode Enabled

"+message+"

"; if (this.Tests.hasOwnProperty("description")) { content += "

" + this.Tests.description + "

"; } var theTable = ""; 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 += ""; theTable += ""; theTable += ""; // now what do we put over here? depends on the type if (type === "test") { // it is a test; dump the assertions theTable += ""; } else if (type === "attribute" ) { if (subtest.hasOwnProperty("attribute") && subtest.hasOwnProperty("value") && subtest.hasOwnProperty("element")) { if (subtest.value === "none") { theTable += ""; } else { theTable += ""; } } } else if (type === "event" ) { // it is some events if (subtest.hasOwnProperty("event") && subtest.hasOwnProperty("element")) { theTable += ""; } } else if (type === "script" ) { // it is a script fragment theTable += ""; } else { theTable += ""; } theTable += ""; }.bind(this)); theTable += "
StepTypeElement IDAssertions
" + subtest.title +"" + type + "" + id +"" + this.buildAssertionTable(subtest.test) + "Remove attribute " + subtest.attribute + " from the element with ID " + subtest.element + "Set attribute " + subtest.attribute + " on the element with ID " + subtest.element + " to the value " + subtest.value + "Send event " + subtest.event + " to the element with ID " + subtest.element + "Script: " + subtest.script + "Unknown type: " + type + "
"; ref.innerHTML = content + theTable ; } }, buildAssertionTable: function(asserts) { "use strict"; var output = ""; 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 += ""; 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 += "" + item + ""; }); output += ""; if (index < lastRow) { output += ""; } }); }); output += "
API NameAssertions
"+theAPI+"
"; 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 = "

Logging information recorded

"; if (this.startResponse && this.startResponse.hasOwnProperty("API")) { content += "

ATTA Information

"; content += "
"+JSON.stringify(this.startResponse, null, "  ")+"
"; } content += ""; 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: