tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

ATTAcomm.js (31437B)


      1 /* globals Promise, window, done, assert_true, on_event, promise_test */
      2 
      3 /**
      4 * Creates an ATTAcomm object.  If the parameters are supplied
      5 * it sets up event listeners to send the test data to an ATTA if one
      6 * is available.  If the ATTA does not respond, it will assume the test
      7 * is being done manually and the results are being entered in the
      8 * parent test window.
      9 *
     10 * @constructor
     11 * @param {object} params
     12 * @param {string} [params.test] - object containing JSON test definition
     13 * @param {string} [params.testFile] - URI of a file with JSON test definition
     14 * @param {string} params.ATTAuri - URI to use to exercise the window
     15 * @event DOMContentLoaded Calls go once DOM is fully loaded
     16 * @returns {object} Reference to the new object
     17 *
     18 */
     19 
     20 function ATTAcomm(params) {
     21  'use strict';
     22 
     23  this.Params = null;       // parameters passed in
     24  this.Promise = null;      // master Promise that resolves when intialization is complete
     25  this.Properties = null;   // testharness_properties from the opening window
     26  this.Tests = null;        // test object being processed
     27  this.testName = "";       // name of test being run
     28  this.log = "";            // a buffer to capture log information for debugging
     29  this.startReponse = {};   // startTest response will go in here for debugging
     30 
     31  this.loading = true;
     32 
     33  this.timeout = 5000;
     34 
     35  var pending = [] ;
     36 
     37  // set up in case DOM finishes loading early
     38  pending.push(new Promise(function(resolve) {
     39    on_event(document, "DOMContentLoaded", function() {
     40        resolve(true);
     41    }.bind(this));
     42  }.bind(this)));
     43 
     44  // if we are under runner, then there are props in the parent window
     45  //
     46  // if "output" is set in that, then pause at the end of running so the output
     47  // can be analyzed. @@@TODO@@@
     48  if (window && window.opener && window.opener.testharness_properties) {
     49    this.Properties = window.opener.testharness_properties;
     50  }
     51 
     52  this.Params = params;
     53 
     54  if (this.Params.hasOwnProperty("ATTAuri")) {
     55    this.ATTAuri = this.Params.ATTAuri;
     56  } else {
     57    this.ATTAuri = "http://localhost:4119";
     58  }
     59 
     60  if (this.Params.hasOwnProperty("title")) {
     61    this.testName = this.Params.title;
     62  }
     63 
     64  // start by loading the test (it might be inline, but
     65  // loadTest deals with that
     66  pending.push(this.loadTest(params)
     67    .then(function(tests) {
     68      // if the test is NOT an object, turn it into one
     69      if (typeof tests === 'string') {
     70        tests = JSON.parse(tests) ;
     71      }
     72 
     73      this.Tests = tests;
     74 
     75    }.bind(this)));
     76 
     77  this.Promise = new Promise(function(resolve, reject) {
     78    // once the DOM and the test is loaded... set us up
     79    Promise.all(pending)
     80    .then(function() {
     81      // Everything is loaded
     82      this.loading = false ;
     83      // run the automated tests (or setup for manual testing)
     84      this.go();
     85      resolve(this);
     86    }.bind(this))
     87    .catch(function(err) {
     88      // loading the components failed somehow - report the errors and mark the test failed
     89      test( function() {
     90        assert_true(false, "Loading of test components failed: " +JSON.stringify(err)) ;
     91      }, "Loading test components");
     92      this.dumpLog();
     93      done() ;
     94      reject("Loading of test components failed: "+JSON.stringify(err));
     95      return ;
     96    }.bind(this));
     97  }.bind(this));
     98 
     99  return this;
    100 }
    101 
    102 ATTAcomm.prototype = {
    103 
    104  /**
    105   * go sets up the connection to the ATTA
    106   *
    107   * If that succeeds and the tests in this test file have methods for
    108   * the API supported by the ATTA, then it automatically runs those tests.
    109   *
    110   * Otherwise it sets up for manualt testing.
    111   */
    112  go: function() {
    113    'use strict';
    114    // everything is ready.  Let's talk to the ATTA
    115    this.startTest().then(function(res) {
    116 
    117      // start was successful - iterate over steps
    118      var API = res.body.API;
    119 
    120      var subtestsForAPI = false;
    121 
    122      // check main and potentially nested lists of tests for
    123      // tests with this API.  If any step is missing this API
    124      // mapping, then we need to be manual
    125      this.Tests.forEach(function(subtest) {
    126        if (subtest.hasOwnProperty("test") &&
    127            subtest.test.hasOwnProperty(API)) {
    128          // there is at least one subtest for this API so
    129          // this is a test that needs to be looked at by an atta
    130          subtestsForAPI = true;
    131        } else if (Array.isArray(subtest)) {
    132          subtest.forEach(function(st) {
    133            if (st.hasOwnProperty("test") &&
    134                st.test.hasOwnProperty(API)) {
    135              subtestsForAPI = true;
    136            }
    137          });
    138        }
    139      });
    140 
    141      if (subtestsForAPI) {
    142        this.runTests(API, this.Tests)
    143        .then(function() {
    144          // the tests all ran; close it out
    145          this.endTest().then(function() {
    146            this.dumpLog();
    147            done();
    148          }.bind(this));
    149        }.bind(this))
    150        .catch(function(err) {
    151          this.endTest().then(function() {
    152            this.dumpLog();
    153            done();
    154          }.bind(this));
    155        }.bind(this));
    156      } else {
    157        // we don't know this API for this test
    158        // but we ARE talking to an ATTA; skip this test
    159        this.dumpLog();
    160        if (window.opener && window.opener.completion_callback) {
    161          window.opener.completion_callback([], { status: 3, message: "No steps for AT API " + API } );
    162        } else {
    163          done();
    164        }
    165        // this.setupManualTest("Unknown AT API: " + API);
    166      }
    167    }.bind(this))
    168    .catch(function(res) {
    169      // startTest failed so just sit and wait for a manual test to occur
    170      if (res.timeout || res.status === 102) {
    171        this.setupManualTest("No response from ATTA at " + this.ATTAuri);
    172      } else if (res.status === 200 ) {
    173        this.setupManualTest(res.message);
    174      } else if (res.statusText === "No response from ATTA") {
    175        this.setupManualTest("");
    176      } else {
    177        this.setupManualTest("Error from ATTA: " + res.status + ": " + res.statusText);
    178      }
    179    }.bind(this));
    180  },
    181 
    182  runTests: function(API, collection) {
    183    // this method returns a promise
    184 
    185    return new Promise(function(resolve, reject) {
    186      // accumulate promises; complete when done
    187      var pending = [];
    188      var testCount = 0;
    189 
    190      this.sendEvents(API, collection)
    191      .then(function(eventStatus) {
    192 
    193        /* Loop strategy...
    194         *
    195         * If the step is a 'test' then push it into the pending queue as a promise
    196         *
    197         * If the step is anything else, then if there is anything in pending, wait on it
    198         * Once it resolves, clear the queue and then execute the other step.
    199         *
    200         */
    201        collection.forEach(function(subtest) {
    202          //  what "type" of step in the sequence is this?
    203          var theType = "test" ;
    204          if (Array.isArray(subtest)) {
    205            // it is a group
    206            Promise.all(pending).then(function() {
    207              pending = [];
    208              // recursively run the tests
    209              pending.push(this.runTests(API, subtest));
    210            }.bind(this));
    211          } else if (subtest.hasOwnProperty("type")) {
    212            theType = subtest.type;
    213          }
    214          testCount++;
    215          if (theType === "test") {
    216            // this is a set of assertions that should be evaluated
    217            pending.push(this.runTest(testCount, API, subtest));
    218          } else if (theType === "script") {
    219            Promise.all(pending).then(function() {
    220              pending = [];
    221              // execute the script
    222              this.runScript(testCount, subtest);
    223            }.bind(this));
    224          } else if (theType === "attribute") {
    225            Promise.all(pending).then(function() {
    226              pending = [];
    227              // raise the event
    228              this.handleAttribute(testCount, subtest);
    229            }.bind(this));
    230          // } else {
    231          } else if (theType === "event") {
    232            Promise.all(pending).then(function() {
    233              pending = [];
    234              // raise the event
    235              this.raiseEvent(testCount, subtest);
    236            }.bind(this));
    237          // } else {
    238          }
    239        }.bind(this));
    240 
    241        Promise.all(pending)
    242        .then(function() {
    243          // this collection all ran
    244          if (eventStatus !== "NOEVENTS") {
    245            // there were some events at the beginning
    246            this.sendStopListen().then(function() {
    247              resolve(true);
    248            });
    249          } else {
    250            resolve(true);
    251          }
    252        }.bind(this));
    253      }.bind(this));
    254    }.bind(this));
    255  },
    256 
    257  setupManualTest: function(message) {
    258    // if we determine the test should run manually, then expose all of the conditions that are
    259    // in the TEST data structure so that a human can to the inspection and calculate the result
    260    //
    261    'use strict';
    262 
    263    var ref = document.getElementById("manualMode");
    264    if (ref) {
    265      // we have a manualMode block.  Populate it
    266      var content = "<h2>Manual Mode Enabled</h2><p>"+message+"</p>";
    267      if (this.Tests.hasOwnProperty("description")) {
    268        content += "<p>" + this.Tests.description + "</p>";
    269      }
    270      var theTable = "<table id='steps'><tr><th>Step</th><th>Type</th><th>Element ID</th><th>Assertions</th></tr>";
    271      this.Tests.forEach(function(subtest) {
    272        var type = "test";
    273        if (subtest.hasOwnProperty("type")) {
    274          type = subtest.type;
    275        }
    276        var id = "" ;
    277        if (subtest.hasOwnProperty("element")) {
    278          id = subtest.element;
    279        }
    280        theTable += "<tr><td class='step'>" + subtest.title +"</td>";
    281        theTable += "<td class='type'>" + type + "</td>";
    282        theTable += "<td class='element'>" + id +"</td>";
    283 
    284        // now what do we put over here? depends on the type
    285        if (type === "test") {
    286          // it is a test; dump the assertions
    287          theTable += "<td>" + this.buildAssertionTable(subtest.test) + "</td>";
    288        } else if (type === "attribute" ) {
    289          if (subtest.hasOwnProperty("attribute") && subtest.hasOwnProperty("value") && subtest.hasOwnProperty("element")) {
    290            if (subtest.value === "none") {
    291              theTable += "<td>Remove attribute <code>" + subtest.attribute + "</code> from the element with ID <code>" + subtest.element + "</code></td>";
    292            } else {
    293              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>";
    294            }
    295          }
    296        } else if (type === "event" ) {
    297          // it is some events
    298          if (subtest.hasOwnProperty("event") && subtest.hasOwnProperty("element")) {
    299            theTable += "<td>Send event <code>" + subtest.event + "</code> to the element with ID <code>" + subtest.element + "</code></td>";
    300          }
    301        } else if (type === "script" ) {
    302          // it is a script fragment
    303          theTable += "<td>Script: " + subtest.script + "</td>";
    304        } else {
    305          theTable += "<td>Unknown type: " + type + "</td>";
    306        }
    307        theTable += "</tr>";
    308 
    309 
    310      }.bind(this));
    311 
    312      theTable += "</table>";
    313      ref.innerHTML = content + theTable ;
    314    }
    315  },
    316 
    317  buildAssertionTable:  function(asserts) {
    318    "use strict";
    319    var output = "<table class='api'><tr><th>API Name</th><th colspan='4'>Assertions</th></tr>";
    320    var APIs = [] ;
    321    for (var k in asserts) {
    322      if (asserts.hasOwnProperty(k)) {
    323        APIs.push(k);
    324      }
    325    }
    326 
    327    APIs.sort().forEach(function(theAPI) {
    328      var rows = asserts[theAPI] ;
    329      var height = rows.length;
    330      output += "<tr><td rowspan='" + height + "' class='apiName'>"+theAPI+"</td>";
    331      var lastRow = rows.length - 1;
    332      rows.forEach(function(theRow, index) {
    333        var span = 4 - theRow.length;
    334        var colspan = span ? " colspan='"+span+"'" : "";
    335        theRow.forEach(function(item) {
    336          output += "<td" + colspan + ">" + item + "</td>";
    337        });
    338        output += "</tr>";
    339        if (index < lastRow) {
    340          output += "<tr>";
    341        }
    342      });
    343    });
    344 
    345    output += "</table>";
    346    return output;
    347  },
    348 
    349  // eventList - find the events for an API
    350  //
    351  // @param {string} API
    352  // @param {array} collection - a collection of tests
    353  // @returns {array} list of event names
    354 
    355  eventList: function(API, collection) {
    356    var eventHash = {};
    357 
    358    if (!API || API === "") {
    359      return [];
    360    }
    361 
    362    collection.forEach(function(subtest) {
    363      if (subtest.hasOwnProperty("test") &&
    364          subtest.test.hasOwnProperty(API)) {
    365        // this is a subtest for this API; look at the events
    366        subtest.test[API].forEach(function(assert) {
    367          // look for event names
    368          if (assert[0] === "event" && assert[1] === "type" && assert[2] === "is") {
    369            eventHash[assert[3]] = 1;
    370          }
    371        });
    372      }
    373    });
    374 
    375    return Object.keys(eventHash);
    376  },
    377 
    378  // handleAttribute - set or clear an attribute
    379  /**
    380   * @param {integer} testNum - The subtest number
    381   * @param {object} subtest - attribute information to set
    382   */
    383  handleAttribute: function(testNum, subtest) {
    384    "use strict";
    385    if (subtest) {
    386      if (subtest.hasOwnProperty("attribute") && subtest.hasOwnProperty("element") && subtest.hasOwnProperty("value")) {
    387        // update an attribute
    388        try {
    389          var node = document.getElementById(subtest.element);
    390          if (node) {
    391            if (subtest.value === "none") {
    392              // remove this attribute
    393              node.removeAttribute(subtest.attribute);
    394            } else if (subtest.value === '""') {
    395              node.setAttribute(subtest.attribute, "");
    396            } else if (subtest.value.match(/^"/) ) {
    397              var v = subtest.value;
    398              v = v.replace(/^"/, '');
    399              v = v.replace(/"$/, '');
    400              node.setAttribute(subtest.attribute, v);
    401            } else {
    402              node.setAttribute(subtest.attribute, subtest.value);
    403            }
    404          }
    405        }
    406        catch (e) {
    407          test(function() {
    408            assert_true(false, "Subtest attribute failed to update: " +e);
    409          }, "Attribute subtest " + testNum);
    410        }
    411      } else {
    412        test(function() {
    413          var err = "";
    414          if (!subtest.hasOwnProperty("attribute")) {
    415            err += "Attribute subtest has no attribute property; ";
    416          } else if (!subtest.hasOwnProperty("value")) {
    417            err += "Attribute subtest has no value property; ";
    418          } else if (!subtest.hasOwnProperty("element")) {
    419            err += "Attribute subtest has no element property; ";
    420          }
    421          assert_true(false, err);
    422        }, "Attribute subtest " + testNum );
    423      }
    424    }
    425    return;
    426  },
    427 
    428 
    429 
    430  // raiseEvent - throw an event at an item
    431  /**
    432   * @param {integer} testNum - The subtest number
    433   * @param {object} subtest - event information to throw
    434   */
    435  raiseEvent: function(testNum, subtest) {
    436    "use strict";
    437    var evt;
    438    if (subtest) {
    439      var kp = function(target, key) {
    440        evt = document.createEvent("KeyboardEvent");
    441        evt.initKeyEvent ("keypress", true, true, window,
    442                          0, 0, 0, 0, 0, "e".charCodeAt(0));
    443        target.dispatchEvent(evt);
    444      };
    445      if (subtest.hasOwnProperty("event") && subtest.hasOwnProperty("element")) {
    446        // throw an event
    447        try {
    448          var node = document.getElementById(subtest.element);
    449          if (node) {
    450            if (subtest.event === "focus") {
    451              node.focus();
    452            } else if (subtest.event === "select") {
    453              node.click();
    454            } else if (subtest.event.startsWith('key:')) {
    455              var key = subtest.event.replace('key:', '');
    456              evt = new KeyboardEvent("keypress", { "key": key});
    457              node.dispatchEvent(evt);
    458            } else {
    459              evt = new Event(subtest.element);
    460              node.dispatchEvent(evt);
    461            }
    462          }
    463        }
    464        catch (e) {
    465          test(function() {
    466            assert_true(false, "Subtest event failed to dispatch: " +e);
    467          }, "Event subtest " + testNum);
    468        }
    469      } else {
    470        test(function() {
    471          var err = "";
    472          if (!subtest.hasOwnProperty("event")) {
    473            err += "Event subtest has no event property; ";
    474          } else if (!subtest.hasOwnProperty("element")) {
    475            err += "Event subtest has no element property; ";
    476          }
    477          assert_true(false, err);
    478        }, "Event subtest " + testNum );
    479      }
    480    }
    481    return;
    482  },
    483 
    484  // runScript - run a script in the context of the window
    485  /**
    486   * @param {integer} testNum - The subtest number
    487   * @param {object} subtest - script and related information
    488   */
    489  runScript: function(testNum, subtest) {
    490    "use strict";
    491    if (subtest) {
    492      if (subtest.hasOwnProperty("script") && typeof subtest.script === "string") {
    493        try {
    494          /* jshint evil:true */
    495          eval(subtest.script);
    496        }
    497        catch (e) {
    498          test(function() {
    499            assert_true(false, "Subtest script " + subtest.script + " failed to evaluate: " +e);
    500          }, "Event subtest " + testNum);
    501        }
    502      } else {
    503        test(function() {
    504          assert_true(false, "Event subtest has no script property");
    505        }, "Event subtest " + testNum );
    506      }
    507    }
    508    return;
    509  },
    510 
    511  // runTest - process subtest
    512  /**
    513   * @param {integer} testNum - The subtest number
    514   * @param {string} API - name of the API being tested
    515   * @param {object} subtest - a subtest to run; contains 'title', 'element', and
    516   * 'test array'
    517   * @returns {Promise} - a Promise that resolves when the test completes
    518   */
    519  runTest: function(testNum, API, subtest) {
    520    'use strict';
    521 
    522    var data = {
    523      "title" : subtest.title,
    524      "id" : subtest.element,
    525      "data": this.normalize(subtest.test[API])
    526    };
    527 
    528    return new Promise(function(resolve) {
    529      var ANNO = this;
    530      if (subtest.test[API]) {
    531        // we actually have a test to run
    532        promise_test(function() {
    533          // force a resolve of the promise regardless
    534          this.add_cleanup(function() { resolve(true); });
    535          return ANNO.sendTest(data)
    536            .then(function(res) {
    537              if (typeof res.body === "object" && res.body.hasOwnProperty("status")) {
    538                // we got some sort of response
    539                if (res.body.status === "OK") {
    540                  // the test ran - yay!
    541                  var messages = "";
    542                  var thisResult = null;
    543                  var theLog = "";
    544                  var assertionCount = 0;
    545                  res.body.results.forEach( function (a) {
    546                    if (typeof a === "object") {
    547                      // we have a result for this assertion
    548                      // first, what is the assertion?
    549                      var aRef = data.data[assertionCount];
    550                      var assertionText = '"' + aRef.join(" ") +'"';
    551 
    552                      if (a.hasOwnProperty("log") && a.log !== null && a.log !== '' ) {
    553                        // there is log data - save it
    554                        theLog += "\n--- Assertion " + assertionCount + " ---";
    555                        theLog += "\nAssertion: " + assertionText + "\nLog data: "+a.log ;
    556                      }
    557 
    558                      // is there a message?
    559                      var theMessage = "";
    560                      if (a.hasOwnProperty("message")) {
    561                        theMessage = a.message;
    562                      }
    563                      if (!a.hasOwnProperty("result")) {
    564                        messages += "ATTA did not report a result " + theMessage + "; ";
    565                      } else if (a.result === "ERROR") {
    566                        messages += "ATTA reported ERROR with message: " + theMessage + "; ";
    567                      } else if (a.result === "FAIL") {
    568                        thisResult = false;
    569                        messages += assertionText + " failed " + theMessage + "; ";
    570                      } else if (a.result === "PASS" && thisResult === null) {
    571                        // if we got a pass and there was no other result thus far
    572                        // then we are passing
    573                        thisResult = true;
    574                      }
    575                    }
    576                    assertionCount++;
    577                  });
    578                  if (theLog !== "") {
    579                    ANNO.saveLog("runTest", theLog, subtest);
    580                  }
    581                  if (thisResult !== null) {
    582                    assert_true(thisResult, messages);
    583                  } else {
    584                    assert_true(false, "ERROR: No results reported from ATTA; " + messages);
    585                  }
    586                } else if (res.body.status === "ERROR") {
    587                  assert_true(false, "ATTA returned ERROR with message: " + res.body.statusText);
    588                } else {
    589                  assert_true(false, "ATTA returned unknown status " + res.body.status + " with message: " + res.body.statusText);
    590                }
    591              } else {
    592                // the return wasn't an object!
    593                assert_true(false, "ATTA failed to return a result object: returned: "+JSON.stringify(res));
    594              }
    595            });
    596        }, subtest.name );
    597      } else {
    598        // there are no test steps for this API.  fake a subtest result
    599        promise_test(function() {
    600          // force a resolve of the promise regardless
    601          this.add_cleanup(function() { resolve(true); });
    602          return new Promise(function(innerResolve) {
    603            innerResolve(true);
    604          })
    605          .then(function(res) {
    606            var theLog = "\nSUBTEST NOTRUN: No assertions for API " + API + "\n";
    607            if (theLog !== "") {
    608              ANNO.saveLog("runTest", theLog, subtest);
    609            }
    610            assert_false(true, "NOTRUN: No assertion for API " + API);
    611          });
    612        }, subtest.name );
    613      }
    614    }.bind(this));
    615  },
    616 
    617  // loadTest - load a test from an external JSON file
    618  //
    619  // returns a promise that resolves with the contents of the
    620  // test
    621 
    622  loadTest: function(params) {
    623    'use strict';
    624 
    625    if (params.hasOwnProperty('stepFile')) {
    626      // the test is referred to by a file name
    627      return this._fetch("GET", params.stepFile);
    628    } // else
    629    return new Promise(function(resolve, reject) {
    630      if (params.hasOwnProperty('steps')) {
    631        resolve(params.steps);
    632      } else {
    633        reject("Must supply a 'steps' or 'stepFile' parameter");
    634      }
    635    });
    636  },
    637 
    638  /* dumpLog - put log information into the log div on the page if it exists
    639   */
    640 
    641  dumpLog: function() {
    642    'use strict';
    643    if (this.log !== "") {
    644      var ref = document.getElementById("ATTAmessages");
    645      if (ref) {
    646        // we have a manualMode block.  Populate it
    647        var content = "<h2>Logging information recorded</h2>";
    648        if (this.startResponse && this.startResponse.hasOwnProperty("API")) {
    649          content += "<h3>ATTA Information</h3>";
    650          content += "<pre>"+JSON.stringify(this.startResponse, null, "  ")+"</pre>";
    651        }
    652        content += "<textarea rows='50' style='width:100%'>"+this.log+"</textarea>";
    653        ref.innerHTML = content ;
    654      }
    655    }
    656  },
    657 
    658  /* saveLog - capture logging information so that it can be displayed on the page after testing is complete
    659   *
    660   * @param {string} caller name
    661   * @param {string} log message
    662   * @param {object} subtest
    663   */
    664 
    665  saveLog: function(caller, message, subtest) {
    666    'use strict';
    667 
    668    if (typeof message === "string" && message !== "") {
    669      this.log += "============================================================\n";
    670      this.log += "Message from " + caller + "\n";
    671      if (subtest && typeof subtest === "object") {
    672        var API = this.startResponse.API;
    673        this.log += "\n    SUBTEST TITLE: " + subtest.title;
    674        this.log += "\n  SUBTEST ELEMENT: " + subtest.element;
    675        this.log += "\n     SUBTEST DATA: " + JSON.stringify(subtest.test[API]);
    676        this.log += "\n\n";
    677      }
    678      this.log += message;
    679    }
    680    return;
    681  },
    682 
    683  // startTest - send the test start message
    684  //
    685  // @returns {Promise} resolves if the start is successful, or rejects with
    686 
    687  startTest: function() {
    688    'use strict';
    689 
    690    return new Promise(function(resolve, reject) {
    691      var params = {
    692        test: this.testName || window.title,
    693        url: document.location.href
    694      };
    695 
    696      this._fetch("POST", this.ATTAuri + "/start", null, params)
    697      .then(function(res) {
    698        if (res.body.hasOwnProperty("status")) {
    699          if (res.body.status === "READY") {
    700            this.startResponse = res.body;
    701            if (res.body.hasOwnProperty("log")) {
    702              // there is some logging data - capture it
    703              this.saveLog("startTest", res.body.log);
    704            }
    705            // the system is ready for us - is it really?
    706            if (res.body.hasOwnProperty("API")) {
    707              resolve(res);
    708            } else {
    709              res.message = "No API in response from ATTA";
    710              reject(res);
    711            }
    712          } else {
    713            // the system reported something else - fail out with the statusText as a result
    714            res.message = "ATTA reported an error: " + res.body.statusText;
    715            reject(res);
    716          }
    717        } else {
    718          res.message = "ATTA did not report a status";
    719          reject(res);
    720        }
    721      }.bind(this))
    722      .catch(function(res) {
    723        reject(res);
    724      });
    725    }.bind(this));
    726  },
    727 
    728  // sendEvents - send the list of events the ATTA needs to listen for
    729  //
    730  // @param {string} API
    731  // @param {array} collection - a list of tests
    732  // @returns {Promise} resolves if the message is successful, or rejects with
    733 
    734  sendEvents: function(API, collection) {
    735    'use strict';
    736 
    737    return new Promise(function(resolve, reject) {
    738      var eList = this.eventList(API, collection) ;
    739      if (eList && eList.length) {
    740        var params = {
    741          events: eList
    742        };
    743 
    744        this._fetch("POST", this.ATTAuri + "/startlisten", null, params)
    745        .then(function(res) {
    746          if (res.body.hasOwnProperty("status")) {
    747            if (res.body.status === "READY") {
    748              if (res.body.hasOwnProperty("log")) {
    749                // there is some logging data - capture it
    750                this.saveLog("sendEvents", res.body.log);
    751              }
    752              resolve(res.body.status);
    753            } else {
    754              // the system reported something else - fail out with the statusText as a result
    755              res.message = "ATTA reported an error: " + res.body.statusText;
    756              reject(res);
    757            }
    758          } else {
    759            res.message = "ATTA did not report a status";
    760            reject(res);
    761          }
    762        }.bind(this))
    763        .catch(function(res) {
    764          reject(res);
    765        });
    766      } else {
    767        // there are no events
    768        resolve("NOEVENTS");
    769      }
    770    }.bind(this));
    771  },
    772 
    773  sendStopListen: function() {
    774    'use strict';
    775 
    776    return this._fetch("POST", this.ATTAuri + "/stoplisten", null, null);
    777  },
    778 
    779  // sendTest - send test data to an ATTA and wait for a response
    780  //
    781  // returns a promise that resolves with the results of the test
    782 
    783  sendTest: function(testData) {
    784    'use strict';
    785 
    786    if (typeof testData !== "string") {
    787      testData = JSON.stringify(testData);
    788    }
    789    var ret = this._fetch("POST", this.ATTAuri + "/test", null, testData, true);
    790    ret.then(function(res) {
    791      if (res.body.hasOwnProperty("log")) {
    792        // there is some logging data - capture it
    793        this.saveLog("sendTest", res.body.log);
    794      }
    795    }.bind(this));
    796    return ret;
    797  },
    798 
    799  endTest: function() {
    800    'use strict';
    801 
    802    return this._fetch("GET", this.ATTAuri + "/end");
    803  },
    804 
    805  /* normalize - ensure subtest data conforms to ATTA spec
    806   */
    807 
    808  normalize: function( data ) {
    809    'use strict';
    810 
    811    var ret = [] ;
    812 
    813    if (data) {
    814      data.forEach(function(assert) {
    815        var normal = [] ;
    816        // ensure if there is a value list it is compressed
    817        if (Array.isArray(assert)) {
    818          // we have an array
    819          normal[0] = assert[0];
    820          normal[1] = assert[1];
    821          normal[2] = assert[2];
    822          if ("string" === typeof assert[3] && assert[3].match(/^\[.*\]$/)) {
    823            // it is a string and matches the valuelist pattern
    824            normal[3] = assert[3].replace(/, +/, ',');
    825          } else {
    826            normal[3] = assert[3];
    827          }
    828          ret.push(normal);
    829        } else {
    830          ret.push(assert);
    831        }
    832      });
    833    }
    834    return ret;
    835  },
    836 
    837  // _fetch - return a promise after sending data
    838  //
    839  // Resolves with the returned information in a structure
    840  // including:
    841  //
    842  // xhr - a raw xhr object
    843  // headers - an array of headers sent in the request
    844  // status - the status code
    845  // statusText - the text of the return status
    846  // text - raw returned data
    847  // body - an object parsed from the returned content
    848  //
    849 
    850  _fetch: function (method, url, headers, content, parse) {
    851    'use strict';
    852    if (method === null || method === undefined) {
    853      method = "GET";
    854    }
    855    if (parse === null || parse === undefined) {
    856      parse = true;
    857    }
    858    if (headers === null || headers === undefined) {
    859      headers = [];
    860    }
    861 
    862 
    863    // note that this Promise always resolves - there is no reject
    864    // condition
    865 
    866    return new Promise(function (resolve, reject) {
    867      var xhr = new XMLHttpRequest();
    868 
    869      // this gets returned when the request completes
    870      var resp = {
    871        xhr: xhr,
    872        headers: null,
    873        status: 0,
    874        statusText: "",
    875        body: null,
    876        text: ""
    877      };
    878 
    879      xhr.open(method, url);
    880 
    881      // headers?
    882      headers.forEach(function(ref) {
    883        xhr.setRequestHeader(ref[0], ref[1]);
    884      });
    885 
    886      //if (this.timeout) {
    887      //  xhr.timeout = this.timeout;
    888      //}
    889 
    890      xhr.ontimeout = function() {
    891        resp.timeout = this.timeout;
    892        resolve(resp);
    893      };
    894 
    895      xhr.onerror = function() {
    896        if (this.status) {
    897          resp.status = this.status;
    898          resp.statusText = xhr.statusText;
    899        } else if (this.status === 0) {
    900          resp.status = 0;
    901          resp.statusText = "No response from ATTA";
    902        }
    903        reject(resp);
    904      };
    905 
    906      xhr.onload = function () {
    907        resp.status = this.status;
    908        if (this.status >= 200 && this.status < 300) {
    909          var d = xhr.response;
    910          // return the raw text of the response
    911          resp.text = d;
    912          // we have it; what is it?
    913          if (parse) {
    914            try {
    915              d = JSON.parse(d);
    916              resp.body = d;
    917            }
    918            catch(err) {
    919              resp.body = null;
    920            }
    921          }
    922          resolve(resp);
    923        } else {
    924          reject({
    925            status: this.status,
    926            statusText: xhr.statusText
    927          });
    928        }
    929      };
    930 
    931      if (content !== null && content !== undefined) {
    932        if ("object" === typeof(content)) {
    933          xhr.send(JSON.stringify(content));
    934        } else if ("function" === typeof(content)) {
    935          xhr.send(content());
    936        } else if ("string" === typeof(content)) {
    937          xhr.send(content);
    938        }
    939      } else {
    940        xhr.send();
    941      }
    942    });
    943  },
    944 
    945 };
    946 
    947 // vim: set ts=2 sw=2: