From e21c89f49679722d48df814497a1c88fd1dca787 Mon Sep 17 00:00:00 2001 From: Claudia Meadows Date: Wed, 30 Oct 2024 21:56:14 -0700 Subject: [PATCH] Bring back `m.request` - it's too useful. It mostly delegates to `fetch`. Also, kill `m.withProgress` as that only existed because there was no `m.request`. --- src/entry/mithril.esm.js | 4 +- src/std/fetch.js | 71 ++ src/std/with-progress.js | 28 - tests/std/fetch.js | 2007 +++++++++++++++++++++++++++++++++++++ tests/std/withProgress.js | 67 -- 5 files changed, 2080 insertions(+), 97 deletions(-) create mode 100644 src/std/fetch.js delete mode 100644 src/std/with-progress.js create mode 100644 tests/std/fetch.js delete mode 100644 tests/std/withProgress.js diff --git a/src/entry/mithril.esm.js b/src/entry/mithril.esm.js index a45d2ad6e..b51937f03 100644 --- a/src/entry/mithril.esm.js +++ b/src/entry/mithril.esm.js @@ -4,16 +4,16 @@ import {debouncer, throttler} from "../std/rate-limit.js" import {link, route} from "../std/router.js" import {match, p, query} from "../std/path-query.js" import {tracked, trackedList} from "../std/tracked.js" +import fetch from "../std/fetch.js" import init from "../std/init.js" import lazy from "../std/lazy.js" -import withProgress from "../std/with-progress.js" m.route = route m.link = link m.p = p m.query = query m.match = match -m.withProgress = withProgress +m.fetch = fetch m.lazy = lazy m.init = init m.tracked = tracked diff --git a/src/std/fetch.js b/src/std/fetch.js new file mode 100644 index 000000000..a9ac8f0f1 --- /dev/null +++ b/src/std/fetch.js @@ -0,0 +1,71 @@ +/* global fetch */ + +import {checkCallback} from "../util.js" + +var mfetch = async (url, opts = {}) => { + checkCallback(opts.onprogress, true, "opts.onprogress") + checkCallback(opts.extract, true, "opts.extract") + + try { + var response = await fetch(url, opts) + + if (opts.onprogress && response.body) { + var reader = response.body.getReader() + var rawLength = response.headers.get("content-length") || "" + // This is explicit coercion, but ESLint is frequently too dumb to detect it correctly. + // Another example: https://github.com/eslint/eslint/issues/14623 + // eslint-disable-next-line no-implicit-coercion + var total = (/^\d+$/).test(rawLength) ? +rawLength : -1 + var current = 0 + + response = new Response(new ReadableStream({ + type: "bytes", + start: (ctrl) => reader || ctrl.close(), + cancel: (reason) => reader.cancel(reason), + async pull(ctrl) { + var result = await reader.read() + if (result.done) { + ctrl.close() + } else { + current += result.value.length + ctrl.enqueue(result.value) + opts.onprogress(current, total) + } + }, + }), response) + } + + if (response.ok) { + if (opts.extract) { + return await opts.extract(response) + } + + switch (opts.responseType || "json") { + case "json": return await response.json() + case "formdata": return await response.formData() + case "arraybuffer": return await response.arrayBuffer() + case "blob": return await response.blob() + case "text": return await response.text() + case "document": + // eslint-disable-next-line no-undef + return new DOMParser() + .parseFromString(await response.text(), response.headers.get("content-type") || "text/html") + default: + throw new TypeError(`Unknown response type: ${opts.responseType}`) + } + } + + var message = (await response.text()) || response.statusText + } catch (e) { + var cause = e + var message = e.message + } + + var e = new Error(message) + e.status = response ? response.status : 0 + e.response = response + e.cause = cause + throw e +} + +export {mfetch as default} diff --git a/src/std/with-progress.js b/src/std/with-progress.js deleted file mode 100644 index 37561b18f..000000000 --- a/src/std/with-progress.js +++ /dev/null @@ -1,28 +0,0 @@ -import {checkCallback} from "../util.js" - -/** - * @param {ReadableStream | null} source - * @param {(current: number) => void} notify - */ -export default (source, notify) => { - checkCallback(notify, false, "notify") - - var reader = source && source.getReader() - var current = 0 - - return new ReadableStream({ - type: "bytes", - start: (ctrl) => reader || ctrl.close(), - cancel: (reason) => reader.cancel(reason), - async pull(ctrl) { - var result = await reader.read() - if (result.done) { - ctrl.close() - } else { - current += result.value.length - ctrl.enqueue(result.value) - notify(current) - } - }, - }) -} diff --git a/tests/std/fetch.js b/tests/std/fetch.js new file mode 100644 index 000000000..dbcc1d45f --- /dev/null +++ b/tests/std/fetch.js @@ -0,0 +1,2007 @@ +/* global FormData */ + +// This alone amounts to over 200k assertions total, but that's because it almost fully +// exhaustively tests the function. (Turns out it's not all that hard.) The function's pretty +// simple, so it doesn't take as long as you'd think. + +import o from "ospec" + +import m from "../../src/entry/mithril.esm.js" +import {setupGlobals} from "../../test-utils/global.js" + +o.spec("fetch", () => { + let global, oldFetch + setupGlobals({ + initialize(g) { + global = g + oldFetch = g.fetch + }, + cleanup(g) { + global = null + g.fetch = oldFetch + }, + }) + + const methods = [ + "HEAD", + "GET", + "PATCH", + "POST", + "PUT", + "DELETE", + ] + + const okStatuses = { + 200: "OK", + 201: "Created", + 202: "Accepted", + 203: "Non-authoritative Information", + 206: "Partial Content", + 207: "Multi-Status", + 208: "Already Reported", + 226: "IM Used", + } + + const emptyStatuses = { + 204: "No Content", + 205: "Reset Content", + } + + const emptyErrorStatuses = { + // 1xx statuses aren't supported: https://github.com/whatwg/fetch/issues/1759 + // It's likely that in the future, 101 may be supported, but not 103. + // 101: "Switching Protocols", + // 103: "Early Hints", + 304: "Not Modified", + } + + const errorStatuses = { + // 1xx statuses aren't supported: https://github.com/whatwg/fetch/issues/1759 + // 100: "Continue", + // 102: "Processing", + 300: "Multiple Choices", + 301: "Moved Permanently", + 302: "Found", + 303: "See Other", + 305: "Use Proxy", + 307: "Temporary Redirect", + 308: "Permanent Redirect", + 400: "Bad Request", + 401: "Unauthorized", + 402: "Payment Required", + 403: "Forbidden", + 404: "Not Found", + 405: "Method Not Allowed", + 406: "Not Acceptable", + 407: "Proxy Authentication Required", + 408: "Request Timeout", + 409: "Conflict", + 410: "Gone", + 411: "Length Required", + 412: "Precondition Failed", + 413: "Payload Too Large", + 414: "Request-URI Too Long", + 415: "Unsupported Media Type", + 416: "Requested Range Not Satisfiable", + 417: "Expectation Failed", + 418: "I'm a teapot", + 421: "Misdirected Request", + 422: "Unprocessable Entity", + 423: "Locked", + 424: "Failed Dependency", + 425: "Too Early", + 426: "Upgrade Required", + 428: "Precondition Required", + 429: "Too Many Requests", + 431: "Request Header Fields Too Large", + 444: "Connection Closed Without Response", + 451: "Unavailable For Legal Reasons", + 499: "Client Closed Request", + 500: "Internal Server Error", + 501: "Not Implemented", + 502: "Bad Gateway", + 503: "Service Unavailable", + 504: "Gateway Timeout", + 505: "HTTP Version Not Supported", + 506: "Variant Also Negotiates", + 507: "Insufficient Storage", + 508: "Loop Detected", + 510: "Not Extended", + 511: "Network Authentication Required", + 599: "Network Connect Timeout Error", + } + + const allStatuses = {...okStatuses, ...emptyStatuses, ...emptyErrorStatuses, ...errorStatuses} + + const allResponseTypes = ["json", "formdata", "arraybuffer", "blob", "text", "document"] + + /** + * @param {object} options + * @param {number} options.status + * @param {string} [options.contentType] + * @param {boolean} [options.contentLength] + * @param {null | Array} options.body + */ + const setupFetch = ({status, headers = {}, contentLength, body}) => { + global.fetch = o.spy(() => { + const encoder = new TextEncoder() + const chunks = body == null ? null : body.map((chunk) => ( + typeof chunk === "string" ? encoder.encode(chunk) : Uint8Array.from(chunk) + )) + if (contentLength) headers["content-length"] = chunks == null ? 0 : chunks.reduce((s, c) => s + c.length, 0) + let i = 0 + return new Response(body == null ? null : new ReadableStream({ + type: "bytes", + pull(ctrl) { + if (i === chunks.length) { + ctrl.close() + } else { + ctrl.enqueue(Uint8Array.from(chunks[i++])) + } + }, + }), {status, statusText: allStatuses[status], headers}) + }) + } + + const bufferToArray = (v) => [...new Uint8Array(v)] + + for (const method of methods) { + for (const status of Object.keys(okStatuses)) { + o.spec(`method ${method}, status ${status}`, () => { + o.spec("arraybuffer, no content length", () => { + o("null body", async () => { + setupFetch({status: Number(status), body: null}) + + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body", async () => { + setupFetch({status: Number(status), body: []}) + + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk", async () => { + setupFetch({status: Number(status), body: [[10]]}) + + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([10]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks", async () => { + setupFetch({status: Number(status), body: [[10], [20]]}) + + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([10, 20]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("null body + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([]) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: []}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([]) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: [[10]]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([10]) + o(reports).deepEquals([[1, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: [[10], [20]]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([10, 20]) + o(reports).deepEquals([[1, -1], [2, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("arraybuffer, has content length", () => { + o("null body", async () => { + setupFetch({status: Number(status), contentLength: true, body: null}) + + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body", async () => { + setupFetch({status: Number(status), contentLength: true, body: []}) + + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10]]}) + + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([10]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10], [20]]}) + + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([10, 20]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("null body + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: null}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([]) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: []}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([]) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10]]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([10]) + o(reports).deepEquals([[1, 1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10], [20]]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([10, 20]) + o(reports).deepEquals([[1, 2], [2, 2]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("text, no content length", () => { + o("null body", async () => { + setupFetch({status: Number(status), body: null}) + + const result = await m.fetch("/url", { + method, + responseType: "text", + }) + + o(result).equals("") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body", async () => { + setupFetch({status: Number(status), body: []}) + + const result = await m.fetch("/url", { + method, + responseType: "text", + }) + + o(result).equals("") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk", async () => { + setupFetch({status: Number(status), body: [[10]]}) + + const result = await m.fetch("/url", { + method, + responseType: "text", + }) + + o(result).equals("\x0A") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks", async () => { + setupFetch({status: Number(status), body: [[10], [20]]}) + + const result = await m.fetch("/url", { + method, + responseType: "text", + }) + + o(result).equals("\x0A\x14") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("null body + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "text", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals("") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: []}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "text", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals("") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: [[10]]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "text", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals("\x0A") + o(reports).deepEquals([[1, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: [[10], [20]]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "text", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals("\x0A\x14") + o(reports).deepEquals([[1, -1], [2, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("text, has content length", () => { + o("null body", async () => { + setupFetch({status: Number(status), contentLength: true, body: null}) + + const result = await m.fetch("/url", { + method, + responseType: "text", + }) + + o(result).equals("") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body", async () => { + setupFetch({status: Number(status), contentLength: true, body: []}) + + const result = await m.fetch("/url", { + method, + responseType: "text", + }) + + o(result).equals("") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10]]}) + + const result = await m.fetch("/url", { + method, + responseType: "text", + }) + + o(result).equals("\x0A") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10], [20]]}) + + const result = await m.fetch("/url", { + method, + responseType: "text", + }) + + o(result).equals("\x0A\x14") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("null body + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: null}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "text", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals("") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: []}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "text", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals("") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10]]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "text", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals("\x0A") + o(reports).deepEquals([[1, 1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10], [20]]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "text", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals("\x0A\x14") + o(reports).deepEquals([[1, 2], [2, 2]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("blob, no content length", () => { + o("null body", async () => { + setupFetch({status: Number(status), body: null}) + + const blob = await m.fetch("/url", { + method, + responseType: "blob", + }) + const result = await blob.text() + + o(result).equals("") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body", async () => { + setupFetch({status: Number(status), body: []}) + + const blob = await m.fetch("/url", { + method, + responseType: "blob", + }) + const result = await blob.text() + + o(result).equals("") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk", async () => { + setupFetch({status: Number(status), body: [[10]]}) + + const blob = await m.fetch("/url", { + method, + responseType: "blob", + }) + const result = await blob.text() + + o(result).equals("\x0A") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks", async () => { + setupFetch({status: Number(status), body: [[10], [20]]}) + + const blob = await m.fetch("/url", { + method, + responseType: "blob", + }) + const result = await blob.text() + + o(result).equals("\x0A\x14") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("null body + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const reports = [] + const blob = await m.fetch("/url", { + method, + responseType: "blob", + onprogress: (current, total) => reports.push([current, total]), + }) + const result = await blob.text() + + o(result).equals("") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: []}) + + const reports = [] + const blob = await m.fetch("/url", { + method, + responseType: "blob", + onprogress: (current, total) => reports.push([current, total]), + }) + const result = await blob.text() + + o(result).equals("") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: [[10]]}) + + const reports = [] + const blob = await m.fetch("/url", { + method, + responseType: "blob", + onprogress: (current, total) => reports.push([current, total]), + }) + const result = await blob.text() + + o(result).equals("\x0A") + o(reports).deepEquals([[1, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: [[10], [20]]}) + + const reports = [] + const blob = await m.fetch("/url", { + method, + responseType: "blob", + onprogress: (current, total) => reports.push([current, total]), + }) + const result = await blob.text() + + o(result).equals("\x0A\x14") + o(reports).deepEquals([[1, -1], [2, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("blob, has content length", () => { + o("null body", async () => { + setupFetch({status: Number(status), contentLength: true, body: null}) + + const blob = await m.fetch("/url", { + method, + responseType: "blob", + }) + const result = await blob.text() + + o(result).equals("") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body", async () => { + setupFetch({status: Number(status), contentLength: true, body: []}) + + const blob = await m.fetch("/url", { + method, + responseType: "blob", + }) + const result = await blob.text() + + o(result).equals("") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10]]}) + + const blob = await m.fetch("/url", { + method, + responseType: "blob", + }) + const result = await blob.text() + + o(result).equals("\x0A") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10], [20]]}) + + const blob = await m.fetch("/url", { + method, + responseType: "blob", + }) + const result = await blob.text() + + o(result).equals("\x0A\x14") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("null body + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: null}) + + const reports = [] + const blob = await m.fetch("/url", { + method, + responseType: "blob", + onprogress: (current, total) => reports.push([current, total]), + }) + const result = await blob.text() + + o(result).equals("") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: []}) + + const reports = [] + const blob = await m.fetch("/url", { + method, + responseType: "blob", + onprogress: (current, total) => reports.push([current, total]), + }) + const result = await blob.text() + + o(result).equals("") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10]]}) + + const reports = [] + const blob = await m.fetch("/url", { + method, + responseType: "blob", + onprogress: (current, total) => reports.push([current, total]), + }) + const result = await blob.text() + + o(result).equals("\x0A") + o(reports).deepEquals([[1, 1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10], [20]]}) + + const reports = [] + const blob = await m.fetch("/url", { + method, + responseType: "blob", + onprogress: (current, total) => reports.push([current, total]), + }) + const result = await blob.text() + + o(result).equals("\x0A\x14") + o(reports).deepEquals([[1, 2], [2, 2]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("json, no content length", () => { + o("null body", async () => { + setupFetch({status: Number(status), body: null}) + + let error + try { + await m.fetch("/url", { + method, + responseType: "json", + }) + } catch (e) { + error = e + } + + o(error).notEquals(undefined) + o(error.cause).notEquals(undefined) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body", async () => { + setupFetch({status: Number(status), body: []}) + + let error + try { + await m.fetch("/url", { + method, + responseType: "json", + }) + } catch (e) { + error = e + } + + o(error).notEquals(undefined) + o(error.cause).notEquals(undefined) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk", async () => { + setupFetch({status: Number(status), body: ["123"]}) + + const result = await m.fetch("/url", { + method, + responseType: "json", + }) + + o(result).equals(123) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks", async () => { + setupFetch({status: Number(status), body: ["123", "456"]}) + + const result = await m.fetch("/url", { + method, + responseType: "json", + }) + + o(result).equals(123456) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("null body + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const reports = [] + + let error + try { + await m.fetch("/url", { + method, + responseType: "json", + }) + } catch (e) { + error = e + } + + o(error).notEquals(undefined) + o(error.cause).notEquals(undefined) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: []}) + + const reports = [] + + let error + try { + await m.fetch("/url", { + method, + responseType: "json", + }) + } catch (e) { + error = e + } + + o(error).notEquals(undefined) + o(error.cause).notEquals(undefined) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: ["123"]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "json", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals(123) + o(reports).deepEquals([[3, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: ["123", "456"]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "json", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals(123456) + o(reports).deepEquals([[3, -1], [6, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("json, has content length", () => { + o("null body", async () => { + setupFetch({status: Number(status), contentLength: true, body: null}) + + let error + try { + await m.fetch("/url", { + method, + responseType: "json", + }) + } catch (e) { + error = e + } + + o(error).notEquals(undefined) + o(error.cause).notEquals(undefined) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body", async () => { + setupFetch({status: Number(status), contentLength: true, body: []}) + + let error + try { + await m.fetch("/url", { + method, + responseType: "json", + }) + } catch (e) { + error = e + } + + o(error).notEquals(undefined) + o(error.cause).notEquals(undefined) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk", async () => { + setupFetch({status: Number(status), contentLength: true, body: ["123"]}) + + const result = await m.fetch("/url", { + method, + responseType: "json", + }) + + o(result).equals(123) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks", async () => { + setupFetch({status: Number(status), contentLength: true, body: ["123", "456"]}) + + const result = await m.fetch("/url", { + method, + responseType: "json", + }) + + o(result).equals(123456) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("null body + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: null}) + + const reports = [] + + let error + try { + await m.fetch("/url", { + method, + responseType: "json", + }) + } catch (e) { + error = e + } + + o(error).notEquals(undefined) + o(error.cause).notEquals(undefined) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: []}) + + const reports = [] + + let error + try { + await m.fetch("/url", { + method, + responseType: "json", + }) + } catch (e) { + error = e + } + + o(error).notEquals(undefined) + o(error.cause).notEquals(undefined) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: ["123"]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "json", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals(123) + o(reports).deepEquals([[3, 3]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: ["123", "456"]}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "json", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals(123456) + o(reports).deepEquals([[3, 6], [6, 6]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + if (typeof FormData === "function") { + o.spec("form data", () => { + o("works", async () => { + setupFetch({ + status: Number(status), + headers: { + "content-type": "multipart/form-data; boundary=123456", + }, + contentLength: true, + body: [ + "--123456\r\n", + "Content-Disposition: form-data; name=\"test\"\r\n", + "\r\n", + "value\r\n", + "--123456--\r\n", + ], + }) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "formdata", + onprogress: (current, total) => reports.push([current, total]), + }) + + o([...result]).deepEquals([ + ["test", "value"], + ]) + o(reports).deepEquals([ + [10, 76], + [55, 76], + [57, 76], + [64, 76], + [76, 76], + ]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + } + + if (typeof DOMParser === "function") { + o.spec("document", () => { + o("works without content type", async () => { + setupFetch({ + status: Number(status), + body: ["
"], + }) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "formdata", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result.getElementById("foo")).notEquals(null) + o(reports).deepEquals([[33, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("works with content type text/html", async () => { + setupFetch({ + status: Number(status), + headers: { + "content-type": "text/html", + }, + body: ["
"], + }) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "formdata", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result.getElementById("foo")).notEquals(null) + o(reports).deepEquals([[33, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("works with content type application/xhtml+xml", async () => { + setupFetch({ + status: Number(status), + headers: { + "content-type": "application/xhtml+xml", + }, + body: ['test
'], + }) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "formdata", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result.getElementById("foo")).notEquals(null) + o(reports).deepEquals([[33, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("works with content type application/xml", async () => { + setupFetch({ + status: Number(status), + headers: { + "content-type": "application/xml", + }, + body: ['test
'], + }) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "formdata", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result.getElementById("foo")).notEquals(null) + o(reports).deepEquals([[33, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("works with content type text/xml", async () => { + setupFetch({ + status: Number(status), + headers: { + "content-type": "text/xml", + }, + body: ['test
'], + }) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "formdata", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result.getElementById("foo")).notEquals(null) + o(reports).deepEquals([[33, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("works with content type image/svg+xml", async () => { + setupFetch({ + status: Number(status), + headers: { + "content-type": "image/svg+xml", + }, + body: [''], + }) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "formdata", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result.getElementById("foo")).notEquals(null) + o(reports).deepEquals([[33, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + } + + o.spec("custom extract", () => { + o("works", async () => { + setupFetch({ + status: Number(status), + body: ["123"], + }) + + const reports = [] + const result = await m.fetch("/url", { + method, + onprogress: (current, total) => reports.push([current, total]), + extract: async (response) => `${await response.text()}456`, + }) + + o(result).equals("123456") + o(reports).deepEquals([ + [3, -1], + ]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + }) + } + + for (const status of Object.keys(emptyStatuses)) { + o.spec(`method ${method}, status ${status}`, () => { + o.spec("arraybuffer", () => { + o("no `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("with `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "arraybuffer", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result instanceof ArrayBuffer).equals(true) + o(bufferToArray(result)).deepEquals([]) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("text", () => { + o("no `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const result = await m.fetch("/url", { + method, + responseType: "text", + }) + + o(result).equals("") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("with `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const reports = [] + const result = await m.fetch("/url", { + method, + responseType: "text", + onprogress: (current, total) => reports.push([current, total]), + }) + + o(result).equals("") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("blob", () => { + o("no `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const blob = await m.fetch("/url", { + method, + responseType: "blob", + }) + const result = await blob.text() + + o(result).equals("") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("with `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const reports = [] + const blob = await m.fetch("/url", { + method, + responseType: "blob", + onprogress: (current, total) => reports.push([current, total]), + }) + const result = await blob.text() + + o(result).equals("") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("json", () => { + o("no `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + let error + try { + await m.fetch("/url", { + method, + responseType: "json", + }) + } catch (e) { + error = e + } + + o(error).notEquals(undefined) + o(error.cause).notEquals(undefined) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("with `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const reports = [] + + let error + try { + await m.fetch("/url", { + method, + responseType: "json", + }) + } catch (e) { + error = e + } + + o(error).notEquals(undefined) + o(error.cause).notEquals(undefined) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec("custom extract", () => { + o("works", async () => { + setupFetch({ + status: Number(status), + body: null, + }) + + const reports = [] + const result = await m.fetch("/url", { + method, + onprogress: (current, total) => reports.push([current, total]), + extract: async (response) => `${await response.text()}456`, + }) + + o(result).equals("456") + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + }) + } + + for (const status of Object.keys(emptyErrorStatuses)) { + o.spec(`method ${method}, status ${status}`, () => { + for (const responseType of allResponseTypes) { + o.spec(responseType, () => { + o("no `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + let error + try { + await m.fetch("/url", { + method, + responseType, + }) + } catch (e) { + error = e + } + + o(error.message).equals(emptyErrorStatuses[status]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("with `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const reports = [] + let error + try { + await m.fetch("/url", { + method, + responseType, + onprogress: (current, total) => reports.push([current, total]), + }) + } catch (e) { + error = e + } + + o(error.message).equals(emptyErrorStatuses[status]) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + } + }) + } + + for (const status of Object.keys(errorStatuses)) { + o.spec(`method ${method}, status ${status}`, () => { + for (const responseType of allResponseTypes) { + o.spec(`${responseType}, no content length`, () => { + o("null body", async () => { + setupFetch({status: Number(status), body: null}) + + let error + try { + await m.fetch("/url", { + method, + responseType, + }) + } catch (e) { + error = e + } + + o(error.message).equals(errorStatuses[status]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body", async () => { + setupFetch({status: Number(status), body: []}) + + let error + try { + await m.fetch("/url", { + method, + responseType, + }) + } catch (e) { + error = e + } + + o(error.message).equals(errorStatuses[status]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk", async () => { + setupFetch({status: Number(status), body: [[10]]}) + + let error + try { + await m.fetch("/url", { + method, + responseType, + }) + } catch (e) { + error = e + } + + o(error.message).equals("\x0A") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks", async () => { + setupFetch({status: Number(status), body: [[10], [20]]}) + + let error + try { + await m.fetch("/url", { + method, + responseType, + }) + } catch (e) { + error = e + } + + o(error.message).equals("\x0A\x14") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("null body + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: null}) + + const reports = [] + let error + try { + await m.fetch("/url", { + method, + responseType, + onprogress: (current, total) => reports.push([current, total]), + }) + } catch (e) { + error = e + } + + o(error.message).equals(errorStatuses[status]) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: []}) + + const reports = [] + let error + try { + await m.fetch("/url", { + method, + responseType, + onprogress: (current, total) => reports.push([current, total]), + }) + } catch (e) { + error = e + } + + o(error.message).equals(errorStatuses[status]) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: [[10]]}) + + const reports = [] + let error + try { + await m.fetch("/url", { + method, + responseType, + onprogress: (current, total) => reports.push([current, total]), + }) + } catch (e) { + error = e + } + + o(error.message).equals("\x0A") + o(reports).deepEquals([[1, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks + `onprogress` listener", async () => { + setupFetch({status: Number(status), body: [[10], [20]]}) + + const reports = [] + let error + try { + await m.fetch("/url", { + method, + responseType, + onprogress: (current, total) => reports.push([current, total]), + }) + } catch (e) { + error = e + } + + o(error.message).equals("\x0A\x14") + o(reports).deepEquals([[1, -1], [2, -1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + + o.spec(`${responseType}, has content length`, () => { + o("null body", async () => { + setupFetch({status: Number(status), contentLength: true, body: null}) + + let error + try { + await m.fetch("/url", { + method, + responseType, + }) + } catch (e) { + error = e + } + + o(error.message).equals(errorStatuses[status]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body", async () => { + setupFetch({status: Number(status), contentLength: true, body: []}) + + let error + try { + await m.fetch("/url", { + method, + responseType, + }) + } catch (e) { + error = e + } + + o(error.message).equals(errorStatuses[status]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10]]}) + + let error + try { + await m.fetch("/url", { + method, + responseType, + }) + } catch (e) { + error = e + } + + o(error.message).equals("\x0A") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10], [20]]}) + + let error + try { + await m.fetch("/url", { + method, + responseType, + }) + } catch (e) { + error = e + } + + o(error.message).equals("\x0A\x14") + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("null body + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: null}) + + const reports = [] + let error + try { + await m.fetch("/url", { + method, + responseType, + onprogress: (current, total) => reports.push([current, total]), + }) + } catch (e) { + error = e + } + + o(error.message).equals(errorStatuses[status]) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("empty body + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: []}) + + const reports = [] + let error + try { + await m.fetch("/url", { + method, + responseType, + onprogress: (current, total) => reports.push([current, total]), + }) + } catch (e) { + error = e + } + + o(error.message).equals(errorStatuses[status]) + o(reports).deepEquals([]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("single non-empty chunk + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10]]}) + + const reports = [] + let error + try { + await m.fetch("/url", { + method, + responseType, + onprogress: (current, total) => reports.push([current, total]), + }) + } catch (e) { + error = e + } + + o(error.message).equals("\x0A") + o(reports).deepEquals([[1, 1]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + + o("two non-empty chunks + `onprogress` listener", async () => { + setupFetch({status: Number(status), contentLength: true, body: [[10], [20]]}) + + const reports = [] + let error + try { + await m.fetch("/url", { + method, + responseType, + onprogress: (current, total) => reports.push([current, total]), + }) + } catch (e) { + error = e + } + + o(error.message).equals("\x0A\x14") + o(reports).deepEquals([[1, 2], [2, 2]]) + o(global.fetch.callCount).equals(1) + o(global.fetch.args[0]).equals("/url") + o(global.fetch.args[1].method).equals(method) + }) + }) + } + }) + } + } +}) diff --git a/tests/std/withProgress.js b/tests/std/withProgress.js deleted file mode 100644 index e9062a3a8..000000000 --- a/tests/std/withProgress.js +++ /dev/null @@ -1,67 +0,0 @@ -import o from "ospec" - -import withProgress from "../../src/std/with-progress.js" - -if (typeof ReadableStream === "function") { - o.spec("withProgress", () => { - function sequence(chunks) { - let i = 0 - return new ReadableStream({ - type: "bytes", - pull(ctrl) { - if (i === chunks.length) { - ctrl.close() - } else { - ctrl.enqueue(Uint8Array.from(chunks[i++])) - } - }, - }) - } - - function drain(stream) { - return new Response(stream).arrayBuffer().then((buf) => [...new Uint8Array(buf)]) - } - - o("handles null body", () => { - var reports = [] - var watched = withProgress(null, (current) => reports.push(current)) - - return drain(watched).then((result) => { - o(result).deepEquals([]) - o(reports).deepEquals([]) - }) - }) - - o("handles empty body", () => { - var reports = [] - var watched = withProgress(sequence([]), (current) => reports.push(current)) - - return drain(watched).then((result) => { - o(result).deepEquals([]) - o(reports).deepEquals([]) - }) - }) - - o("adds single non-empty chunk", () => { - var reports = [] - var watched = withProgress(sequence([[10]]), (current) => reports.push(current)) - - return drain(watched).then((result) => { - o(result).deepEquals([10]) - o(reports).deepEquals([1]) - }) - }) - - o("adds multiple non-empty chunks", () => { - var reports = [] - var watched = withProgress(sequence([[10], [20]]), (current) => reports.push(current)) - - return drain(watched).then((result) => { - o(result).deepEquals([10, 20]) - o(reports).deepEquals([1, 2]) - }) - }) - }) -} else { - console.log("Skipping `withProgress` as `ReadableStream` is missing.") -}