UNPKG

mithril

Version:

A framework for building brilliant applications

911 lines (877 loc) 28.3 kB
"use strict" var o = require("ospec") var callAsync = require("../../test-utils/callAsync") var xhrMock = require("../../test-utils/xhrMock") var Request = require("../../request/request") var PromisePolyfill = require("../../promise/promise") o.spec("request", function() { var mock, request, complete o.beforeEach(function() { mock = xhrMock() complete = o.spy() request = Request(mock, PromisePolyfill, complete).request }) o.spec("success", function() { o("works via GET", function(done) { mock.$defineRoutes({ "GET /item": function() { return {status: 200, responseText: JSON.stringify({a: 1})} } }) request({method: "GET", url: "/item"}).then(function(data) { o(data).deepEquals({a: 1}) }).then(function() { done() }) }) o("implicit GET method", function(done){ mock.$defineRoutes({ "GET /item": function() { return {status: 200, responseText: JSON.stringify({a: 1})} } }) request({url: "/item"}).then(function(data) { o(data).deepEquals({a: 1}) }).then(function() { done() }) }) o("first argument can be a string aliasing url property", function(done){ mock.$defineRoutes({ "GET /item": function() { return {status: 200, responseText: JSON.stringify({a: 1})} } }) request("/item").then(function(data) { o(data).deepEquals({a: 1}) }).then(function() { done() }) }) o("works via POST", function(done) { mock.$defineRoutes({ "POST /item": function() { return {status: 200, responseText: JSON.stringify({a: 1})} } }) request({method: "POST", url: "/item"}).then(function(data) { o(data).deepEquals({a: 1}) }).then(done) }) o("first argument can act as URI with second argument providing options", function(done) { mock.$defineRoutes({ "POST /item": function() { return {status: 200, responseText: JSON.stringify({a: 1})} } }) request("/item", {method: "POST"}).then(function(data) { o(data).deepEquals({a: 1}) }).then(done) }) o("first argument keeps protocol", function(done) { mock.$defineRoutes({ "POST /item": function(request) { o(request.rawUrl).equals("https://example.com/item") return {status: 200, responseText: JSON.stringify({a: 1})} } }) request("https://example.com/item", {method: "POST"}).then(function(data) { o(data).deepEquals({a: 1}) }).then(done) }) o("works w/ parameterized data via GET", function(done) { mock.$defineRoutes({ "GET /item": function(request) { return {status: 200, responseText: JSON.stringify({a: request.query})} } }) request({method: "GET", url: "/item", params: {x: "y"}}).then(function(data) { o(data).deepEquals({a: "?x=y"}) }).then(done) }) o("works w/ parameterized data via POST", function(done) { mock.$defineRoutes({ "POST /item": function(request) { return {status: 200, responseText: JSON.stringify({a: JSON.parse(request.body)})} } }) request({method: "POST", url: "/item", body: {x: "y"}}).then(function(data) { o(data).deepEquals({a: {x: "y"}}) }).then(done) }) o("works w/ parameterized data containing colon via GET", function(done) { mock.$defineRoutes({ "GET /item": function(request) { return {status: 200, responseText: JSON.stringify({a: request.query})} } }) request({method: "GET", url: "/item", params: {x: ":y"}}).then(function(data) { o(data).deepEquals({a: "?x=%3Ay"}) }).then(done) }) o("works w/ parameterized data containing colon via POST", function(done) { mock.$defineRoutes({ "POST /item": function(request) { return {status: 200, responseText: JSON.stringify({a: JSON.parse(request.body)})} } }) request({method: "POST", url: "/item", body: {x: ":y"}}).then(function(data) { o(data).deepEquals({a: {x: ":y"}}) }).then(done) }) o("works w/ parameterized url via GET", function(done) { mock.$defineRoutes({ "GET /item/y": function(request) { return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: request.body})} } }) request({method: "GET", url: "/item/:x", params: {x: "y"}}).then(function(data) { o(data).deepEquals({a: "/item/y", b: {}, c: null}) }).then(done) }) o("works w/ parameterized url via POST", function(done) { mock.$defineRoutes({ "POST /item/y": function(request) { return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: request.body})} } }) request({method: "POST", url: "/item/:x", params: {x: "y"}}).then(function(data) { o(data).deepEquals({a: "/item/y", b: {}, c: null}) }).then(done) }) o("works w/ parameterized url + body via GET", function(done) { mock.$defineRoutes({ "GET /item/y": function(request) { return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: JSON.parse(request.body)})} } }) 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"}}) }).then(done) }) o("works w/ parameterized url + body via POST", function(done) { mock.$defineRoutes({ "POST /item/y": function(request) { return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: JSON.parse(request.body)})} } }) 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"}}) }).then(done) }) o("works w/ parameterized url + query via GET", function(done) { mock.$defineRoutes({ "GET /item/y": function(request) { return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: request.body})} } }) 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}) }).then(done) }) o("works w/ parameterized url + query via POST", function(done) { mock.$defineRoutes({ "POST /item/y": function(request) { return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: request.body})} } }) 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}) }).then(done) }) o("works w/ parameterized url + query + body via GET", function(done) { mock.$defineRoutes({ "GET /item/y": function(request) { return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: JSON.parse(request.body)})} } }) 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"}}) }).then(done) }) o("works w/ parameterized url + query + body via POST", function(done) { mock.$defineRoutes({ "POST /item/y": function(request) { return {status: 200, responseText: JSON.stringify({a: request.url, b: request.query, c: JSON.parse(request.body)})} } }) 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"}}) }).then(done) }) o("works w/ array", function(done) { mock.$defineRoutes({ "POST /items": function(request) { return {status: 200, responseText: JSON.stringify({a: request.url, b: JSON.parse(request.body)})} } }) request({method: "POST", url: "/items", body: [{x: "y"}]}).then(function(data) { o(data).deepEquals({a: "/items", b: [{x: "y"}]}) }).then(done) }) o("works w/ URLSearchParams body", function(done) { mock.$defineRoutes({ "POST /item": function(request) { return {status: 200, responseText: JSON.stringify({a: request.url, b: request.body.toString()})} } }) request({method: "POST", url: "/item", body: new URLSearchParams({x: "y", z: "w"})}).then(function(data) { o(data).deepEquals({a: "/item", b: "x=y&z=w"}) }).then(done) }); o("ignores unresolved parameter via GET", function(done) { mock.$defineRoutes({ "GET /item/:x": function(request) { return {status: 200, responseText: JSON.stringify({a: request.url})} } }) request({method: "GET", url: "/item/:x"}).then(function(data) { o(data).deepEquals({a: "/item/:x"}) }).then(done) }) o("ignores unresolved parameter via POST", function(done) { mock.$defineRoutes({ "GET /item/:x": function(request) { return {status: 200, responseText: JSON.stringify({a: request.url})} } }) request({method: "GET", url: "/item/:x"}).then(function(data) { o(data).deepEquals({a: "/item/:x"}) }).then(done) }) o("type parameter works for Array responses", function(done) { var Entity = function(args) { return {_id: args.id} } mock.$defineRoutes({ "GET /item": function() { return {status: 200, responseText: JSON.stringify([{id: 1}, {id: 2}, {id: 3}])} } }) request({method: "GET", url: "/item", type: Entity}).then(function(data) { o(data).deepEquals([{_id: 1}, {_id: 2}, {_id: 3}]) }).then(done) }) o("type parameter works for Object responses", function(done) { var Entity = function(args) { return {_id: args.id} } mock.$defineRoutes({ "GET /item": function() { return {status: 200, responseText: JSON.stringify({id: 1})} } }) request({method: "GET", url: "/item", type: Entity}).then(function(data) { o(data).deepEquals({_id: 1}) }).then(done) }) o("serialize parameter works in GET", function(done) { var serialize = function(data) { return "id=" + data.id } mock.$defineRoutes({ "GET /item": function(request) { return {status: 200, responseText: JSON.stringify({body: request.query})} } }) request({method: "GET", url: "/item", serialize: serialize, params: {id: 1}}).then(function(data) { o(data.body).equals("?id=1") }).then(done) }) o("serialize parameter works in POST", function(done) { var serialize = function(data) { return "id=" + data.id } mock.$defineRoutes({ "POST /item": function(request) { return {status: 200, responseText: JSON.stringify({body: request.body})} } }) request({method: "POST", url: "/item", serialize: serialize, body: {id: 1}}).then(function(data) { o(data.body).equals("id=1") }).then(done) }) o("deserialize parameter works in GET", function(done) { var deserialize = function(data) { return data } mock.$defineRoutes({ "GET /item": function() { return {status: 200, responseText: JSON.stringify({test: 123})} } }) request({method: "GET", url: "/item", deserialize: deserialize}).then(function(data) { o(data).deepEquals({test: 123}) }).then(done) }) o("deserialize parameter works in POST", function(done) { var deserialize = function(data) { return data } mock.$defineRoutes({ "POST /item": function() { return {status: 200, responseText: JSON.stringify({test: 123})} } }) request({method: "POST", url: "/item", deserialize: deserialize}).then(function(data) { o(data).deepEquals({test: 123}) }).then(done) }) o("extract parameter works in GET", function(done) { var extract = function() { return {test: 123} } mock.$defineRoutes({ "GET /item": function() { return {status: 200, responseText: ""} } }) request({method: "GET", url: "/item", extract: extract}).then(function(data) { o(data).deepEquals({test: 123}) }).then(done) }) o("extract parameter works in POST", function(done) { var extract = function() { return {test: 123} } mock.$defineRoutes({ "POST /item": function() { return {status: 200, responseText: ""} } }) request({method: "POST", url: "/item", extract: extract}).then(function(data) { o(data).deepEquals({test: 123}) }).then(done) }) o("ignores deserialize if extract is defined", function(done) { var extract = function(data) { return data.status } var deserialize = o.spy() mock.$defineRoutes({ "GET /item": function() { return {status: 200, responseText: ""} } }) request({method: "GET", url: "/item", extract: extract, deserialize: deserialize}).then(function(data) { o(data).equals(200) }).then(function() { o(deserialize.callCount).equals(0) }).then(done) }) o("config parameter works", function(done) { mock.$defineRoutes({ "POST /item": function() { return {status: 200, responseText: ""} } }) request({method: "POST", url: "/item", config: config}).then(done) function config(xhr) { o(typeof xhr.setRequestHeader).equals("function") o(typeof xhr.open).equals("function") o(typeof xhr.send).equals("function") } }) o("requests don't block each other", function(done) { mock.$defineRoutes({ "GET /item": function() { return {status: 200, responseText: "[]"} } }) request("/item").then(function() { return request("/item") }) request("/item").then(function() { return request("/item") }) setTimeout(function() { o(complete.callCount).equals(4) done() }, 20) }) o("requests trigger finally once with a chained then", function(done) { mock.$defineRoutes({ "GET /item": function() { return {status: 200, responseText: "[]"} } }) var promise = request("/item") promise.then(function() {}).then(function() {}) promise.then(function() {}).then(function() {}) setTimeout(function() { o(complete.callCount).equals(1) done() }, 20) }) o("requests does not trigger finally when background: true", function(done) { mock.$defineRoutes({ "GET /item": function() { return {status: 200, responseText: "[]"} } }) request("/item", {background: true}).then(function() {}) setTimeout(function() { o(complete.callCount).equals(0) done() }, 20) }) o("headers are set when header arg passed", function(done) { mock.$defineRoutes({ "POST /item": function() { return {status: 200, responseText: ""} } }) request({method: "POST", url: "/item", config: config, headers: {"Custom-Header": "Value"}}).then(done) function config(xhr) { o(xhr.getRequestHeader("Custom-Header")).equals("Value") } }) o("headers are with higher precedence than default headers", function(done) { mock.$defineRoutes({ "POST /item": function() { return {status: 200, responseText: ""} } }) request({method: "POST", url: "/item", config: config, headers: {"Content-Type": "Value"}}).then(done) function config(xhr) { o(xhr.getRequestHeader("Content-Type")).equals("Value") } }) o("doesn't fail on abort", function(done) { mock.$defineRoutes({ "GET /item": function() { return {status: 200, responseText: JSON.stringify({a: 1})} } }) var failed = false var resolved = false function handleAbort(xhr) { var onreadystatechange = xhr.onreadystatechange xhr.onreadystatechange = function() { onreadystatechange.call(xhr, {target: xhr}) setTimeout(function() { // allow promises to (not) resolve first o(failed).equals(false) o(resolved).equals(false) done() }, 0) } xhr.abort() } request({method: "GET", url: "/item", config: handleAbort}).catch(function() { failed = true }) .then(function() { resolved = true }) }) o("doesn't fail on replaced abort", function(done) { mock.$defineRoutes({ "GET /item": function() { return {status: 200, responseText: JSON.stringify({a: 1})} } }) var failed = false var resolved = false var abortSpy = o.spy() var replacement function handleAbort(xhr) { var onreadystatechange = xhr.onreadystatechange xhr.onreadystatechange = function() { onreadystatechange.call(xhr, {target: xhr}) setTimeout(function() { // allow promises to (not) resolve first o(failed).equals(false) o(resolved).equals(false) done() }, 0) } return replacement = { send: xhr.send.bind(xhr), abort: abortSpy, } } request({method: "GET", url: "/item", config: handleAbort}).then(function() { resolved = true }, function() { failed = true }) replacement.abort() o(abortSpy.callCount).equals(1) }) o("doesn't fail on file:// status 0", function(done) { mock.$defineRoutes({ "GET /item": function() { return {status: 0, responseText: JSON.stringify({a: 1})} } }) var failed = false request({method: "GET", url: "file:///item"}).catch(function() { failed = true }).then(function(data) { o(failed).equals(false) o(data).deepEquals({a: 1}) }).then(function() { done() }) }) o("set timeout to xhr instance", function() { mock.$defineRoutes({ "GET /item": function() { return {status: 200, responseText: ""} } }) return request({ method: "GET", url: "/item", timeout: 42, config: function(xhr) { o(xhr.timeout).equals(42) } }) }) o("set responseType to request instance", function() { mock.$defineRoutes({ "GET /item": function() { return {status: 200, responseText: ""} } }) return request({ method: "GET", url: "/item", responseType: "blob", config: function(xhr) { o(xhr.responseType).equals("blob") } }) }) o("params unmodified after interpolate", function() { mock.$defineRoutes({ "PUT /items/1": function() { return {status: 200, responseText: "[]"} } }) var params = {x: 1, y: 2} var p = request({method: "PUT", url: "/items/:x", params: params}) o(params).deepEquals({x: 1, y: 2}) return p }) o("can return replacement from config", function() { mock.$defineRoutes({ "GET /a": function() { return {status: 200, responseText: "[]"} } }) var result return request({ url: "/a", config: function(xhr) { return result = { send: o.spy(xhr.send.bind(xhr)), } }, }) .then(function () { o(result.send.callCount).equals(1) }) }) o("can abort from replacement", function() { mock.$defineRoutes({ "GET /a": function() { return {status: 200, responseText: "[]"} } }) var result request({ url: "/a", config: function(xhr) { return result = { send: o.spy(xhr.send.bind(xhr)), abort: o.spy(), } }, }) result.abort() }) }) o.spec("failure", function() { o("rejects on server error", function(done) { mock.$defineRoutes({ "GET /item": function() { return {status: 500, responseText: JSON.stringify({error: "error"})} } }) request({method: "GET", url: "/item"}).catch(function(e) { o(e instanceof Error).equals(true) o(e.message).equals("[object Object]") o(e.response).deepEquals({error: "error"}) o(e.code).equals(500) }).then(done) }) o("adds response to Error", function(done) { mock.$defineRoutes({ "GET /item": function() { return {status: 500, responseText: JSON.stringify({message: "error", stack: "error on line 1"})} } }) request({method: "GET", url: "/item"}).catch(function(e) { o(e instanceof Error).equals(true) o(e.response.message).equals("error") o(e.response.stack).equals("error on line 1") }).then(done) }) o("rejects on non-JSON server error", function(done) { mock.$defineRoutes({ "GET /item": function() { return {status: 500, responseText: "error"} } }) request({method: "GET", url: "/item"}).catch(function(e) { o(e.message).equals("null") o(e.response).equals(null) }).then(done) }) o("triggers all branched catches upon rejection", function(done) { mock.$defineRoutes({ "GET /item": function() { return {status: 500, responseText: "error"} } }) var promise = request({method: "GET", url: "/item"}) var then = o.spy() var catch1 = o.spy() var catch2 = o.spy() var catch3 = o.spy() promise.catch(catch1) promise.then(then, catch2) promise.then(then).catch(catch3) callAsync(function() { callAsync(function() { callAsync(function() { o(catch1.callCount).equals(1) o(then.callCount).equals(0) o(catch2.callCount).equals(1) o(catch3.callCount).equals(1) done() }) }) }) }) o("rejects on cors-like error", function(done) { mock.$defineRoutes({ "GET /item": function() { return {status: 0} } }) request({method: "GET", url: "/item"}).catch(function(e) { o(e instanceof Error).equals(true) }).then(done) }) o("rejects on request timeout", function(done) { var timeout = 50 var timeToGetItem = timeout + 1 mock.$defineRoutes({ "GET /item": function() { return new Promise(function(resolve) { setTimeout(function() { resolve({status: 200}) }, timeToGetItem) }) } }) request({ method: "GET", url: "/item", timeout: timeout }).catch(function(e) { o(e instanceof Error).equals(true) o(e.message).equals("Request timed out") o(e.code).equals(0) }).then(function() { done() }) }) o("does not reject when time to request resource does not exceed timeout", function(done) { var timeout = 50 var timeToGetItem = timeout - 1 var isRequestRejected = false mock.$defineRoutes({ "GET /item": function() { return new Promise(function(resolve) { setTimeout(function() { resolve({status: 200}) }, timeToGetItem) }) } }) request({ method: "GET", url: "/item", timeout: timeout }).catch(function(e) { isRequestRejected = true o(e.message).notEquals("Request timed out") }).then(function() { o(isRequestRejected).equals(false) done() }) }) o("does not reject on status error code when extract provided", function(done) { mock.$defineRoutes({ "GET /item": function() { return {status: 500, responseText: JSON.stringify({message: "error"})} } }) request({ method: "GET", url: "/item", extract: function(xhr) {return JSON.parse(xhr.responseText)} }).then(function(data) { o(data.message).equals("error") done() }) }) o("rejects on error in extract", function(done) { mock.$defineRoutes({ "GET /item": function() { return {status: 200, responseText: JSON.stringify({a: 1})} } }) request({ method: "GET", url: "/item", extract: function() {throw new Error("error")} }).catch(function(e) { o(e instanceof Error).equals(true) o(e.message).equals("error") }).then(function() { done() }) }) }) o.spec("json header", function() { function checkUnset(method) { o("doesn't set header on " + method + " without body", function(done) { var routes = {} routes[method + " /item"] = function() { return {status: 200, responseText: JSON.stringify({a: 1})} } mock.$defineRoutes(routes) request({ method: method, url: "/item", config: function(xhr) { var header = xhr.getRequestHeader("Content-Type") o(header).equals(undefined) header = xhr.getRequestHeader("Accept") o(header).equals("application/json, text/*") } }).then(function(result) { o(result).deepEquals({a: 1}) done() }).catch(function(e) { done(e) }) }) } function checkSet(method, body) { o("sets header on " + method + " with body", function(done) { var routes = {} routes[method + " /item"] = function(response) { return { status: 200, responseText: JSON.stringify({body: JSON.parse(response.body)}), } } mock.$defineRoutes(routes) request({ method: method, url: "/item", body: body, config: function(xhr) { var header = xhr.getRequestHeader("Content-Type") o(header).equals("application/json; charset=utf-8") header = xhr.getRequestHeader("Accept") o(header).equals("application/json, text/*") } }).then(function(result) { o(result).deepEquals({body: body}) done() }).catch(function(e) { done(e) }) }) } checkUnset("GET") checkUnset("HEAD") checkUnset("OPTIONS") checkUnset("POST") checkUnset("PUT") checkUnset("DELETE") checkUnset("PATCH") checkSet("GET", {foo: "bar"}) checkSet("HEAD", {foo: "bar"}) checkSet("OPTIONS", {foo: "bar"}) checkSet("POST", {foo: "bar"}) checkSet("PUT", {foo: "bar"}) checkSet("DELETE", {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 () { // Use the native promise for correct semantics. This test will fail // if you use the polyfill, as it's based on `setImmediate` (falling // back to `setTimeout`), and promise microtasks are run at higher // priority than either of those. request = Request(mock, Promise, complete).request 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. } })