| /* globals add_completion_callback, Promise, showdown, done, assert_true, Ajv, on_event */ |
| |
| /** |
| * Creates a JSONtest object. If the parameters are supplied |
| * it also loads a referenced testFile, processes that file, loads any |
| * referenced external assertions, and sets up event listeners to process the |
| * user's test data. The loading is done asynchronously via Promises. The test |
| * button's text is changed to Loading while it is processing, and to "Check |
| * JSON" once the data is loaded. |
| * |
| * @constructor |
| * @param {object} params |
| * @param {string} [params.test] - object containing JSON test definition |
| * @param {string} [params.testFile] - URI of a file with JSON test definition |
| * @param {string} params.runTest - IDREF of an element that when clicked will run the test |
| * @param {string} params.testInput - IDREF of an element that contains the JSON(-LD) to evaluate against the assertions in the test / testFile |
| * @event DOMContentLoaded Calls init once DOM is fully loaded |
| * @returns {object} Reference to the new object |
| */ |
| |
| function JSONtest(params) { |
| 'use strict'; |
| |
| this.Assertions = []; // object that will contain the assertions to process |
| this.AssertionText = ""; // string that holds the titles of all the assertions in use |
| this.DescriptionText = ""; |
| this.Base = null; // URI "base" for the test suite being run |
| this.TestDir = null; // URI "base" for the test case being run |
| this.Params = null; // paramaters passed in |
| this.Promise = null; // master Promise that resolves when intialization is complete |
| this.Properties = null; // testharness_properties from the opening window |
| this.SkipFailures = []; // list of assertionType values that should be skipped if their test would fail |
| this.Test = null; // test being run |
| this.AssertionCounter = 0;// keeps track of which assertion is being processed |
| |
| this._assertionCache = [];// Array to put loaded assertions into |
| this._assertionText = []; // Array of text or nested arrays of assertions |
| this._loading = true; |
| |
| showdown.extension('strip', function() { |
| return [ |
| { type: 'output', |
| regex: /<p>/, |
| replace: '' |
| }, |
| { type: 'output', |
| regex: /<\/p>$/, |
| replace: '' |
| } |
| ]; |
| }); |
| |
| |
| this.markdown = new showdown.Converter({ extensions: [ 'strip' ] }) ; |
| |
| var pending = [] ; |
| |
| // set up in case DOM finishes loading early |
| pending.push(new Promise(function(resolve) { |
| on_event(document, "DOMContentLoaded", function() { |
| resolve(true); |
| }.bind(this)); |
| }.bind(this))); |
| |
| // create an ajv object that will stay around so that caching |
| // of schema that are compiled just works |
| this.ajv = new Ajv({allErrors: true, validateSchema: false}) ; |
| |
| // determine the base URI for the test collection. This is |
| // the top level folder in the test "document.location" |
| |
| var l = document.location; |
| var p = l.pathname; |
| this.TestDir = p.substr(0, 1+p.lastIndexOf('/')); |
| this.Base = p.substr(0, 1+p.indexOf('/', 1)); |
| |
| // if we are under runner, then there are props in the parent window |
| // |
| // if "output" is set in that, then pause at the end of running so the output |
| // can be analyzed. @@@TODO@@@ |
| if (window && window.opener && window.opener.testharness_properties) { |
| this.Properties = window.opener.testharness_properties; |
| } |
| |
| this.Params = params; |
| |
| // if there is a list of definitions in the params, |
| // include them |
| if (this.Params.schemaDefs) { |
| var defPromise = new Promise(function(resolve, reject) { |
| var promisedSchema = this.Params.schemaDefs.map(function(item) { |
| return this.loadDefinition(item); |
| }.bind(this)); |
| |
| // Once all the loadAssertion promises resolve... |
| Promise.all(promisedSchema) |
| .then(function (schemaContents) { |
| this.ajv.addSchema(schemaContents); |
| resolve(true); |
| }.bind(this)) |
| .catch(function(err) { |
| reject(err); |
| }.bind(this)); |
| }.bind(this)); |
| // these schema need to load up too |
| pending.push(defPromise) ; |
| } |
| |
| // start by loading the test (it might be inline, but |
| // loadTest deals with that |
| pending.push(this.loadTest(params) |
| .then(function(test) { |
| // if the test is NOT an object, turn it into one |
| if (typeof test === 'string') { |
| test = JSON.parse(test) ; |
| } |
| |
| this.Test = test; |
| |
| // Test should have information that we can put in the template |
| |
| if (test.description) { |
| this.DescriptionText = test.description; |
| } |
| |
| if (test.hasOwnProperty("skipFailures") && Array.isArray(test.skipFailures) ) { |
| this.SkipFailures = test.skipFailures; |
| } |
| |
| if (test.content) { |
| // we have content |
| if (typeof test.content === "string") { |
| // the test content is a string - meaning it is a reference to a file of content |
| var cPromise = new Promise(function(resolve, reject) { |
| this.loadDefinition(test.content) |
| .then(function(content) { |
| if (typeof content === 'string') { |
| content = JSON.parse(content) ; |
| } |
| test.content = content; |
| resolve(true); |
| }.bind(this)) |
| .catch(function(err) { |
| reject("Loading " + test.content + ": " + JSON.stringify(err)); |
| }); |
| |
| }.bind(this)); |
| pending.push(cPromise); |
| } |
| } |
| |
| return new Promise(function(resolve, reject) { |
| if (test.assertions && |
| typeof test.assertions === "object") { |
| // we have at least one assertion |
| // get the inline contents and the references to external files |
| var assertFiles = this._assertionRefs(test.assertions); |
| |
| var promisedAsserts = assertFiles.map(function(item) { |
| return this.loadAssertion(item); |
| }.bind(this)); |
| |
| // Once all the loadAssertion promises resolve... |
| Promise.all(promisedAsserts) |
| .then(function (assertContents) { |
| // assertContents has assertions in document order |
| |
| var typeMap = { |
| 'must' : "<b>[MANDATORY]</b> ", |
| 'may' : "<b>[OPTIONAL]</b> ", |
| 'should' : "<b>[RECOMMENDED]</b> " |
| }; |
| |
| var assertIdx = 0; |
| |
| // populate the display of assertions that are being exercised |
| // returns the list of top level assertions to walk through |
| |
| var buildList = function(assertions, level) { |
| if (level === undefined) { |
| level = 1; |
| } |
| |
| // accumulate the assertions - but only when level is 0 |
| var list = [] ; |
| |
| var type = ""; |
| if (assertions) { |
| if (typeof assertions === "object" && assertions.hasOwnProperty('assertions')) { |
| // this is a conditionObject |
| if (level === 0) { |
| list.push(assertContents[assertIdx]); |
| } |
| type = assertContents[assertIdx].hasOwnProperty('assertionType') ? |
| assertContents[assertIdx].assertionType : "must" ; |
| |
| // ensure type defaults to must |
| if (!typeMap.hasOwnProperty(type)) { |
| type = "must"; |
| } |
| |
| this.AssertionText += "<li>" + typeMap[type] + this.markdown.makeHtml(assertContents[assertIdx++].title); |
| this.AssertionText += "<ol>"; |
| buildList(assertions.assertions, level+1) ; |
| this.AssertionText += "</ol></li>\n"; |
| } else { |
| // it is NOT a conditionObject - must be an array |
| assertions.forEach( function(assert) { |
| if (typeof assert === "object" && Array.isArray(assert)) { |
| this.AssertionText += "<ol>"; |
| // it is a nested list - recurse |
| buildList(assert, level+1) ; |
| this.AssertionText += "</ol>\n"; |
| } else if (typeof assert === "object" && |
| !Array.isArray(assert) && |
| assert.hasOwnProperty('assertions')) { |
| if (level === 0) { |
| list.push(assertContents[assertIdx]); |
| } |
| type = assertContents[assertIdx].hasOwnProperty('assertionType') ? |
| assertContents[assertIdx].assertionType : "must" ; |
| |
| // ensure type defaults to must |
| if (!typeMap.hasOwnProperty(type)) { |
| type = "must"; |
| } |
| |
| // there is a condition object in the array |
| this.AssertionText += "<li>" + typeMap[type] + this.markdown.makeHtml(assertContents[assertIdx++].title); |
| this.AssertionText += "<ol>"; |
| buildList(assert, level+1) ; // capture the children too |
| this.AssertionText += "</ol></li>\n"; |
| } else { |
| if (level === 0) { |
| list.push(assertContents[assertIdx]); |
| } |
| type = assertContents[assertIdx].hasOwnProperty('assertionType') ? |
| assertContents[assertIdx].assertionType : "must" ; |
| |
| // ensure type defaults to must |
| if (!typeMap.hasOwnProperty(type)) { |
| type = "must"; |
| } |
| |
| this.AssertionText += "<li>" + typeMap[type] + this.markdown.makeHtml(assertContents[assertIdx++].title) + "</li>\n"; |
| } |
| }.bind(this)); |
| } |
| } |
| return list; |
| }.bind(this); |
| |
| // Assertions will ONLY contain the top level assertions |
| this.Assertions = buildList(test.assertions, 0); |
| resolve(true); |
| }.bind(this)) |
| .catch(function(err) { |
| reject(err); |
| }.bind(this)); |
| } else { |
| if (!test.assertions) { |
| reject("Test has no assertion property"); |
| } else { |
| reject("Test assertion property is not an Array"); |
| } |
| } |
| }.bind(this)); |
| }.bind(this))); |
| |
| this.Promise = new Promise(function(resolve, reject) { |
| // once the DOM and the test / assertions are loaded... set us up |
| Promise.all(pending) |
| .then(function() { |
| this.loading = false; |
| this.init(); |
| resolve(this); |
| }.bind(this)) |
| .catch(function(err) { |
| // loading the components failed somehow - report the errors and mark the test failed |
| test( function() { |
| assert_true(false, "Loading of test components failed: " +JSON.stringify(err)) ; |
| }, "Loading test components"); |
| done() ; |
| reject("Loading of test components failed: "+JSON.stringify(err)); |
| return ; |
| }.bind(this)); |
| }.bind(this)); |
| |
| return this; |
| } |
| |
| JSONtest.prototype = { |
| |
| /** |
| * @listens click |
| */ |
| init: function() { |
| 'use strict'; |
| // set up a handler |
| var runButton = document.getElementById(this.Params.runTest) ; |
| var closeButton = document.getElementById(this.Params.closeWindow) ; |
| var testInput = document.getElementById(this.Params.testInput) ; |
| var assertion = document.getElementById("assertion") ; |
| var desc = document.getElementById("testDescription") ; |
| |
| if (!this.loading) { |
| if (runButton) { |
| runButton.disabled = false; |
| runButton.value = "Check JSON"; |
| } |
| if (desc) { |
| desc.innerHTML = this.DescriptionText; |
| } |
| if (assertion) { |
| assertion.innerHTML = "<ol>" + this.AssertionText + "</ol>\n"; |
| } |
| } else { |
| window.alert("Loading did not finish before init handler was called!"); |
| } |
| |
| // @@@TODO@@@ implement the output showing handler |
| if (0 && this.Properties && this.Properties.output && closeButton) { |
| // set up a callback |
| add_completion_callback( function() { |
| var p = new Promise(function(resolve) { |
| closeButton.style.display = "inline"; |
| closeButton.disabled = false; |
| on_event(closeButton, "click", function() { |
| resolve(true); |
| }); |
| }.bind(this)); |
| p.then(); |
| }.bind(this)); |
| } |
| |
| if (runButton) { |
| on_event(runButton, "click", function() { |
| // user clicked |
| var content = testInput.value; |
| runButton.disabled = true; |
| |
| // make sure content is an object |
| if (typeof content === "string") { |
| try { |
| content = JSON.parse(content) ; |
| } catch(err) { |
| // if the parsing failed, create a special test and mark it failed |
| test( function() { |
| assert_true(false, "Parse of JSON failed: " + err) ; |
| }, "Parsing submitted input"); |
| // and just give up |
| done(); |
| return ; |
| } |
| } |
| |
| // iterate over all of the tests for this instance |
| this.runTests(this.Assertions, content); |
| |
| // explicitly tell the test framework we are done |
| done(); |
| }.bind(this)); |
| } |
| }, |
| |
| // runTests - process tests |
| /** |
| * @param {object} assertions - List of assertions to process |
| * @param {string} content - JSON(-LD) to be evaluated |
| * @param {string} [testAction='continue'] - state of test processing (in parent when recursing) |
| * @param {integer} [level=0] - depth of recursion since assertion lists can nest |
| * @param {string} [compareWith='and'] - the way the results of the referenced assertions should be compared |
| * @returns {string} - the testAction resulting from evaluating all of the assertions |
| */ |
| runTests: function(assertions, content, testAction, level, compareWith) { |
| 'use strict'; |
| |
| // level |
| if (level === undefined) { |
| level = 1; |
| } |
| |
| // testAction |
| if (testAction === undefined) { |
| testAction = 'continue'; |
| } |
| |
| // compareWith |
| if (compareWith === undefined) { |
| compareWith = 'and'; |
| } |
| |
| var typeMap = { |
| 'must' : "", |
| 'may' : "INFORMATIONAL: ", |
| 'should' : "WARNING: " |
| }; |
| |
| |
| // for each assertion (in order) load the external json schema if |
| // one is referenced, or use the inline schema if supplied |
| // validate content against the referenced schema |
| |
| var theResults = [] ; |
| |
| if (assertions) { |
| |
| assertions.forEach( function(assert, num) { |
| |
| var expected = assert.hasOwnProperty('expectedResult') ? |
| assert.expectedResult : 'valid' ; |
| var message = assert.hasOwnProperty('errorMessage') ? |
| assert.errorMessage : "Result was not " + expected; |
| var type = assert.hasOwnProperty('assertionType') ? |
| assert.assertionType.toLowerCase() : "must" ; |
| if (!typeMap.hasOwnProperty(type)) { |
| type = "must"; |
| } |
| |
| // first - what is the type of the assert |
| if (typeof assert === "object" && !Array.isArray(assert)) { |
| if (assert.hasOwnProperty("compareWith") && assert.hasOwnProperty("assertions") && Array.isArray(assert.assertions) ) { |
| // this is a comparisonObject |
| var r = this.runTests(assert.assertions, content, testAction, level+1, assert.compareWith); |
| // r is an object that contains, among other things, an array of results from the child assertions |
| testAction = r.action; |
| |
| // evaluate the results against the compareWith setting |
| var result = true; |
| var data = r.results ; |
| var i; |
| |
| if (assert.compareWith === "or") { |
| result = false; |
| for(i = 0; i < data.length; i++) { |
| if (data[i]) { |
| result = true; |
| } |
| } |
| } else { |
| for(i = 0; i < data.length; i++) { |
| if (!data[i]) { |
| result = false; |
| } |
| } |
| } |
| |
| // create a test and push the result |
| test(function() { |
| var newAction = this.determineAction(assert, result) ; |
| // next time around we will use this action |
| testAction = newAction; |
| |
| var err = ";"; |
| |
| if (testAction === 'abort') { |
| err += "; Aborting execution of remaining assertions;"; |
| } else if (testAction === 'skip') { |
| err += "; Skipping execution of remaining assertions at level " + level + ";"; |
| } |
| |
| if (result === false) { |
| // test result was unexpected; use message |
| assert_true(result, message + err); |
| } else { |
| assert_true(result, err) ; |
| } |
| }.bind(this), "" + level + ":" + (num+1) + " " + assert.title); |
| // we are going to return out of this |
| return; |
| } |
| } else if (typeof assert === "object" && Array.isArray(assert)) { |
| // it is a nested list - recurse |
| var o = this.runTests(assert, content, testAction, level+1); |
| if (o.result && o.result === 'abort') { |
| // we are bailing out |
| testAction = 'abort'; |
| } |
| } |
| |
| if (testAction === 'abort') { |
| return {action: 'abort' }; |
| } |
| |
| var schemaName = "inline " + level + ":" + (num+1); |
| |
| if (typeof assert === "string") { |
| // the assertion passed in is a file name; find it in the cache |
| if (this._assertionCache[assert]) { |
| assert = this._assertionCache[assert]; |
| } else { |
| test( function() { |
| assert_true(false, "Reference to assertion " + assert + " at level " + level + ":" + (num+1) + " unresolved") ; |
| }, "Processing " + assert); |
| return ; |
| } |
| } |
| |
| if (assert.assertionFile) { |
| schemaName = "external file " + assert.assertionFile + " " + level + ":" + (num+1); |
| } |
| |
| var validate = null; |
| |
| try { |
| validate = this.ajv.compile(assert); |
| } |
| catch(err) { |
| test( function() { |
| assert_true(false, "Compilation of schema " + level + ":" + (num+1) + " failed: " + err) ; |
| }, "Compiling " + schemaName); |
| return ; |
| } |
| |
| if (testAction === 'continue') { |
| // a previous test told us to not run this test; skip it |
| // test(function() { }, "SKIPPED: " + assert.title); |
| // start an actual sub-test |
| var valid = validate(content) ; |
| |
| var theResult = this.determineResult(assert, valid) ; |
| |
| // remember the result |
| theResults.push(theResult); |
| |
| var newAction = this.determineAction(assert, theResult) ; |
| // next time around we will use this action |
| testAction = newAction; |
| |
| // only run the test if we are NOT skipping fails for some types |
| // or the result is expected |
| if ( theResult === true || !this.SkipFailures.includes(type) ) { |
| test(function() { |
| var err = ";"; |
| if (validate.errors !== null && !assert.hasOwnProperty("errorMessage")) { |
| err = "; Errors: " + this.ajv.errorsText(validate.errors) + ";" ; |
| } |
| if (testAction === 'abort') { |
| err += "; Aborting execution of remaining assertions;"; |
| } else if (testAction === 'skip') { |
| err += "; Skipping execution of remaining assertions at level " + level + ";"; |
| } |
| if (theResult === false) { |
| // test result was unexpected; use message |
| assert_true(theResult, typeMap[type] + message + err); |
| } else { |
| assert_true(theResult, err) ; |
| } |
| }.bind(this), "" + level + ":" + (num+1) + " " + assert.title); |
| } |
| } |
| }.bind(this)); |
| } |
| |
| return { action: testAction, results: theResults} ; |
| }, |
| |
| determineResult: function(schema, valid) { |
| 'use strict'; |
| var r = 'valid' ; |
| if (schema.hasOwnProperty('expectedResult')) { |
| r = schema.expectedResult; |
| } |
| |
| if (r === 'valid' && valid || r === 'invalid' && !valid) { |
| return true; |
| } else { |
| return false; |
| } |
| }, |
| |
| determineAction: function(schema, result) { |
| 'use strict'; |
| // mapping from results to actions |
| var mapping = { |
| 'failAndContinue' : 'continue', |
| 'failAndSkip' : 'skip', |
| 'failAndAbort' : 'abort', |
| 'passAndContinue': 'continue', |
| 'passAndSkip' : 'skip', |
| 'passAndAbort' : 'abort' |
| }; |
| |
| // if the result was as expected, then just keep going |
| if (result) { |
| return 'continue'; |
| } |
| |
| var a = 'failAndContinue'; |
| |
| if (schema.hasOwnProperty('onUnexpectedResult')) { |
| a = schema.onUnexpectedResult; |
| } |
| |
| if (mapping[a]) { |
| return mapping[a]; |
| } else { |
| return 'continue'; |
| } |
| }, |
| |
| // loadAssertion - load an Assertion from an external JSON file |
| // |
| // returns a promise that resolves with the contents of the assertion file |
| |
| loadAssertion: function(afile) { |
| 'use strict'; |
| if (typeof(afile) === 'string') { |
| var theFile = this._parseURI(afile); |
| // it is a file reference - load it |
| return new Promise(function(resolve, reject) { |
| this._loadFile("GET", theFile, true) |
| .then(function(data) { |
| data.assertionFile = afile; |
| this._assertionCache[afile] = data; |
| resolve(data); |
| }.bind(this)) |
| .catch(function(err) { |
| if (typeof err === "object") { |
| err.theFile = theFile; |
| } |
| reject(err); |
| }); |
| }.bind(this)); |
| } |
| else if (afile.hasOwnProperty("assertionFile")) { |
| // this object is referecing an external assertion |
| return new Promise(function(resolve, reject) { |
| var theFile = this._parseURI(afile.assertionFile); |
| this._loadFile("GET", theFile, true) |
| .then(function(external) { |
| // okay - we have an external object |
| Object.keys(afile).forEach(function(key) { |
| if (key !== 'assertionFile') { |
| external[key] = afile[key]; |
| } |
| }); |
| resolve(external); |
| }.bind(this)) |
| .catch(function(err) { |
| if (typeof err === "object") { |
| err.theFile = theFile; |
| } |
| reject(err); |
| }); |
| }.bind(this)); |
| } else { |
| // it is already a loaded assertion - just use it |
| return new Promise(function(resolve) { |
| resolve(afile); |
| }); |
| } |
| }, |
| |
| // loadDefinition - load a JSON Schema definition from an external JSON file |
| // |
| // returns a promise that resolves with the contents of the definition file |
| |
| loadDefinition: function(dfile) { |
| 'use strict'; |
| return new Promise(function(resolve, reject) { |
| this._loadFile("GET", this._parseURI(dfile), true) |
| .then(function(data) { |
| resolve(data); |
| }.bind(this)) |
| .catch(function(err) { |
| reject(err); |
| }); |
| }.bind(this)); |
| }, |
| |
| |
| // loadTest - load a test from an external JSON file |
| // |
| // returns a promise that resolves with the contents of the |
| // test |
| |
| loadTest: function(params) { |
| 'use strict'; |
| |
| if (params.hasOwnProperty('testFile')) { |
| // the test is referred to by a file name |
| return this._loadFile("GET", params.testFile); |
| } // else |
| return new Promise(function(resolve, reject) { |
| if (params.hasOwnProperty('test')) { |
| resolve(params.test); |
| } else { |
| reject("Must supply a 'test' or 'testFile' parameter"); |
| } |
| }); |
| }, |
| |
| _parseURI: function(theURI) { |
| 'use strict'; |
| // determine what the top level URI should be |
| if (theURI.indexOf('/') === -1) { |
| // no slash - it's relative to where we are |
| // so just use it |
| return this.TestDir + theURI; |
| } else if (theURI.indexOf('/') === 0 || theURI.indexOf('http:') === 0 || theURI.indexOf('https:') === 0) { |
| // it is an absolute URI so just use it |
| return theURI; |
| } else { |
| // it is relative and contains a slash. |
| // make it relative to the current test root |
| return this.Base + theURI; |
| } |
| }, |
| |
| /** |
| * return a list of all inline assertions or references |
| * |
| * @param {array} assertions list of assertions to examine |
| */ |
| |
| _assertionRefs: function(assertions) { |
| 'use strict'; |
| var ret = [] ; |
| |
| // when the reference is to an object that has an array of assertions in it (a conditionObject) |
| // then remember that one and loop over its embedded assertions |
| if (typeof(assertions) === "object" && !Array.isArray(assertions) && assertions.hasOwnProperty('assertions')) { |
| ret.push(assertions) ; |
| assertions = assertions.assertions; |
| } |
| if (typeof(assertions) === "object" && Array.isArray(assertions)) { |
| assertions.forEach( function(assert) { |
| // first - what is the type of the assert |
| if (typeof assert === "object" && Array.isArray(assert)) { |
| // it is a nested list - recurse |
| this._assertionRefs(assert).forEach( function(item) { |
| ret.push(item); |
| }.bind(this)); |
| } else if (typeof assert === "object") { |
| ret.push(assert) ; |
| if (assert.hasOwnProperty("assertions")) { |
| // there are embedded assertions; get those too |
| ret.concat(this._assertionRefs(assert.assertions)); |
| } |
| } else { |
| // it is a file name |
| ret.push(assert) ; |
| } |
| }.bind(this)); |
| } |
| return ret; |
| }, |
| |
| // _loadFile - return a promise loading a file |
| // |
| _loadFile: function(method, url, parse) { |
| 'use strict'; |
| if (parse === undefined) { |
| parse = true; |
| } |
| |
| return new Promise(function (resolve, reject) { |
| if (document.location.search) { |
| var s = document.location.search; |
| s = s.replace(/^\?/, ''); |
| if (url.indexOf('?') !== -1) { |
| url += "&" + s; |
| } else { |
| url += "?" + s; |
| } |
| } |
| var xhr = new XMLHttpRequest(); |
| xhr.open(method, url); |
| xhr.onload = function () { |
| if (this.status >= 200 && this.status < 300) { |
| var d = xhr.response; |
| if (parse) { |
| try { |
| d = JSON.parse(d); |
| resolve(d); |
| } |
| catch(err) { |
| reject({ status: this.status, |
| statusText: "Parsing of " + url + " failed: " + err } |
| ); |
| } |
| } else { |
| resolve(d); |
| } |
| } else { |
| reject({ |
| status: this.status, |
| statusText: xhr.statusText |
| }); |
| } |
| }; |
| xhr.onerror = function () { |
| reject({ |
| status: this.status, |
| statusText: xhr.statusText |
| }); |
| }; |
| xhr.send(); |
| }); |
| }, |
| |
| }; |