tor-browser

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

JSONtest.js (27229B)


      1 /* globals add_completion_callback, Promise, showdown, done, assert_true, Ajv, on_event */
      2 
      3 /**
      4 * Creates a JSONtest object.  If the parameters are supplied
      5 * it also loads a referenced testFile, processes that file, loads any
      6 * referenced external assertions, and sets up event listeners to process the
      7 * user's test data.  The loading is done asynchronously via Promises.  The test
      8 * button's text is changed to Loading while it is processing, and to "Check
      9 * JSON" once the data is loaded.
     10 *
     11 * @constructor
     12 * @param {object} params
     13 * @param {string} [params.test] - object containing JSON test definition
     14 * @param {string} [params.testFile] - URI of a file with JSON test definition
     15 * @param {string} params.runTest - IDREF of an element that when clicked will run the test
     16 * @param {string} params.testInput - IDREF of an element that contains the JSON(-LD) to evaluate against the assertions in the test / testFile
     17 * @event DOMContentLoaded Calls init once DOM is fully loaded
     18 * @returns {object} Reference to the new object
     19 */
     20 
     21 function JSONtest(params) {
     22  'use strict';
     23 
     24  this.Assertions = [];     // object that will contain the assertions to process
     25  this.AssertionText = "";  // string that holds the titles of all the assertions in use
     26  this.DescriptionText = "";
     27  this.Base = null;         // URI "base" for the test suite being run
     28  this.TestDir = null;      // URI "base" for the test case being run
     29  this.Params = null;       // paramaters passed in
     30  this.Promise = null;             // master Promise that resolves when intialization is complete
     31  this.Properties = null;   // testharness_properties from the opening window
     32  this.SkipFailures = [];   // list of assertionType values that should be skipped if their test would fail
     33  this.Test = null;         // test being run
     34  this.AssertionCounter = 0;// keeps track of which assertion is being processed
     35 
     36  this._assertionCache = [];// Array to put loaded assertions into
     37  this._assertionText = []; // Array of text or nested arrays of assertions
     38  this._loading = true;
     39 
     40  showdown.extension('strip', function() {
     41    return [
     42    { type: 'output',
     43      regex: /<p>/,
     44      replace: ''
     45    },
     46    { type: 'output',
     47      regex: /<\/p>$/,
     48      replace: ''
     49    }
     50    ];
     51  });
     52 
     53 
     54  this.markdown = new showdown.Converter({ extensions: [ 'strip' ] }) ;
     55 
     56  var pending = [] ;
     57 
     58  // set up in case DOM finishes loading early
     59  pending.push(new Promise(function(resolve) {
     60    on_event(document, "DOMContentLoaded", function() {
     61        resolve(true);
     62    }.bind(this));
     63  }.bind(this)));
     64 
     65  // create an ajv object that will stay around so that caching
     66  // of schema that are compiled just works
     67  this.ajv = new Ajv({allErrors: true, validateSchema: false}) ;
     68 
     69  // determine the base URI for the test collection.  This is
     70  // the top level folder in the test "document.location"
     71 
     72  var l = document.location;
     73  var p = l.pathname;
     74  this.TestDir = p.substr(0, 1+p.lastIndexOf('/'));
     75  this.Base = p.substr(0, 1+p.indexOf('/', 1));
     76 
     77  // if we are under runner, then there are props in the parent window
     78  //
     79  // if "output" is set in that, then pause at the end of running so the output
     80  // can be analyzed. @@@TODO@@@
     81  if (window && window.opener && window.opener.testharness_properties) {
     82    this.Properties = window.opener.testharness_properties;
     83  }
     84 
     85  this.Params = params;
     86 
     87  // if there is a list of definitions in the params,
     88  // include them
     89  if (this.Params.schemaDefs) {
     90    var defPromise = new Promise(function(resolve, reject) {
     91      var promisedSchema = this.Params.schemaDefs.map(function(item) {
     92        return this.loadDefinition(item);
     93      }.bind(this));
     94 
     95      // Once all the loadAssertion promises resolve...
     96      Promise.all(promisedSchema)
     97      .then(function (schemaContents) {
     98        this.ajv.addSchema(schemaContents);
     99        resolve(true);
    100      }.bind(this))
    101      .catch(function(err) {
    102        reject(err);
    103      }.bind(this));
    104    }.bind(this));
    105    // these schema need to load up too
    106    pending.push(defPromise) ;
    107  }
    108 
    109  // start by loading the test (it might be inline, but
    110  // loadTest deals with that
    111  pending.push(this.loadTest(params)
    112    .then(function(test) {
    113      // if the test is NOT an object, turn it into one
    114      if (typeof test === 'string') {
    115        test = JSON.parse(test) ;
    116      }
    117 
    118      this.Test = test;
    119 
    120      // Test should have information that we can put in the template
    121 
    122      if (test.description) {
    123        this.DescriptionText = test.description;
    124      }
    125 
    126      if (test.hasOwnProperty("skipFailures") && Array.isArray(test.skipFailures) ) {
    127        this.SkipFailures = test.skipFailures;
    128      }
    129 
    130      if (test.content) {
    131        // we have content
    132        if (typeof test.content === "string") {
    133          // the test content is a string - meaning it is a reference to a file of content
    134          var cPromise = new Promise(function(resolve, reject) {
    135            this.loadDefinition(test.content)
    136              .then(function(content) {
    137                if (typeof content === 'string') {
    138                  content = JSON.parse(content) ;
    139                }
    140                test.content = content;
    141                resolve(true);
    142              }.bind(this))
    143            .catch(function(err) {
    144              reject("Loading " + test.content + ": " + JSON.stringify(err));
    145            });
    146 
    147          }.bind(this));
    148          pending.push(cPromise);
    149        }
    150      }
    151 
    152      return new Promise(function(resolve, reject) {
    153        if (test.assertions &&
    154            typeof test.assertions === "object") {
    155          // we have at least one assertion
    156          // get the inline contents and the references to external files
    157          var assertFiles = this._assertionRefs(test.assertions);
    158 
    159          var promisedAsserts = assertFiles.map(function(item) {
    160            return this.loadAssertion(item);
    161          }.bind(this));
    162 
    163          // Once all the loadAssertion promises resolve...
    164          Promise.all(promisedAsserts)
    165          .then(function (assertContents) {
    166            // assertContents has assertions in document order
    167 
    168            var typeMap = {
    169              'must'   : "<b>[MANDATORY]</b> ",
    170              'may'    : "<b>[OPTIONAL]</b> ",
    171              'should' : "<b>[RECOMMENDED]</b> "
    172            };
    173 
    174            var assertIdx = 0;
    175 
    176            // populate the display of assertions that are being exercised
    177            // returns the list of top level assertions to walk through
    178 
    179            var buildList = function(assertions, level) {
    180              if (level === undefined) {
    181                level = 1;
    182              }
    183 
    184              // accumulate the assertions - but only when level is 0
    185              var list = [] ;
    186 
    187              var type = "";
    188              if (assertions) {
    189                if (typeof assertions === "object" && assertions.hasOwnProperty('assertions')) {
    190                  // this is a conditionObject
    191                  if (level === 0) {
    192                    list.push(assertContents[assertIdx]);
    193                  }
    194                  type = assertContents[assertIdx].hasOwnProperty('assertionType') ?
    195                    assertContents[assertIdx].assertionType : "must" ;
    196 
    197                  // ensure type defaults to must
    198                  if (!typeMap.hasOwnProperty(type)) {
    199                    type = "must";
    200                  }
    201 
    202                  this.AssertionText += "<li>" + typeMap[type] + this.markdown.makeHtml(assertContents[assertIdx++].title);
    203                  this.AssertionText += "<ol>";
    204                  buildList(assertions.assertions, level+1) ;
    205                  this.AssertionText += "</ol></li>\n";
    206                } else {
    207                  // it is NOT a conditionObject - must be an array
    208                  assertions.forEach( function(assert) {
    209                    if (typeof assert === "object" && Array.isArray(assert)) {
    210                      this.AssertionText += "<ol>";
    211                      // it is a nested list - recurse
    212                      buildList(assert, level+1) ;
    213                      this.AssertionText += "</ol>\n";
    214                    } else if (typeof assert === "object" &&
    215                        !Array.isArray(assert) &&
    216                        assert.hasOwnProperty('assertions')) {
    217                      if (level === 0) {
    218                        list.push(assertContents[assertIdx]);
    219                      }
    220                      type = assertContents[assertIdx].hasOwnProperty('assertionType') ?
    221                        assertContents[assertIdx].assertionType : "must" ;
    222 
    223                      // ensure type defaults to must
    224                      if (!typeMap.hasOwnProperty(type)) {
    225                        type = "must";
    226                      }
    227 
    228                      // there is a condition object in the array
    229                      this.AssertionText += "<li>" + typeMap[type] + this.markdown.makeHtml(assertContents[assertIdx++].title);
    230                      this.AssertionText += "<ol>";
    231                      buildList(assert, level+1) ; // capture the children too
    232                      this.AssertionText += "</ol></li>\n";
    233                    } else {
    234                      if (level === 0) {
    235                        list.push(assertContents[assertIdx]);
    236                      }
    237                      type = assertContents[assertIdx].hasOwnProperty('assertionType') ?
    238                        assertContents[assertIdx].assertionType : "must" ;
    239 
    240                      // ensure type defaults to must
    241                      if (!typeMap.hasOwnProperty(type)) {
    242                        type = "must";
    243                      }
    244 
    245                      this.AssertionText += "<li>" + typeMap[type] + this.markdown.makeHtml(assertContents[assertIdx++].title) + "</li>\n";
    246                    }
    247                  }.bind(this));
    248                }
    249              }
    250              return list;
    251            }.bind(this);
    252 
    253            // Assertions will ONLY contain the top level assertions
    254            this.Assertions = buildList(test.assertions, 0);
    255            resolve(true);
    256          }.bind(this))
    257          .catch(function(err) {
    258            reject(err);
    259          }.bind(this));
    260        } else {
    261          if (!test.assertions) {
    262            reject("Test has no assertion property");
    263          } else {
    264            reject("Test assertion property is not an Array");
    265          }
    266        }
    267      }.bind(this));
    268    }.bind(this)));
    269 
    270  this.Promise = new Promise(function(resolve, reject) {
    271    // once the DOM and the test / assertions are loaded... set us up
    272    Promise.all(pending)
    273    .then(function() {
    274      this.loading = false;
    275      this.init();
    276      resolve(this);
    277    }.bind(this))
    278    .catch(function(err) {
    279      // loading the components failed somehow - report the errors and mark the test failed
    280      test( function() {
    281        assert_true(false, "Loading of test components failed: " +JSON.stringify(err)) ;
    282      }, "Loading test components");
    283      done() ;
    284      reject("Loading of test components failed: "+JSON.stringify(err));
    285      return ;
    286    }.bind(this));
    287  }.bind(this));
    288 
    289  return this;
    290 }
    291 
    292 JSONtest.prototype = {
    293 
    294  /**
    295   * @listens click
    296   */
    297  init: function() {
    298    'use strict';
    299    // set up a handler
    300    var runButton = document.getElementById(this.Params.runTest) ;
    301    var closeButton = document.getElementById(this.Params.closeWindow) ;
    302    var testInput  = document.getElementById(this.Params.testInput) ;
    303    var assertion  = document.getElementById("assertion") ;
    304    var desc  = document.getElementById("testDescription") ;
    305 
    306    if (!this.loading) {
    307      if (runButton) {
    308        runButton.disabled = false;
    309        runButton.value = "Check JSON";
    310      }
    311      if (desc) {
    312        desc.innerHTML = this.DescriptionText;
    313      }
    314      if (assertion) {
    315        assertion.innerHTML = "<ol>" + this.AssertionText + "</ol>\n";
    316      }
    317    } else {
    318      window.alert("Loading did not finish before init handler was called!");
    319    }
    320 
    321    // @@@TODO@@@ implement the output showing handler
    322    if (0 && this.Properties && this.Properties.output && closeButton) {
    323      // set up a callback
    324      add_completion_callback( function() {
    325        var p = new Promise(function(resolve) {
    326          closeButton.style.display = "inline";
    327          closeButton.disabled = false;
    328          on_event(closeButton, "click", function() {
    329            resolve(true);
    330          });
    331        }.bind(this));
    332        p.then();
    333      }.bind(this));
    334    }
    335 
    336    if (runButton) {
    337      on_event(runButton, "click", function() {
    338        // user clicked
    339        var content = testInput.value;
    340        runButton.disabled = true;
    341 
    342        // make sure content is an object
    343        if (typeof content === "string") {
    344          try {
    345            content = JSON.parse(content) ;
    346          } catch(err) {
    347            // if the parsing failed, create a special test and mark it failed
    348            test( function() {
    349              assert_true(false, "Parse of JSON failed: " + err) ;
    350            }, "Parsing submitted input");
    351            // and just give up
    352            done();
    353            return ;
    354          }
    355        }
    356 
    357        // iterate over all of the tests for this instance
    358        this.runTests(this.Assertions, content);
    359 
    360        // explicitly tell the test framework we are done
    361        done();
    362      }.bind(this));
    363    }
    364  },
    365 
    366  // runTests - process tests
    367  /**
    368   * @param {object} assertions - List of assertions to process
    369   * @param {string} content - JSON(-LD) to be evaluated
    370   * @param {string} [testAction='continue'] - state of test processing (in parent when recursing)
    371   * @param {integer} [level=0] - depth of recursion since assertion lists can nest
    372   * @param {string} [compareWith='and'] - the way the results of the referenced assertions should be compared
    373   * @returns {string} - the testAction resulting from evaluating all of the assertions
    374   */
    375  runTests: function(assertions, content, testAction, level, compareWith) {
    376    'use strict';
    377 
    378    // level
    379    if (level === undefined) {
    380      level = 1;
    381    }
    382 
    383    // testAction
    384    if (testAction === undefined) {
    385      testAction = 'continue';
    386    }
    387 
    388    // compareWith
    389    if (compareWith === undefined) {
    390      compareWith = 'and';
    391    }
    392 
    393    var typeMap = {
    394      'must'   : "",
    395      'may'    : "INFORMATIONAL: ",
    396      'should' : "WARNING: "
    397    };
    398 
    399 
    400    // for each assertion (in order) load the external json schema if
    401    // one is referenced, or use the inline schema if supplied
    402    // validate content against the referenced schema
    403 
    404    var theResults = [] ;
    405 
    406    if (assertions) {
    407 
    408      assertions.forEach( function(assert, num) {
    409 
    410        var expected = assert.hasOwnProperty('expectedResult') ?
    411          assert.expectedResult : 'valid' ;
    412        var message = assert.hasOwnProperty('errorMessage') ?
    413          assert.errorMessage : "Result was not " + expected;
    414        var type = assert.hasOwnProperty('assertionType') ?
    415          assert.assertionType.toLowerCase() : "must" ;
    416        if (!typeMap.hasOwnProperty(type)) {
    417          type = "must";
    418        }
    419 
    420        // first - what is the type of the assert
    421        if (typeof assert === "object" && !Array.isArray(assert)) {
    422          if (assert.hasOwnProperty("compareWith") && assert.hasOwnProperty("assertions") && Array.isArray(assert.assertions) ) {
    423            // this is a comparisonObject
    424            var r = this.runTests(assert.assertions, content, testAction, level+1, assert.compareWith);
    425            // r is an object that contains, among other things, an array of results from the child assertions
    426            testAction = r.action;
    427 
    428            // evaluate the results against the compareWith setting
    429            var result = true;
    430            var data = r.results ;
    431            var i;
    432 
    433            if (assert.compareWith === "or") {
    434              result = false;
    435              for(i = 0; i < data.length; i++) {
    436                if (data[i]) {
    437                  result = true;
    438                }
    439              }
    440            } else {
    441              for(i = 0; i < data.length; i++) {
    442                if (!data[i]) {
    443                  result = false;
    444                }
    445              }
    446            }
    447 
    448            // create a test and push the result
    449            test(function() {
    450              var newAction = this.determineAction(assert, result) ;
    451              // next time around we will use this action
    452              testAction = newAction;
    453 
    454              var err = ";";
    455 
    456              if (testAction === 'abort') {
    457                err += "; Aborting execution of remaining assertions;";
    458              } else if (testAction === 'skip') {
    459                err += "; Skipping execution of remaining assertions at level " + level + ";";
    460              }
    461 
    462              if (result === false) {
    463                // test result was unexpected; use message
    464                assert_true(result, message + err);
    465              } else {
    466                assert_true(result, err) ;
    467              }
    468            }.bind(this), "" + level + ":" + (num+1) + " " + assert.title);
    469            // we are going to return out of this
    470            return;
    471          }
    472        } else if (typeof assert === "object" && Array.isArray(assert)) {
    473          // it is a nested list - recurse
    474          var o = this.runTests(assert, content, testAction, level+1);
    475          if (o.result && o.result === 'abort') {
    476            // we are bailing out
    477            testAction = 'abort';
    478          }
    479        }
    480 
    481        if (testAction === 'abort') {
    482          return {action: 'abort' };
    483        }
    484 
    485        var schemaName = "inline " + level + ":" + (num+1);
    486 
    487        if (typeof assert === "string") {
    488          // the assertion passed in is a file name; find it in the cache
    489          if (this._assertionCache[assert]) {
    490            assert = this._assertionCache[assert];
    491          } else {
    492            test( function() {
    493              assert_true(false, "Reference to assertion " + assert + " at level " + level + ":" + (num+1) + " unresolved") ;
    494            }, "Processing " + assert);
    495            return ;
    496          }
    497        }
    498 
    499        if (assert.assertionFile) {
    500          schemaName = "external file " + assert.assertionFile + " " + level + ":" + (num+1);
    501        }
    502 
    503        var validate = null;
    504 
    505        try {
    506          validate = this.ajv.compile(assert);
    507        }
    508        catch(err) {
    509          test( function() {
    510            assert_true(false, "Compilation of schema " + level + ":" + (num+1) + " failed: " + err) ;
    511          }, "Compiling " + schemaName);
    512          return ;
    513        }
    514 
    515        if (testAction === 'continue') {
    516          // a previous test told us to not run this test; skip it
    517          // test(function() { }, "SKIPPED: " + assert.title);
    518          // start an actual sub-test
    519          var valid = validate(content) ;
    520 
    521          var theResult = this.determineResult(assert, valid) ;
    522 
    523          // remember the result
    524          theResults.push(theResult);
    525 
    526          var newAction = this.determineAction(assert, theResult) ;
    527          // next time around we will use this action
    528          testAction = newAction;
    529 
    530          // only run the test if we are NOT skipping fails for some types
    531          // or the result is expected
    532          if ( theResult === true || !this.SkipFailures.includes(type) ) {
    533            test(function() {
    534              var err = ";";
    535              if (validate.errors !== null && !assert.hasOwnProperty("errorMessage")) {
    536                err = "; Errors: " + this.ajv.errorsText(validate.errors) + ";" ;
    537              }
    538              if (testAction === 'abort') {
    539                err += "; Aborting execution of remaining assertions;";
    540              } else if (testAction === 'skip') {
    541                err += "; Skipping execution of remaining assertions at level " + level + ";";
    542              }
    543              if (theResult === false) {
    544                // test result was unexpected; use message
    545                assert_true(theResult, typeMap[type] + message + err);
    546              } else {
    547                assert_true(theResult, err) ;
    548              }
    549            }.bind(this), "" + level + ":" + (num+1) + " " + assert.title);
    550          }
    551        }
    552      }.bind(this));
    553    }
    554 
    555    return { action: testAction, results: theResults} ;
    556  },
    557 
    558  determineResult: function(schema, valid) {
    559    'use strict';
    560    var r = 'valid' ;
    561    if (schema.hasOwnProperty('expectedResult')) {
    562      r = schema.expectedResult;
    563    }
    564 
    565    if (r === 'valid' && valid || r === 'invalid' && !valid) {
    566      return true;
    567    } else {
    568      return false;
    569    }
    570  },
    571 
    572  determineAction: function(schema, result) {
    573    'use strict';
    574    // mapping from results to actions
    575    var mapping = {
    576      'failAndContinue' : 'continue',
    577      'failAndSkip'    : 'skip',
    578      'failAndAbort'   : 'abort',
    579      'passAndContinue': 'continue',
    580      'passAndSkip'    : 'skip',
    581      'passAndAbort'   : 'abort'
    582    };
    583 
    584    // if the result was as expected, then just keep going
    585    if (result) {
    586      return 'continue';
    587    }
    588 
    589    var a = 'failAndContinue';
    590 
    591    if (schema.hasOwnProperty('onUnexpectedResult')) {
    592      a = schema.onUnexpectedResult;
    593    }
    594 
    595    if (mapping[a]) {
    596      return mapping[a];
    597    } else {
    598      return 'continue';
    599    }
    600  },
    601 
    602  // loadAssertion - load an Assertion from an external JSON file
    603  //
    604  // returns a promise that resolves with the contents of the assertion file
    605 
    606  loadAssertion: function(afile) {
    607    'use strict';
    608    if (typeof(afile) === 'string') {
    609      var theFile = this._parseURI(afile);
    610      // it is a file reference - load it
    611      return new Promise(function(resolve, reject) {
    612        this._loadFile("GET", theFile, true)
    613          .then(function(data) {
    614            data.assertionFile = afile;
    615            this._assertionCache[afile] = data;
    616            resolve(data);
    617          }.bind(this))
    618          .catch(function(err) {
    619            if (typeof err === "object") {
    620              err.theFile = theFile;
    621            }
    622            reject(err);
    623          });
    624        }.bind(this));
    625      }
    626      else if (afile.hasOwnProperty("assertionFile")) {
    627      // this object is referecing an external assertion
    628      return new Promise(function(resolve, reject) {
    629        var theFile = this._parseURI(afile.assertionFile);
    630        this._loadFile("GET", theFile, true)
    631        .then(function(external) {
    632          // okay - we have an external object
    633          Object.keys(afile).forEach(function(key) {
    634            if (key !== 'assertionFile') {
    635              external[key] = afile[key];
    636            }
    637          });
    638          resolve(external);
    639        }.bind(this))
    640        .catch(function(err) {
    641          if (typeof err === "object") {
    642            err.theFile = theFile;
    643          }
    644          reject(err);
    645        });
    646      }.bind(this));
    647    } else {
    648      // it is already a loaded assertion - just use it
    649      return new Promise(function(resolve) {
    650        resolve(afile);
    651      });
    652    }
    653  },
    654 
    655  // loadDefinition - load a JSON Schema definition from an external JSON file
    656  //
    657  // returns a promise that resolves with the contents of the definition file
    658 
    659  loadDefinition: function(dfile) {
    660    'use strict';
    661    return new Promise(function(resolve, reject) {
    662      this._loadFile("GET", this._parseURI(dfile), true)
    663        .then(function(data) {
    664          resolve(data);
    665        }.bind(this))
    666        .catch(function(err) {
    667          reject(err);
    668        });
    669      }.bind(this));
    670  },
    671 
    672 
    673  // loadTest - load a test from an external JSON file
    674  //
    675  // returns a promise that resolves with the contents of the
    676  // test
    677 
    678  loadTest: function(params) {
    679    'use strict';
    680 
    681    if (params.hasOwnProperty('testFile')) {
    682      // the test is referred to by a file name
    683      return this._loadFile("GET", params.testFile);
    684    } // else
    685    return new Promise(function(resolve, reject) {
    686      if (params.hasOwnProperty('test')) {
    687        resolve(params.test);
    688      } else {
    689        reject("Must supply a 'test' or 'testFile' parameter");
    690      }
    691    });
    692  },
    693 
    694  _parseURI: function(theURI) {
    695    'use strict';
    696    // determine what the top level URI should be
    697    if (theURI.indexOf('/') === -1) {
    698      // no slash - it's relative to where we are
    699      // so just use it
    700      return this.TestDir + theURI;
    701    } else if (theURI.indexOf('/') === 0 || theURI.indexOf('http:') === 0 || theURI.indexOf('https:') === 0) {
    702      // it is an absolute URI so just use it
    703      return theURI;
    704    } else {
    705      // it is relative and contains a slash.
    706      // make it relative to the current test root
    707      return this.Base + theURI;
    708    }
    709  },
    710 
    711  /**
    712   * return a list of all inline assertions or references
    713   *
    714   * @param {array} assertions list of assertions to examine
    715   */
    716 
    717  _assertionRefs: function(assertions) {
    718    'use strict';
    719    var ret = [] ;
    720 
    721    // when the reference is to an object that has an array of assertions in it (a conditionObject)
    722    // then remember that one and loop over its embedded assertions
    723    if (typeof(assertions) === "object" && !Array.isArray(assertions) && assertions.hasOwnProperty('assertions')) {
    724      ret.push(assertions) ;
    725      assertions = assertions.assertions;
    726    }
    727    if (typeof(assertions) === "object" && Array.isArray(assertions)) {
    728      assertions.forEach( function(assert) {
    729        // first - what is the type of the assert
    730        if (typeof assert === "object" && Array.isArray(assert)) {
    731          // it is a nested list - recurse
    732          this._assertionRefs(assert).forEach( function(item) {
    733            ret.push(item);
    734          }.bind(this));
    735        } else if (typeof assert === "object") {
    736          ret.push(assert) ;
    737          if (assert.hasOwnProperty("assertions")) {
    738            // there are embedded assertions; get those too
    739            ret.concat(this._assertionRefs(assert.assertions));
    740          }
    741        } else {
    742          // it is a file name
    743          ret.push(assert) ;
    744        }
    745      }.bind(this));
    746    }
    747    return ret;
    748  },
    749 
    750  // _loadFile - return a promise loading a file
    751  //
    752  _loadFile: function(method, url, parse) {
    753    'use strict';
    754    if (parse === undefined) {
    755      parse = true;
    756    }
    757 
    758    return new Promise(function (resolve, reject) {
    759      if (document.location.search) {
    760        var s = document.location.search;
    761        s = s.replace(/^\?/, '');
    762        if (url.indexOf('?') !== -1) {
    763          url += "&" + s;
    764        } else {
    765          url += "?" + s;
    766        }
    767      }
    768      var xhr = new XMLHttpRequest();
    769      xhr.open(method, url);
    770      xhr.onload = function () {
    771        if (this.status >= 200 && this.status < 300) {
    772          var d = xhr.response;
    773          if (parse) {
    774            try {
    775              d = JSON.parse(d);
    776              resolve(d);
    777            }
    778            catch(err) {
    779              reject({ status: this.status,
    780                       statusText: "Parsing of " + url + " failed: " + err }
    781                   );
    782            }
    783          } else {
    784            resolve(d);
    785          }
    786        } else {
    787          reject({
    788            status: this.status,
    789            statusText: xhr.statusText
    790          });
    791        }
    792      };
    793      xhr.onerror = function () {
    794        reject({
    795          status: this.status,
    796          statusText: xhr.statusText
    797        });
    798      };
    799      xhr.send();
    800    });
    801  },
    802 
    803 };