diff --git a/changelog.md b/changelog.md index 4864635..b2a0450 100644 --- a/changelog.md +++ b/changelog.md @@ -21,6 +21,7 @@ - “Plain” objects in the GraphQL operation that aren’t `Object` instances (e.g. `Object.create(null)`) are now also deep cloned when searching for extractable files. - Updated dev dependencies, some of which require newer Node.js versions than previously supported. +- Refactored tests to use the standard `AbortController`, `AbortSignal`, `File`, `FormData`, and `Response` APIs available in modern Node.js and removed the dev dependencies [`abort-controller`](https://npm.im/abort-controller), [`formdata-node`](https://npm.im/formdata-node), and [`node-fetch`](https://npm.im/node-fetch). - Public modules are now individually listed in the package `files` and `exports` fields. - Removed the package main index module; deep imports must be used. To migrate: diff --git a/createUploadLink.test.mjs b/createUploadLink.test.mjs index 122108c..74d5259 100644 --- a/createUploadLink.test.mjs +++ b/createUploadLink.test.mjs @@ -1,11 +1,10 @@ +import "./test/polyfillFile.mjs"; + import { ApolloLink } from "@apollo/client/link/core/ApolloLink.js"; import { concat } from "@apollo/client/link/core/concat.js"; import { execute } from "@apollo/client/link/core/execute.js"; -import { AbortController, AbortSignal } from "abort-controller"; import { deepEqual, deepStrictEqual, strictEqual } from "assert"; -import { File, FormData } from "formdata-node"; import gql from "graphql-tag"; -import { AbortError, Response } from "node-fetch"; import revertableGlobals from "revertable-globals"; import createUploadLink from "./createUploadLink.mjs"; @@ -71,10 +70,7 @@ export default (tests) => { const { signal: fetchOptionsSignal, ...fetchOptionsRest } = fetchOptions; - // Defined in Node.js v15+. - if (global.AbortSignal) - strictEqual(fetchOptionsSignal instanceof global.AbortSignal, true); - + strictEqual(fetchOptionsSignal instanceof AbortSignal, true); deepEqual(fetchOptionsRest, { method: "POST", headers: { accept: "*/*", "content-type": "application/json" }, @@ -99,8 +95,6 @@ export default (tests) => { const fileName = "a.txt"; const fileType = "text/plain"; const revertGlobals = revertableGlobals({ - File, - FormData, async fetch(uri, options) { fetchUri = uri; fetchOptions = options; @@ -140,10 +134,7 @@ export default (tests) => { ...fetchOptionsRest } = fetchOptions; - // Defined in Node.js v15+. - if (global.AbortSignal) - strictEqual(fetchOptionsSignal instanceof global.AbortSignal, true); - + strictEqual(fetchOptionsSignal instanceof AbortSignal, true); strictEqual(fetchOptionsBody instanceof FormData, true); const formDataEntries = Array.from(fetchOptionsBody.entries()); @@ -218,10 +209,7 @@ export default (tests) => { const { signal: fetchOptionsSignal, ...fetchOptionsRest } = fetchOptions; - // Defined in Node.js v15+. - if (global.AbortSignal) - strictEqual(fetchOptionsSignal instanceof global.AbortSignal, true); - + strictEqual(fetchOptionsSignal instanceof AbortSignal, true); deepEqual(fetchOptionsRest, { method: "POST", headers: { accept: "*/*", "content-type": "application/json" }, @@ -280,10 +268,7 @@ export default (tests) => { const { signal: fetchOptionsSignal, ...fetchOptionsRest } = fetchOptions; - // Defined in Node.js v15+. - if (global.AbortSignal) - strictEqual(fetchOptionsSignal instanceof global.AbortSignal, true); - + strictEqual(fetchOptionsSignal instanceof AbortSignal, true); deepEqual(fetchOptionsRest, { method: "POST", headers: { accept: "*/*", "content-type": "application/json" }, @@ -347,10 +332,7 @@ export default (tests) => { const { signal: fetchOptionsSignal, ...fetchOptionsRest } = fetchOptions; - // Defined in Node.js v15+. - if (global.AbortSignal) - strictEqual(fetchOptionsSignal instanceof global.AbortSignal, true); - + strictEqual(fetchOptionsSignal instanceof AbortSignal, true); deepEqual(fetchOptionsRest, { method: "GET", headers: { accept: "*/*", "content-type": "application/json" }, @@ -408,10 +390,7 @@ export default (tests) => { const { signal: fetchOptionsSignal, ...fetchOptionsRest } = fetchOptions; - // Defined in Node.js v15+. - if (global.AbortSignal) - strictEqual(fetchOptionsSignal instanceof global.AbortSignal, true); - + strictEqual(fetchOptionsSignal instanceof AbortSignal, true); deepEqual(fetchOptionsRest, { method: "GET", headers: { accept: "*/*", "content-type": "application/json" }, @@ -431,84 +410,76 @@ export default (tests) => { const payload = { data: { a: true } }; const fileName = "a.txt"; const fileType = "text/plain"; - const revertGlobals = revertableGlobals({ File }); - try { - await timeLimitPromise( - new Promise((resolve, reject) => { - execute( - createUploadLink({ - useGETForQueries: true, - FormData, - async fetch(uri, options) { - fetchUri = uri; - fetchOptions = options; + await timeLimitPromise( + new Promise((resolve, reject) => { + execute( + createUploadLink({ + useGETForQueries: true, + FormData, + async fetch(uri, options) { + fetchUri = uri; + fetchOptions = options; - return new Response( - JSON.stringify(payload), - graphqlResponseOptions, - ); - }, - }), - { - query: gql(query), - variables: { - a: new File(["a"], fileName, { type: fileType }), - }, - }, - ).subscribe({ - next(data) { - nextData = data; - }, - error() { - reject(createUnexpectedCallError()); + return new Response( + JSON.stringify(payload), + graphqlResponseOptions, + ); }, - complete() { - resolve(); + }), + { + query: gql(query), + variables: { + a: new File(["a"], fileName, { type: fileType }), }, - }); - }), - ); - - strictEqual(fetchUri, defaultUri); - strictEqual(typeof fetchOptions, "object"); + }, + ).subscribe({ + next(data) { + nextData = data; + }, + error() { + reject(createUnexpectedCallError()); + }, + complete() { + resolve(); + }, + }); + }), + ); - const { - signal: fetchOptionsSignal, - body: fetchOptionsBody, - ...fetchOptionsRest - } = fetchOptions; + strictEqual(fetchUri, defaultUri); + strictEqual(typeof fetchOptions, "object"); - // Defined in Node.js v15+. - if (global.AbortSignal) - strictEqual(fetchOptionsSignal instanceof global.AbortSignal, true); + const { + signal: fetchOptionsSignal, + body: fetchOptionsBody, + ...fetchOptionsRest + } = fetchOptions; - strictEqual(fetchOptionsBody instanceof FormData, true); + strictEqual(fetchOptionsSignal instanceof AbortSignal, true); + strictEqual(fetchOptionsBody instanceof FormData, true); - const formDataEntries = Array.from(fetchOptionsBody.entries()); + const formDataEntries = Array.from(fetchOptionsBody.entries()); - strictEqual(formDataEntries.length, 3); - strictEqual(formDataEntries[0][0], "operations"); - deepStrictEqual(JSON.parse(formDataEntries[0][1]), { - query, - variables: { a: null }, - }); - strictEqual(formDataEntries[1][0], "map"); - deepStrictEqual(JSON.parse(formDataEntries[1][1]), { - 1: ["variables.a"], - }); - strictEqual(formDataEntries[2][0], "1"); - strictEqual(formDataEntries[2][1] instanceof File, true); - strictEqual(formDataEntries[2][1].name, fileName); - strictEqual(formDataEntries[2][1].type, fileType); - deepEqual(fetchOptionsRest, { - method: "POST", - headers: { accept: "*/*" }, - }); - deepStrictEqual(nextData, payload); - } finally { - revertGlobals(); - } + strictEqual(formDataEntries.length, 3); + strictEqual(formDataEntries[0][0], "operations"); + deepStrictEqual(JSON.parse(formDataEntries[0][1]), { + query, + variables: { a: null }, + }); + strictEqual(formDataEntries[1][0], "map"); + deepStrictEqual(JSON.parse(formDataEntries[1][1]), { + 1: ["variables.a"], + }); + strictEqual(formDataEntries[2][0], "1"); + strictEqual(formDataEntries[2][1] instanceof File, true); + strictEqual(formDataEntries[2][1].name, fileName); + strictEqual(formDataEntries[2][1].type, fileType); + deepEqual(fetchOptionsRest, { + method: "POST", + headers: { accept: "*/*" }, + }); + deepStrictEqual(nextData, payload); }, ); @@ -613,10 +584,7 @@ export default (tests) => { const { signal: fetchOptionsSignal, ...fetchOptionsRest } = fetchOptions; - // Defined in Node.js v15+. - if (global.AbortSignal) - strictEqual(fetchOptionsSignal instanceof global.AbortSignal, true); - + strictEqual(fetchOptionsSignal instanceof AbortSignal, true); deepEqual(fetchOptionsRest, { method: "POST", headers: { accept: "*/*", "content-type": "application/json" }, @@ -676,10 +644,7 @@ export default (tests) => { const { signal: fetchOptionsSignal, ...fetchOptionsRest } = fetchOptions; - // Defined in Node.js v15+. - if (global.AbortSignal) - strictEqual(fetchOptionsSignal instanceof global.AbortSignal, true); - + strictEqual(fetchOptionsSignal instanceof AbortSignal, true); deepEqual(fetchOptionsRest, { method: "POST", headers: { @@ -753,10 +718,7 @@ export default (tests) => { const { signal: fetchOptionsSignal, ...fetchOptionsRest } = fetchOptions; - // Defined in Node.js v15+. - if (global.AbortSignal) - strictEqual(fetchOptionsSignal instanceof global.AbortSignal, true); - + strictEqual(fetchOptionsSignal instanceof AbortSignal, true); deepEqual(fetchOptionsRest, { method: "POST", headers: { @@ -842,10 +804,7 @@ export default (tests) => { ...fetchOptionsRest } = fetchOptions; - // Defined in Node.js v15+. - if (global.AbortSignal) - strictEqual(fetchOptionsSignal instanceof global.AbortSignal, true); - + strictEqual(fetchOptionsSignal instanceof AbortSignal, true); strictEqual(fetchOptionsBody instanceof FormData, true); const formDataEntries = Array.from(fetchOptionsBody.entries()); @@ -997,71 +956,66 @@ export default (tests) => { const query = "{\n a\n}"; const payload = { data: { a: true } }; const controller = new AbortController(); - const fetchError = new AbortError("The operation was aborted."); - const revertGlobals = revertableGlobals({ AbortController, AbortSignal }); + const fetchError = new Error("The operation was aborted."); - try { - const observerErrorPromise = timeLimitPromise( - new Promise((resolve, reject) => { - execute( - createUploadLink({ - fetchOptions: { signal: controller.signal }, - fetch(uri, options) { - fetchUri = uri; - fetchOptions = options; + const observerErrorPromise = timeLimitPromise( + new Promise((resolve, reject) => { + execute( + createUploadLink({ + fetchOptions: { signal: controller.signal }, + fetch(uri, options) { + fetchUri = uri; + fetchOptions = options; - return new Promise((resolve, reject) => { - // Sleep a few seconds to simulate a slow request and - // response. In this test the fetch should be aborted before - // the timeout. - const timeout = setTimeout(() => { - resolve( - new Response( - JSON.stringify(payload), - graphqlResponseOptions, - ), - ); - }, 4000); - - options.signal.addEventListener("abort", () => { - clearTimeout(timeout); - reject(fetchError); - }); + return new Promise((resolve, reject) => { + // Sleep a few seconds to simulate a slow request and + // response. In this test the fetch should be aborted before + // the timeout. + const timeout = setTimeout(() => { + resolve( + new Response( + JSON.stringify(payload), + graphqlResponseOptions, + ), + ); + }, 4000); + + options.signal.addEventListener("abort", () => { + clearTimeout(timeout); + reject(fetchError); }); - }, - }), - { - query: gql(query), - }, - ).subscribe({ - next() { - reject(createUnexpectedCallError()); - }, - error(error) { - resolve(error); - }, - complete() { - reject(createUnexpectedCallError()); + }); }, - }); - }), - ); + }), + { + query: gql(query), + }, + ).subscribe({ + next() { + reject(createUnexpectedCallError()); + }, + error(error) { + resolve(error); + }, + complete() { + reject(createUnexpectedCallError()); + }, + }); + }), + ); - controller.abort(); + controller.abort(); - const observerError = await observerErrorPromise; + const observerError = await observerErrorPromise; - strictEqual(fetchUri, defaultUri); - deepEqual(fetchOptions, { - method: "POST", - headers: { accept: "*/*", "content-type": "application/json" }, - body: JSON.stringify({ variables: {}, query }), - signal: controller.signal, - }); - strictEqual(observerError, fetchError); - } finally { - revertGlobals(); - } + strictEqual(fetchUri, defaultUri); + deepEqual(fetchOptions, { + method: "POST", + headers: { accept: "*/*", "content-type": "application/json" }, + body: JSON.stringify({ variables: {}, query }), + signal: controller.signal, + }); + strictEqual(observerError, fetchError); }, ); @@ -1077,57 +1031,52 @@ export default (tests) => { const controller = new AbortController(); controller.abort(); - const fetchError = new AbortError("The operation was aborted."); - const revertGlobals = revertableGlobals({ AbortController, AbortSignal }); + const fetchError = new Error("The operation was aborted."); - try { - const observerErrorPromise = timeLimitPromise( - new Promise((resolve, reject) => { - execute( - createUploadLink({ - fetchOptions: { signal: controller.signal }, - async fetch(uri, options) { - fetchUri = uri; - fetchOptions = options; + const observerErrorPromise = timeLimitPromise( + new Promise((resolve, reject) => { + execute( + createUploadLink({ + fetchOptions: { signal: controller.signal }, + async fetch(uri, options) { + fetchUri = uri; + fetchOptions = options; - if (options.signal.aborted) throw fetchError; + if (options.signal.aborted) throw fetchError; - return new Response( - JSON.stringify(payload), - graphqlResponseOptions, - ); - }, - }), - { - query: gql(query), - }, - ).subscribe({ - next() { - reject(createUnexpectedCallError()); - }, - error(error) { - resolve(error); - }, - complete() { - reject(createUnexpectedCallError()); + return new Response( + JSON.stringify(payload), + graphqlResponseOptions, + ); }, - }); - }), - ); + }), + { + query: gql(query), + }, + ).subscribe({ + next() { + reject(createUnexpectedCallError()); + }, + error(error) { + resolve(error); + }, + complete() { + reject(createUnexpectedCallError()); + }, + }); + }), + ); - const observerError = await observerErrorPromise; + const observerError = await observerErrorPromise; - strictEqual(fetchUri, defaultUri); - deepEqual(fetchOptions, { - method: "POST", - headers: { accept: "*/*", "content-type": "application/json" }, - body: JSON.stringify({ variables: {}, query }), - signal: controller.signal, - }); - strictEqual(observerError, fetchError); - } finally { - revertGlobals(); - } + strictEqual(fetchUri, defaultUri); + deepEqual(fetchOptions, { + method: "POST", + headers: { accept: "*/*", "content-type": "application/json" }, + body: JSON.stringify({ variables: {}, query }), + signal: controller.signal, + }); + strictEqual(observerError, fetchError); }, ); }; diff --git a/formDataAppendFile.test.mjs b/formDataAppendFile.test.mjs index 370996f..8595924 100644 --- a/formDataAppendFile.test.mjs +++ b/formDataAppendFile.test.mjs @@ -1,5 +1,6 @@ +import "./test/polyfillFile.mjs"; + import { strictEqual } from "assert"; -import { File, FormData } from "formdata-node"; import formDataAppendFile from "./formDataAppendFile.mjs"; import assertBundleSize from "./test/assertBundleSize.mjs"; diff --git a/package.json b/package.json index b14b780..11568ec 100644 --- a/package.json +++ b/package.json @@ -50,16 +50,13 @@ }, "devDependencies": { "@apollo/client": "^3.8.6", - "abort-controller": "^3.0.0", "coverage-node": "^8.0.0", "esbuild": "^0.19.5", "eslint": "^8.52.0", "eslint-plugin-simple-import-sort": "^10.0.0", - "formdata-node": "^5.0.1", "graphql": "^16.8.1", "graphql-tag": "^2.12.6", "gzip-size": "^7.0.0", - "node-fetch": "^3.3.2", "prettier": "^3.0.3", "revertable-globals": "^4.0.0", "test-director": "^11.0.0" diff --git a/test/polyfillFile.mjs b/test/polyfillFile.mjs new file mode 100644 index 0000000..09ea552 --- /dev/null +++ b/test/polyfillFile.mjs @@ -0,0 +1,6 @@ +import { File as NodeFile } from "buffer"; + +// TODO: Delete this polyfill once all supported Node.js versions have the +// global `File`: +// https://nodejs.org/api/globals.html#class-file +globalThis.File ??= NodeFile;