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 };