Make m.request work with async/await correctly. (#2428)

* s/xhr/request/g

`m.xhr` was a relic of the rewrite days prior to the release of v1.0.0,
before it was renamed `m.request` to align with v0.2.x. This just strips
some of that legacy naming.

* Make this work with `async`/`await` correctly.

It looked like a V8 bug, but read the two big code comments and follow
their links. It's a bit more subtle than it looks, and V8's in the right
here.
This commit is contained in:
Isiah Meadows 2019-06-10 19:48:04 -04:00 committed by GitHub
parent 8a7eae00ed
commit 0a1a33a036
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 143 additions and 59 deletions

View file

@ -93,6 +93,7 @@
- `https://unpkg.com/mithril` is configured to receive the *minified* bundle, not the development bundle. - `https://unpkg.com/mithril` is configured to receive the *minified* bundle, not the development bundle.
- The raw bundle itself remains accessible at `mithril.js`, and is *not* browser-wrapped. - The raw bundle itself remains accessible at `mithril.js`, and is *not* browser-wrapped.
- Note: this *will* increase overhead with bundlers like Webpack, Rollup, and Browserify. - Note: this *will* increase overhead with bundlers like Webpack, Rollup, and Browserify.
- request: autoredraw support fixed for `async`/`await` in Chrome ([#2428](https://github.com/MithrilJS/mithril.js/pull/2428) [@isiahmeadows](https://github.com/isiahmeadows))
--- ---

View file

@ -6,6 +6,16 @@ module.exports = function($window, Promise) {
var callbackCount = 0 var callbackCount = 0
var oncompletion var oncompletion
function PromiseProxy(executor) {
return new Promise(executor)
}
// In case the global Promise is some userland library's where they rely on
// `foo instanceof this.constructor`, `this.constructor.resolve(value)`, or
// similar. Let's *not* break them.
PromiseProxy.prototype = Promise.prototype
PromiseProxy.__proto__ = Promise // eslint-disable-line no-proto
function makeRequest(factory) { function makeRequest(factory) {
return function(url, args) { return function(url, args) {
if (typeof url !== "string") { args = url; url = url.url } if (typeof url !== "string") { args = url; url = url.url }
@ -33,6 +43,14 @@ module.exports = function($window, Promise) {
function wrap(promise) { function wrap(promise) {
var then = promise.then var then = promise.then
// Set the constructor, so engines know to not await or resolve
// this as a native promise. At the time of writing, this is
// only necessary for V8, but their behavior is the correct
// behavior per spec. See this spec issue for more details:
// https://github.com/tc39/ecma262/issues/1577. Also, see the
// corresponding comment in `request/tests/test-request.js` for
// a bit more background on the issue at hand.
promise.constructor = PromiseProxy
promise.then = function() { promise.then = function() {
count++ count++
var next = then.apply(promise, arguments) var next = then.apply(promise, arguments)

View file

@ -6,12 +6,12 @@ var xhrMock = require("../../test-utils/xhrMock")
var Request = require("../../request/request") var Request = require("../../request/request")
var Promise = require("../../promise/promise") var Promise = require("../../promise/promise")
o.spec("xhr", function() { o.spec("request", function() {
var mock, xhr, complete var mock, request, complete
o.beforeEach(function() { o.beforeEach(function() {
mock = xhrMock() mock = xhrMock()
var requestService = Request(mock, Promise) var requestService = Request(mock, Promise)
xhr = requestService.request request = requestService.request
complete = o.spy() complete = o.spy()
requestService.setCompletionCallback(complete) requestService.setCompletionCallback(complete)
}) })
@ -23,7 +23,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({a: 1})} return {status: 200, responseText: JSON.stringify({a: 1})}
} }
}) })
xhr({method: "GET", url: "/item"}).then(function(data) { request({method: "GET", url: "/item"}).then(function(data) {
o(data).deepEquals({a: 1}) o(data).deepEquals({a: 1})
}).then(function() { }).then(function() {
done() done()
@ -35,7 +35,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({a: 1})} return {status: 200, responseText: JSON.stringify({a: 1})}
} }
}) })
xhr({url: "/item"}).then(function(data) { request({url: "/item"}).then(function(data) {
o(data).deepEquals({a: 1}) o(data).deepEquals({a: 1})
}).then(function() { }).then(function() {
done() done()
@ -47,7 +47,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({a: 1})} return {status: 200, responseText: JSON.stringify({a: 1})}
} }
}) })
xhr("/item").then(function(data) { request("/item").then(function(data) {
o(data).deepEquals({a: 1}) o(data).deepEquals({a: 1})
}).then(function() { }).then(function() {
done() done()
@ -59,7 +59,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({a: 1})} return {status: 200, responseText: JSON.stringify({a: 1})}
} }
}) })
xhr({method: "POST", url: "/item"}).then(function(data) { request({method: "POST", url: "/item"}).then(function(data) {
o(data).deepEquals({a: 1}) o(data).deepEquals({a: 1})
}).then(done) }).then(done)
}) })
@ -69,7 +69,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({a: 1})} return {status: 200, responseText: JSON.stringify({a: 1})}
} }
}) })
xhr("/item", {method: "POST"}).then(function(data) { request("/item", {method: "POST"}).then(function(data) {
o(data).deepEquals({a: 1}) o(data).deepEquals({a: 1})
}).then(done) }).then(done)
}) })
@ -79,7 +79,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({a: request.query})} return {status: 200, responseText: JSON.stringify({a: request.query})}
} }
}) })
xhr({method: "GET", url: "/item", params: {x: "y"}}).then(function(data) { request({method: "GET", url: "/item", params: {x: "y"}}).then(function(data) {
o(data).deepEquals({a: "?x=y"}) o(data).deepEquals({a: "?x=y"})
}).then(done) }).then(done)
}) })
@ -89,7 +89,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({a: JSON.parse(request.body)})} return {status: 200, responseText: JSON.stringify({a: JSON.parse(request.body)})}
} }
}) })
xhr({method: "POST", url: "/item", body: {x: "y"}}).then(function(data) { request({method: "POST", url: "/item", body: {x: "y"}}).then(function(data) {
o(data).deepEquals({a: {x: "y"}}) o(data).deepEquals({a: {x: "y"}})
}).then(done) }).then(done)
}) })
@ -99,7 +99,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({a: request.query})} return {status: 200, responseText: JSON.stringify({a: request.query})}
} }
}) })
xhr({method: "GET", url: "/item", params: {x: ":y"}}).then(function(data) { request({method: "GET", url: "/item", params: {x: ":y"}}).then(function(data) {
o(data).deepEquals({a: "?x=%3Ay"}) o(data).deepEquals({a: "?x=%3Ay"})
}).then(done) }).then(done)
}) })
@ -109,7 +109,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({a: JSON.parse(request.body)})} return {status: 200, responseText: JSON.stringify({a: JSON.parse(request.body)})}
} }
}) })
xhr({method: "POST", url: "/item", body: {x: ":y"}}).then(function(data) { request({method: "POST", url: "/item", body: {x: ":y"}}).then(function(data) {
o(data).deepEquals({a: {x: ":y"}}) o(data).deepEquals({a: {x: ":y"}})
}).then(done) }).then(done)
}) })
@ -119,7 +119,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: request.body})} return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: request.body})}
} }
}) })
xhr({method: "GET", url: "/item/:x", params: {x: "y"}}).then(function(data) { request({method: "GET", url: "/item/:x", params: {x: "y"}}).then(function(data) {
o(data).deepEquals({a: "/item/y", b: {}, c: null}) o(data).deepEquals({a: "/item/y", b: {}, c: null})
}).then(done) }).then(done)
}) })
@ -129,7 +129,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: request.body})} return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: request.body})}
} }
}) })
xhr({method: "POST", url: "/item/:x", params: {x: "y"}}).then(function(data) { request({method: "POST", url: "/item/:x", params: {x: "y"}}).then(function(data) {
o(data).deepEquals({a: "/item/y", b: {}, c: null}) o(data).deepEquals({a: "/item/y", b: {}, c: null})
}).then(done) }).then(done)
}) })
@ -139,7 +139,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: JSON.parse(request.body)})} return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: JSON.parse(request.body)})}
} }
}) })
xhr({method: "GET", url: "/item/:x", params: {x: "y"}, body: {a: "b"}}).then(function(data) { request({method: "GET", url: "/item/:x", params: {x: "y"}, body: {a: "b"}}).then(function(data) {
o(data).deepEquals({a: "/item/y", b: {}, c: {a: "b"}}) o(data).deepEquals({a: "/item/y", b: {}, c: {a: "b"}})
}).then(done) }).then(done)
}) })
@ -149,7 +149,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: JSON.parse(request.body)})} return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: JSON.parse(request.body)})}
} }
}) })
xhr({method: "POST", url: "/item/:x", params: {x: "y"}, body: {a: "b"}}).then(function(data) { request({method: "POST", url: "/item/:x", params: {x: "y"}, body: {a: "b"}}).then(function(data) {
o(data).deepEquals({a: "/item/y", b: {}, c: {a: "b"}}) o(data).deepEquals({a: "/item/y", b: {}, c: {a: "b"}})
}).then(done) }).then(done)
}) })
@ -159,7 +159,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: request.body})} return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: request.body})}
} }
}) })
xhr({method: "GET", url: "/item/:x", params: {x: "y", q: "term"}}).then(function(data) { request({method: "GET", url: "/item/:x", params: {x: "y", q: "term"}}).then(function(data) {
o(data).deepEquals({a: "/item/y", b: "?q=term", c: null}) o(data).deepEquals({a: "/item/y", b: "?q=term", c: null})
}).then(done) }).then(done)
}) })
@ -169,7 +169,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: request.body})} return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: request.body})}
} }
}) })
xhr({method: "POST", url: "/item/:x", params: {x: "y", q: "term"}}).then(function(data) { request({method: "POST", url: "/item/:x", params: {x: "y", q: "term"}}).then(function(data) {
o(data).deepEquals({a: "/item/y", b: "?q=term", c: null}) o(data).deepEquals({a: "/item/y", b: "?q=term", c: null})
}).then(done) }).then(done)
}) })
@ -179,7 +179,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: JSON.parse(request.body)})} return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: JSON.parse(request.body)})}
} }
}) })
xhr({method: "GET", url: "/item/:x", params: {x: "y", q: "term"}, body: {a: "b"}}).then(function(data) { request({method: "GET", url: "/item/:x", params: {x: "y", q: "term"}, body: {a: "b"}}).then(function(data) {
o(data).deepEquals({a: "/item/y", b: "?q=term", c: {a: "b"}}) o(data).deepEquals({a: "/item/y", b: "?q=term", c: {a: "b"}})
}).then(done) }).then(done)
}) })
@ -189,7 +189,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: JSON.parse(request.body)})} return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: JSON.parse(request.body)})}
} }
}) })
xhr({method: "POST", url: "/item/:x", params: {x: "y", q: "term"}, body: {a: "b"}}).then(function(data) { request({method: "POST", url: "/item/:x", params: {x: "y", q: "term"}, body: {a: "b"}}).then(function(data) {
o(data).deepEquals({a: "/item/y", b: "?q=term", c: {a: "b"}}) o(data).deepEquals({a: "/item/y", b: "?q=term", c: {a: "b"}})
}).then(done) }).then(done)
}) })
@ -199,7 +199,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({a: request.url, b: JSON.parse(request.body)})} return {status: 200, responseText: JSON.stringify({a: request.url, b: JSON.parse(request.body)})}
} }
}) })
xhr({method: "POST", url: "/items", body: [{x: "y"}]}).then(function(data) { request({method: "POST", url: "/items", body: [{x: "y"}]}).then(function(data) {
o(data).deepEquals({a: "/items", b: [{x: "y"}]}) o(data).deepEquals({a: "/items", b: [{x: "y"}]})
}).then(done) }).then(done)
}) })
@ -209,7 +209,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({a: request.url})} return {status: 200, responseText: JSON.stringify({a: request.url})}
} }
}) })
xhr({method: "GET", url: "/item/:x"}).then(function(data) { request({method: "GET", url: "/item/:x"}).then(function(data) {
o(data).deepEquals({a: "/item/:x"}) o(data).deepEquals({a: "/item/:x"})
}).then(done) }).then(done)
}) })
@ -219,7 +219,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({a: request.url})} return {status: 200, responseText: JSON.stringify({a: request.url})}
} }
}) })
xhr({method: "GET", url: "/item/:x"}).then(function(data) { request({method: "GET", url: "/item/:x"}).then(function(data) {
o(data).deepEquals({a: "/item/:x"}) o(data).deepEquals({a: "/item/:x"})
}).then(done) }).then(done)
}) })
@ -233,7 +233,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify([{id: 1}, {id: 2}, {id: 3}])} return {status: 200, responseText: JSON.stringify([{id: 1}, {id: 2}, {id: 3}])}
} }
}) })
xhr({method: "GET", url: "/item", type: Entity}).then(function(data) { request({method: "GET", url: "/item", type: Entity}).then(function(data) {
o(data).deepEquals([{_id: 1}, {_id: 2}, {_id: 3}]) o(data).deepEquals([{_id: 1}, {_id: 2}, {_id: 3}])
}).then(done) }).then(done)
}) })
@ -247,7 +247,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({id: 1})} return {status: 200, responseText: JSON.stringify({id: 1})}
} }
}) })
xhr({method: "GET", url: "/item", type: Entity}).then(function(data) { request({method: "GET", url: "/item", type: Entity}).then(function(data) {
o(data).deepEquals({_id: 1}) o(data).deepEquals({_id: 1})
}).then(done) }).then(done)
}) })
@ -261,7 +261,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({body: request.query})} return {status: 200, responseText: JSON.stringify({body: request.query})}
} }
}) })
xhr({method: "GET", url: "/item", serialize: serialize, params: {id: 1}}).then(function(data) { request({method: "GET", url: "/item", serialize: serialize, params: {id: 1}}).then(function(data) {
o(data.body).equals("?id=1") o(data.body).equals("?id=1")
}).then(done) }).then(done)
}) })
@ -275,7 +275,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({body: request.body})} return {status: 200, responseText: JSON.stringify({body: request.body})}
} }
}) })
xhr({method: "POST", url: "/item", serialize: serialize, body: {id: 1}}).then(function(data) { request({method: "POST", url: "/item", serialize: serialize, body: {id: 1}}).then(function(data) {
o(data.body).equals("id=1") o(data.body).equals("id=1")
}).then(done) }).then(done)
}) })
@ -289,7 +289,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({test: 123})} return {status: 200, responseText: JSON.stringify({test: 123})}
} }
}) })
xhr({method: "GET", url: "/item", deserialize: deserialize}).then(function(data) { request({method: "GET", url: "/item", deserialize: deserialize}).then(function(data) {
o(data).deepEquals({test: 123}) o(data).deepEquals({test: 123})
}).then(done) }).then(done)
}) })
@ -303,7 +303,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({test: 123})} return {status: 200, responseText: JSON.stringify({test: 123})}
} }
}) })
xhr({method: "POST", url: "/item", deserialize: deserialize}).then(function(data) { request({method: "POST", url: "/item", deserialize: deserialize}).then(function(data) {
o(data).deepEquals({test: 123}) o(data).deepEquals({test: 123})
}).then(done) }).then(done)
}) })
@ -317,7 +317,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: ""} return {status: 200, responseText: ""}
} }
}) })
xhr({method: "GET", url: "/item", extract: extract}).then(function(data) { request({method: "GET", url: "/item", extract: extract}).then(function(data) {
o(data).deepEquals({test: 123}) o(data).deepEquals({test: 123})
}).then(done) }).then(done)
}) })
@ -331,7 +331,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: ""} return {status: 200, responseText: ""}
} }
}) })
xhr({method: "POST", url: "/item", extract: extract}).then(function(data) { request({method: "POST", url: "/item", extract: extract}).then(function(data) {
o(data).deepEquals({test: 123}) o(data).deepEquals({test: 123})
}).then(done) }).then(done)
}) })
@ -346,7 +346,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: ""} return {status: 200, responseText: ""}
} }
}) })
xhr({method: "GET", url: "/item", extract: extract, deserialize: deserialize}).then(function(data) { request({method: "GET", url: "/item", extract: extract, deserialize: deserialize}).then(function(data) {
o(data).equals(200) o(data).equals(200)
}).then(function() { }).then(function() {
o(deserialize.callCount).equals(0) o(deserialize.callCount).equals(0)
@ -358,7 +358,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: ""} return {status: 200, responseText: ""}
} }
}) })
xhr({method: "POST", url: "/item", config: config}).then(done) request({method: "POST", url: "/item", config: config}).then(done)
function config(xhr) { function config(xhr) {
o(typeof xhr.setRequestHeader).equals("function") o(typeof xhr.setRequestHeader).equals("function")
@ -372,11 +372,11 @@ o.spec("xhr", function() {
return {status: 200, responseText: "[]"} return {status: 200, responseText: "[]"}
} }
}) })
xhr("/item").then(function() { request("/item").then(function() {
return xhr("/item") return request("/item")
}) })
xhr("/item").then(function() { request("/item").then(function() {
return xhr("/item") return request("/item")
}) })
setTimeout(function() { setTimeout(function() {
o(complete.callCount).equals(4) o(complete.callCount).equals(4)
@ -389,7 +389,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: "[]"} return {status: 200, responseText: "[]"}
} }
}) })
var promise = xhr("/item") var promise = request("/item")
promise.then(function() {}).then(function() {}) promise.then(function() {}).then(function() {})
promise.then(function() {}).then(function() {}) promise.then(function() {}).then(function() {})
setTimeout(function() { setTimeout(function() {
@ -403,7 +403,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: "[]"} return {status: 200, responseText: "[]"}
} }
}) })
xhr("/item", {background: true}).then(function() {}) request("/item", {background: true}).then(function() {})
setTimeout(function() { setTimeout(function() {
o(complete.callCount).equals(0) o(complete.callCount).equals(0)
@ -416,7 +416,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: ""} return {status: 200, responseText: ""}
} }
}) })
xhr({method: "POST", url: "/item", config: config, headers: {"Custom-Header": "Value"}}).then(done) request({method: "POST", url: "/item", config: config, headers: {"Custom-Header": "Value"}}).then(done)
function config(xhr) { function config(xhr) {
o(xhr.getRequestHeader("Custom-Header")).equals("Value") o(xhr.getRequestHeader("Custom-Header")).equals("Value")
@ -428,7 +428,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: ""} return {status: 200, responseText: ""}
} }
}) })
xhr({method: "POST", url: "/item", config: config, headers: {"Content-Type": "Value"}}).then(done) request({method: "POST", url: "/item", config: config, headers: {"Content-Type": "Value"}}).then(done)
function config(xhr) { function config(xhr) {
o(xhr.getRequestHeader("Content-Type")).equals("Value") o(xhr.getRequestHeader("Content-Type")).equals("Value")
@ -459,7 +459,7 @@ o.spec("xhr", function() {
}) })
xhr.abort() xhr.abort()
} }
xhr({method: "GET", url: "/item", config: handleAbort}).catch(function() { request({method: "GET", url: "/item", config: handleAbort}).catch(function() {
failed = true failed = true
}) })
.then(function() { .then(function() {
@ -473,7 +473,7 @@ o.spec("xhr", function() {
} }
}) })
var failed = false var failed = false
xhr({method: "GET", url: "file:///item"}).catch(function() { request({method: "GET", url: "file:///item"}).catch(function() {
failed = true failed = true
}).then(function(data) { }).then(function(data) {
o(failed).equals(false) o(failed).equals(false)
@ -488,7 +488,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: ""} return {status: 200, responseText: ""}
} }
}) })
return xhr({ return request({
method: "GET", url: "/item", method: "GET", url: "/item",
timeout: 42, timeout: 42,
config: function(xhr) { config: function(xhr) {
@ -496,13 +496,13 @@ o.spec("xhr", function() {
} }
}) })
}) })
o("set responseType to xhr instance", function() { o("set responseType to request instance", function() {
mock.$defineRoutes({ mock.$defineRoutes({
"GET /item": function() { "GET /item": function() {
return {status: 200, responseText: ""} return {status: 200, responseText: ""}
} }
}) })
return xhr({ return request({
method: "GET", url: "/item", method: "GET", url: "/item",
responseType: "blob", responseType: "blob",
config: function(xhr) { config: function(xhr) {
@ -518,7 +518,7 @@ o.spec("xhr", function() {
}) })
var data = {x: 1, y: 2} var data = {x: 1, y: 2}
var dataCopy = Object.assign({}, data); var dataCopy = Object.assign({}, data);
xhr({method: "PUT", url: "/items/:x", data}) request({method: "PUT", url: "/items/:x", data})
o(data).deepEquals(dataCopy) o(data).deepEquals(dataCopy)
})*/ })*/
@ -530,7 +530,7 @@ o.spec("xhr", function() {
return {status: 500, responseText: JSON.stringify({error: "error"})} return {status: 500, responseText: JSON.stringify({error: "error"})}
} }
}) })
xhr({method: "GET", url: "/item"}).catch(function(e) { request({method: "GET", url: "/item"}).catch(function(e) {
o(e instanceof Error).equals(true) o(e instanceof Error).equals(true)
o(e.message).equals("[object Object]") o(e.message).equals("[object Object]")
o(e.response).deepEquals({error: "error"}) o(e.response).deepEquals({error: "error"})
@ -543,7 +543,7 @@ o.spec("xhr", function() {
return {status: 500, responseText: JSON.stringify({message: "error", stack: "error on line 1"})} return {status: 500, responseText: JSON.stringify({message: "error", stack: "error on line 1"})}
} }
}) })
xhr({method: "GET", url: "/item"}).catch(function(e) { request({method: "GET", url: "/item"}).catch(function(e) {
o(e instanceof Error).equals(true) o(e instanceof Error).equals(true)
o(e.response.message).equals("error") o(e.response.message).equals("error")
o(e.response.stack).equals("error on line 1") o(e.response.stack).equals("error on line 1")
@ -555,7 +555,7 @@ o.spec("xhr", function() {
return {status: 500, responseText: "error"} return {status: 500, responseText: "error"}
} }
}) })
xhr({method: "GET", url: "/item"}).catch(function(e) { request({method: "GET", url: "/item"}).catch(function(e) {
o(e.message).equals("null") o(e.message).equals("null")
o(e.response).equals(null) o(e.response).equals(null)
}).then(done) }).then(done)
@ -566,15 +566,15 @@ o.spec("xhr", function() {
return {status: 500, responseText: "error"} return {status: 500, responseText: "error"}
} }
}) })
var request = xhr({method: "GET", url: "/item"}) var promise = request({method: "GET", url: "/item"})
var then = o.spy() var then = o.spy()
var catch1 = o.spy() var catch1 = o.spy()
var catch2 = o.spy() var catch2 = o.spy()
var catch3 = o.spy() var catch3 = o.spy()
request.catch(catch1) promise.catch(catch1)
request.then(then, catch2) promise.then(then, catch2)
request.then(then).catch(catch3) promise.then(then).catch(catch3)
callAsync(function() { callAsync(function() {
callAsync(function() { callAsync(function() {
@ -592,7 +592,7 @@ o.spec("xhr", function() {
return {status: 0} return {status: 0}
} }
}) })
xhr({method: "GET", url: "/item"}).catch(function(e) { request({method: "GET", url: "/item"}).catch(function(e) {
o(e instanceof Error).equals(true) o(e instanceof Error).equals(true)
}).then(done) }).then(done)
}) })
@ -602,7 +602,7 @@ o.spec("xhr", function() {
return {status: 500, responseText: JSON.stringify({message: "error"})} return {status: 500, responseText: JSON.stringify({message: "error"})}
} }
}) })
xhr({ request({
method: "GET", url: "/item", method: "GET", url: "/item",
extract: function(xhr) {return JSON.parse(xhr.responseText)} extract: function(xhr) {return JSON.parse(xhr.responseText)}
}).then(function(data) { }).then(function(data) {
@ -616,7 +616,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({a: 1})} return {status: 200, responseText: JSON.stringify({a: 1})}
} }
}) })
xhr({ request({
method: "GET", url: "/item", method: "GET", url: "/item",
extract: function() {throw new Error("error")} extract: function() {throw new Error("error")}
}).catch(function(e) { }).catch(function(e) {
@ -635,7 +635,7 @@ o.spec("xhr", function() {
return {status: 200, responseText: JSON.stringify({a: 1})} return {status: 200, responseText: JSON.stringify({a: 1})}
} }
mock.$defineRoutes(routes) mock.$defineRoutes(routes)
xhr({ request({
method: method, url: "/item", method: method, url: "/item",
config: function(xhr) { config: function(xhr) {
var header = xhr.getRequestHeader("Content-Type") var header = xhr.getRequestHeader("Content-Type")
@ -662,7 +662,7 @@ o.spec("xhr", function() {
} }
} }
mock.$defineRoutes(routes) mock.$defineRoutes(routes)
xhr({ request({
method: method, url: "/item", body: body, method: method, url: "/item", body: body,
config: function(xhr) { config: function(xhr) {
var header = xhr.getRequestHeader("Content-Type") var header = xhr.getRequestHeader("Content-Type")
@ -695,4 +695,69 @@ o.spec("xhr", function() {
checkSet("DELETE", {foo: "bar"}) checkSet("DELETE", {foo: "bar"})
checkSet("PATCH", {foo: "bar"}) checkSet("PATCH", {foo: "bar"})
}) })
// See: https://github.com/MithrilJS/mithril.js/issues/2426
//
// TL;DR: lots of subtlety. Make sure you read the ES spec closely before
// updating this code or the corresponding finalizer code in
// `request/request` responsible for scheduling autoredraws, or you might
// inadvertently break things.
//
// The precise behavior here is that it schedules a redraw immediately after
// the second tick *after* the promise resolves, but `await` in engines that
// have implemented the change in https://github.com/tc39/ecma262/pull/1250
// will only take one tick to get the value. Engines that haven't
// implemented that spec change would wait until the tick after the redraw
// was scheduled before it can see the new value. But this only applies when
// the engine needs to coerce the value, and this is where things get a bit
// hairy. As per spec, V8 checks the `.constructor` property of promises and
// if that `=== Promise`, it does *not* coerce it using `.then`, but instead
// just resolves it directly. This, of course, can screw with our autoredraw
// behavior, and we have to work around that. At the time of writing, no
// other browser checks for this additional constraint, and just blindly
// invokes `.then` instead, and so we end up working as anticipated. But for
// obvious reasons, it's a bad idea to rely on a spec violation for things
// to work unless the spec itself is clearly broken (in this case, it's
// not). And so we need to test for this very unusual edge case.
//
// The direct `eval` is just so I can convert early errors to runtime
// errors without having to explicitly wire up all the bindings set up in
// `o.beforeEach`. I evaluate it immediately inside a `try`/`catch` instead
// of inside the test code so any relevant syntax error can be detected
// ahead of time and the test skipped entirely. It might trigger mental
// alarms because `eval` is normally asking for problems, but this is a
// rare case where it's genuinely safe and rational.
try {
// eslint-disable-next-line no-eval
var runAsyncTest = eval(
"async () => {\n" +
" var p = request('/item')\n" +
" o(complete.callCount).equals(0)\n" +
// Note: this step does *not* invoke `.then` on the promise returned
// from `p.then(resolve, reject)`.
" await p\n" +
// The spec prior to https://github.com/tc39/ecma262/pull/1250 used
// to take 3 ticks instead of 1, so `complete` would have been
// called already and we would've been done. After it, it now takes
// 1 tick and so `complete` wouldn't have yet been called - it takes
// 2 ticks to get called. And so we have to wait for one more ticks
// for `complete` to get called.
" await null\n" +
" o(complete.callCount).equals(1)\n" +
"}"
)
o("invokes the redraw in native async/await", function () {
mock.$defineRoutes({
"GET /item": function() {
return {status: 200, responseText: "[]"}
}
})
return runAsyncTest()
})
} catch (e) {
// ignore - this is just for browsers that natively support
// `async`/`await`, like most modern browsers.
// it's just a syntax error anyways.
}
}) })