From 1490df4f2dbdec4db6416a0347a37b24528ad8ef Mon Sep 17 00:00:00 2001 From: Jayden Seric Date: Mon, 23 Oct 2023 19:46:12 +1100 Subject: [PATCH] Use the Node.js test runner API and remove the dev dependency `test-director`. --- changelog.md | 1 + createUploadLink.test.mjs | 1481 +++++++++++++++++------------------ formDataAppendFile.test.mjs | 15 +- package.json | 3 +- test.mjs | 13 - 5 files changed, 730 insertions(+), 783 deletions(-) delete mode 100644 test.mjs diff --git a/changelog.md b/changelog.md index 3d43a68..851b2d2 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. +- Use the Node.js test runner API and remove the dev dependency [`test-director`](https://npm.im/test-director). - 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 8a6ee16..56cccff 100644 --- a/createUploadLink.test.mjs +++ b/createUploadLink.test.mjs @@ -3,6 +3,7 @@ import "./test/polyfillFile.mjs"; import { deepEqual, deepStrictEqual, ok, strictEqual } from "node:assert"; +import { describe, it } from "node:test"; import { ApolloLink } from "@apollo/client/link/core/ApolloLink.js"; import { concat } from "@apollo/client/link/core/concat.js"; @@ -23,174 +24,163 @@ const graphqlResponseOptions = { }, }; -/** - * Adds `createUploadLink` tests. - * @param {import("test-director").default} tests Test director. - */ -export default (tests) => { - tests.add("`createUploadLink` bundle size.", async () => { +describe("Function `createUploadLink`.", { concurrency: true }, () => { + it("Bundle size.", async () => { await assertBundleSize( new URL("./createUploadLink.mjs", import.meta.url), 1800, ); }); - tests.add( - "`createUploadLink` with default options, a query, no files.", - async () => { - /** @type {unknown} */ - let fetchInput; + it("Default options, a query, no files.", async () => { + /** @type {unknown} */ + let fetchInput; - /** @type {RequestInit | undefined} */ - let fetchOptions; + /** @type {RequestInit | undefined} */ + let fetchOptions; - /** @type {unknown} */ - let nextData; + /** @type {unknown} */ + let nextData; - const query = "{\n a\n}"; - const payload = { data: { a: true } }; - const revertGlobals = revertableGlobals({ - /** @satisfies {typeof fetch} */ - fetch: async function fetch(input, options) { - fetchInput = input; - fetchOptions = options; + const query = "{\n a\n}"; + const payload = { data: { a: true } }; + const revertGlobals = revertableGlobals({ + /** @satisfies {typeof fetch} */ + fetch: async function fetch(input, options) { + fetchInput = input; + fetchOptions = options; - return new Response(JSON.stringify(payload), graphqlResponseOptions); - }, - }); + return new Response(JSON.stringify(payload), graphqlResponseOptions); + }, + }); - try { - await timeLimitPromise( - /** @type {Promise} */ ( - new Promise((resolve, reject) => { - execute(createUploadLink(), { - query: gql(query), - }).subscribe({ - next(data) { - nextData = data; - }, - error() { - reject(createUnexpectedCallError()); - }, - complete() { - resolve(); - }, - }); - }) - ), - ); + try { + await timeLimitPromise( + /** @type {Promise} */ ( + new Promise((resolve, reject) => { + execute(createUploadLink(), { + query: gql(query), + }).subscribe({ + next(data) { + nextData = data; + }, + error() { + reject(createUnexpectedCallError()); + }, + complete() { + resolve(); + }, + }); + }) + ), + ); - strictEqual(fetchInput, defaultUri); - ok(typeof fetchOptions === "object"); + strictEqual(fetchInput, defaultUri); + ok(typeof fetchOptions === "object"); - const { signal: fetchOptionsSignal, ...fetchOptionsRest } = - fetchOptions; + const { signal: fetchOptionsSignal, ...fetchOptionsRest } = fetchOptions; - ok(fetchOptionsSignal instanceof AbortSignal); - deepEqual(fetchOptionsRest, { - method: "POST", - headers: { accept: "*/*", "content-type": "application/json" }, - body: JSON.stringify({ variables: {}, query }), - }); - deepStrictEqual(nextData, payload); - } finally { - revertGlobals(); - } - }, - ); - - tests.add( - "`createUploadLink` with default options, a mutation, files.", - async () => { - /** @type {unknown} */ - let fetchInput; - - /** @type {RequestInit | undefined} */ - let fetchOptions; - - /** @type {unknown} */ - let nextData; - - const query = "mutation ($a: Upload!) {\n a(a: $a)\n}"; - const payload = { data: { a: true } }; - const fileName = "a.txt"; - const fileType = "text/plain"; - const revertGlobals = revertableGlobals({ - /** @satisfies {typeof fetch} */ - fetch: async function fetch(input, options) { - fetchInput = input; - fetchOptions = options; - - return new Response(JSON.stringify(payload), graphqlResponseOptions); - }, + ok(fetchOptionsSignal instanceof AbortSignal); + deepEqual(fetchOptionsRest, { + method: "POST", + headers: { accept: "*/*", "content-type": "application/json" }, + body: JSON.stringify({ variables: {}, query }), }); + deepStrictEqual(nextData, payload); + } finally { + revertGlobals(); + } + }); - try { - await timeLimitPromise( - /** @type {Promise} */ ( - new Promise((resolve, reject) => { - execute(createUploadLink(), { - query: gql(query), - variables: { - a: new File(["a"], fileName, { type: fileType }), - }, - }).subscribe({ - next(data) { - nextData = data; - }, - error() { - reject(createUnexpectedCallError()); - }, - complete() { - resolve(); - }, - }); - }) - ), - ); - - strictEqual(fetchInput, defaultUri); - ok(typeof fetchOptions === "object"); - - const { - signal: fetchOptionsSignal, - body: fetchOptionsBody, - ...fetchOptionsRest - } = fetchOptions; - - ok(fetchOptionsSignal instanceof AbortSignal); - ok(fetchOptionsBody instanceof FormData); - - const formDataEntries = Array.from(fetchOptionsBody.entries()); - - strictEqual(formDataEntries.length, 3); - strictEqual(formDataEntries[0][0], "operations"); - ok(typeof formDataEntries[0][1] === "string"); - deepStrictEqual(JSON.parse(formDataEntries[0][1]), { - query, - variables: { a: null }, - }); - strictEqual(formDataEntries[1][0], "map"); - ok(typeof formDataEntries[1][1] === "string"); - deepStrictEqual(JSON.parse(formDataEntries[1][1]), { - 1: ["variables.a"], - }); - strictEqual(formDataEntries[2][0], "1"); - ok(formDataEntries[2][1] instanceof File); - strictEqual(formDataEntries[2][1].name, fileName); - strictEqual(formDataEntries[2][1].type, fileType); - deepEqual(fetchOptionsRest, { - method: "POST", - headers: { accept: "*/*" }, - }); - deepStrictEqual(nextData, payload); - } finally { - revertGlobals(); - } - }, - ); + it("Default options, a mutation, files.", async () => { + /** @type {unknown} */ + let fetchInput; + + /** @type {RequestInit | undefined} */ + let fetchOptions; + + /** @type {unknown} */ + let nextData; + + const query = "mutation ($a: Upload!) {\n a(a: $a)\n}"; + const payload = { data: { a: true } }; + const fileName = "a.txt"; + const fileType = "text/plain"; + const revertGlobals = revertableGlobals({ + /** @satisfies {typeof fetch} */ + fetch: async function fetch(input, options) { + fetchInput = input; + fetchOptions = options; + + return new Response(JSON.stringify(payload), graphqlResponseOptions); + }, + }); + + try { + await timeLimitPromise( + /** @type {Promise} */ ( + new Promise((resolve, reject) => { + execute(createUploadLink(), { + query: gql(query), + variables: { + a: new File(["a"], fileName, { type: fileType }), + }, + }).subscribe({ + next(data) { + nextData = data; + }, + error() { + reject(createUnexpectedCallError()); + }, + complete() { + resolve(); + }, + }); + }) + ), + ); + + strictEqual(fetchInput, defaultUri); + ok(typeof fetchOptions === "object"); + + const { + signal: fetchOptionsSignal, + body: fetchOptionsBody, + ...fetchOptionsRest + } = fetchOptions; - tests.add("`createUploadLink` with option `uri`.", async () => { + ok(fetchOptionsSignal instanceof AbortSignal); + ok(fetchOptionsBody instanceof FormData); + + const formDataEntries = Array.from(fetchOptionsBody.entries()); + + strictEqual(formDataEntries.length, 3); + strictEqual(formDataEntries[0][0], "operations"); + ok(typeof formDataEntries[0][1] === "string"); + deepStrictEqual(JSON.parse(formDataEntries[0][1]), { + query, + variables: { a: null }, + }); + strictEqual(formDataEntries[1][0], "map"); + ok(typeof formDataEntries[1][1] === "string"); + deepStrictEqual(JSON.parse(formDataEntries[1][1]), { + 1: ["variables.a"], + }); + strictEqual(formDataEntries[2][0], "1"); + ok(formDataEntries[2][1] instanceof File); + strictEqual(formDataEntries[2][1].name, fileName); + strictEqual(formDataEntries[2][1].type, fileType); + deepEqual(fetchOptionsRest, { + method: "POST", + headers: { accept: "*/*" }, + }); + deepStrictEqual(nextData, payload); + } finally { + revertGlobals(); + } + }); + + it("Option `uri`.", async () => { /** @type {unknown} */ let fetchInput; @@ -252,7 +242,7 @@ export default (tests) => { deepStrictEqual(nextData, payload); }); - tests.add("`createUploadLink` with option `includeExtensions`.", async () => { + it("Option `includeExtensions`.", async () => { /** @type {unknown} */ let fetchInput; @@ -325,246 +315,296 @@ export default (tests) => { deepStrictEqual(nextData, payload); }); - tests.add( - "`createUploadLink` with option `fetchOptions.method`.", - async () => { - /** @type {unknown} */ - let fetchInput; + it("Option `fetchOptions.method`.", async () => { + /** @type {unknown} */ + let fetchInput; - /** @type {RequestInit | undefined} */ - let fetchOptions; + /** @type {RequestInit | undefined} */ + let fetchOptions; - /** @type {unknown} */ - let nextData; + /** @type {unknown} */ + let nextData; - const query = "{\n a\n}"; - const payload = { data: { a: true } }; + const query = "{\n a\n}"; + const payload = { data: { a: true } }; - await timeLimitPromise( - /** @type {Promise} */ ( - new Promise((resolve, reject) => { - execute( - createUploadLink({ - fetchOptions: { method: "GET" }, - async fetch(input, options) { - fetchInput = input; - fetchOptions = options; + await timeLimitPromise( + /** @type {Promise} */ ( + new Promise((resolve, reject) => { + execute( + createUploadLink({ + fetchOptions: { method: "GET" }, + async fetch(input, options) { + fetchInput = input; + fetchOptions = options; - return new Response( - JSON.stringify(payload), - graphqlResponseOptions, - ); - }, - }), - { - query: gql(query), - }, - ).subscribe({ - next(data) { - nextData = data; - }, - error() { - reject(createUnexpectedCallError()); - }, - complete() { - resolve(); + return new Response( + JSON.stringify(payload), + graphqlResponseOptions, + ); }, - }); - }) - ), - ); + }), + { + query: gql(query), + }, + ).subscribe({ + next(data) { + nextData = data; + }, + error() { + reject(createUnexpectedCallError()); + }, + complete() { + resolve(); + }, + }); + }) + ), + ); - strictEqual( - fetchInput, - `${defaultUri}?query=%7B%0A%20%20a%0A%7D&variables=%7B%7D`, - ); - ok(typeof fetchOptions === "object"); + strictEqual( + fetchInput, + `${defaultUri}?query=%7B%0A%20%20a%0A%7D&variables=%7B%7D`, + ); + ok(typeof fetchOptions === "object"); - const { signal: fetchOptionsSignal, ...fetchOptionsRest } = fetchOptions; + const { signal: fetchOptionsSignal, ...fetchOptionsRest } = fetchOptions; - ok(fetchOptionsSignal instanceof AbortSignal); - deepEqual(fetchOptionsRest, { - method: "GET", - headers: { accept: "*/*", "content-type": "application/json" }, - }); - deepStrictEqual(nextData, payload); - }, - ); + ok(fetchOptionsSignal instanceof AbortSignal); + deepEqual(fetchOptionsRest, { + method: "GET", + headers: { accept: "*/*", "content-type": "application/json" }, + }); + deepStrictEqual(nextData, payload); + }); - tests.add( - "`createUploadLink` with option `useGETForQueries`, query, no files.", - async () => { - /** @type {unknown} */ - let fetchInput; + it("Option `useGETForQueries`, query, no files.", async () => { + /** @type {unknown} */ + let fetchInput; - /** @type {RequestInit | undefined} */ - let fetchOptions; + /** @type {RequestInit | undefined} */ + let fetchOptions; - /** @type {unknown} */ - let nextData; + /** @type {unknown} */ + let nextData; - const query = "{\n a\n}"; - const payload = { data: { a: true } }; + const query = "{\n a\n}"; + const payload = { data: { a: true } }; - await timeLimitPromise( - /** @type {Promise} */ ( - new Promise((resolve, reject) => { - execute( - createUploadLink({ - useGETForQueries: true, - async fetch(input, options) { - fetchInput = input; - fetchOptions = options; + await timeLimitPromise( + /** @type {Promise} */ ( + new Promise((resolve, reject) => { + execute( + createUploadLink({ + useGETForQueries: true, + async fetch(input, options) { + fetchInput = input; + fetchOptions = options; - return new Response( - JSON.stringify(payload), - graphqlResponseOptions, - ); - }, - }), - { - query: gql(query), - }, - ).subscribe({ - next(data) { - nextData = data; - }, - error() { - reject(createUnexpectedCallError()); - }, - complete() { - resolve(); + return new Response( + JSON.stringify(payload), + graphqlResponseOptions, + ); }, - }); - }) - ), - ); - - strictEqual( - fetchInput, - `${defaultUri}?query=%7B%0A%20%20a%0A%7D&variables=%7B%7D`, - ); - ok(typeof fetchOptions === "object"); - - const { signal: fetchOptionsSignal, ...fetchOptionsRest } = fetchOptions; + }), + { + query: gql(query), + }, + ).subscribe({ + next(data) { + nextData = data; + }, + error() { + reject(createUnexpectedCallError()); + }, + complete() { + resolve(); + }, + }); + }) + ), + ); - ok(fetchOptionsSignal instanceof AbortSignal); - deepEqual(fetchOptionsRest, { - method: "GET", - headers: { accept: "*/*", "content-type": "application/json" }, - }); - deepStrictEqual(nextData, payload); - }, - ); + strictEqual( + fetchInput, + `${defaultUri}?query=%7B%0A%20%20a%0A%7D&variables=%7B%7D`, + ); + ok(typeof fetchOptions === "object"); - tests.add( - "`createUploadLink` with option `useGETForQueries`, query, files.", - async () => { - /** @type {unknown} */ - let fetchInput; + const { signal: fetchOptionsSignal, ...fetchOptionsRest } = fetchOptions; - /** @type {RequestInit | undefined} */ - let fetchOptions; + ok(fetchOptionsSignal instanceof AbortSignal); + deepEqual(fetchOptionsRest, { + method: "GET", + headers: { accept: "*/*", "content-type": "application/json" }, + }); + deepStrictEqual(nextData, payload); + }); - /** @type {unknown} */ - let nextData; + it("Option `useGETForQueries`, query, files.", async () => { + /** @type {unknown} */ + let fetchInput; - const query = "query ($a: Upload!) {\n a(a: $a)\n}"; - const payload = { data: { a: true } }; - const fileName = "a.txt"; - const fileType = "text/plain"; + /** @type {RequestInit | undefined} */ + let fetchOptions; - await timeLimitPromise( - /** @type {Promise} */ ( - new Promise((resolve, reject) => { - execute( - createUploadLink({ - useGETForQueries: true, - FormData, - async fetch(input, options) { - fetchInput = input; - fetchOptions = options; + /** @type {unknown} */ + let nextData; - return new Response( - JSON.stringify(payload), - graphqlResponseOptions, - ); - }, - }), - { - query: gql(query), - variables: { - a: new File(["a"], fileName, { type: fileType }), - }, - }, - ).subscribe({ - next(data) { - nextData = data; + const query = "query ($a: Upload!) {\n a(a: $a)\n}"; + const payload = { data: { a: true } }; + const fileName = "a.txt"; + const fileType = "text/plain"; + + await timeLimitPromise( + /** @type {Promise} */ ( + new Promise((resolve, reject) => { + execute( + createUploadLink({ + useGETForQueries: true, + FormData, + async fetch(input, options) { + fetchInput = input; + fetchOptions = options; + + return new Response( + JSON.stringify(payload), + graphqlResponseOptions, + ); }, - error() { - reject(createUnexpectedCallError()); + }), + { + query: gql(query), + variables: { + a: new File(["a"], fileName, { type: fileType }), }, - complete() { - resolve(); + }, + ).subscribe({ + next(data) { + nextData = data; + }, + error() { + reject(createUnexpectedCallError()); + }, + complete() { + resolve(); + }, + }); + }) + ), + ); + + strictEqual(fetchInput, defaultUri); + ok(typeof fetchOptions === "object"); + + const { + signal: fetchOptionsSignal, + body: fetchOptionsBody, + ...fetchOptionsRest + } = fetchOptions; + + ok(fetchOptionsSignal instanceof AbortSignal); + ok(fetchOptionsBody instanceof FormData); + + const formDataEntries = Array.from(fetchOptionsBody.entries()); + + strictEqual(formDataEntries.length, 3); + strictEqual(formDataEntries[0][0], "operations"); + ok(typeof formDataEntries[0][1] === "string"); + deepStrictEqual(JSON.parse(formDataEntries[0][1]), { + query, + variables: { a: null }, + }); + strictEqual(formDataEntries[1][0], "map"); + ok(typeof formDataEntries[1][1] === "string"); + deepStrictEqual(JSON.parse(formDataEntries[1][1]), { + 1: ["variables.a"], + }); + strictEqual(formDataEntries[2][0], "1"); + ok(formDataEntries[2][1] instanceof File); + strictEqual(formDataEntries[2][1].name, fileName); + strictEqual(formDataEntries[2][1].type, fileType); + deepEqual(fetchOptionsRest, { + method: "POST", + headers: { accept: "*/*" }, + }); + deepStrictEqual(nextData, payload); + }); + + it("Option `useGETForQueries`, query, no files, unserializable variables.", async () => { + let fetched = false; + + const query = "query($a: Boolean) {\n a(a: $a)\n}"; + const payload = { data: { a: true } }; + const parseError = new Error("Unserializable."); + const observerError = await timeLimitPromise( + new Promise((resolve, reject) => { + execute( + createUploadLink({ + useGETForQueries: true, + async fetch() { + fetched = true; + + return new Response( + JSON.stringify(payload), + graphqlResponseOptions, + ); + }, + }), + { + query: gql(query), + variables: { + // A circular reference would be a more realistic way to cause a + // `JSON.stringify` error, but unfortunately that triggers an + // `extractFiles` bug: + // https://github.com/jaydenseric/extract-files/issues/14 + toJSON() { + throw parseError; }, - }); - }) - ), - ); + }, + }, + ).subscribe({ + next() { + reject(createUnexpectedCallError()); + }, + error(error) { + resolve(error); + }, + complete() { + reject(createUnexpectedCallError()); + }, + }); + }), + ); - strictEqual(fetchInput, defaultUri); - ok(typeof fetchOptions === "object"); + strictEqual(fetched, false); + ok(typeof observerError === "object"); + strictEqual(observerError.name, "Invariant Violation"); + strictEqual(observerError.parseError, parseError); + }); - const { - signal: fetchOptionsSignal, - body: fetchOptionsBody, - ...fetchOptionsRest - } = fetchOptions; + it("Option `useGETForQueries`, mutation, no files.", async () => { + /** @type {unknown} */ + let fetchInput; - ok(fetchOptionsSignal instanceof AbortSignal); - ok(fetchOptionsBody instanceof FormData); + /** @type {RequestInit | undefined} */ + let fetchOptions; - const formDataEntries = Array.from(fetchOptionsBody.entries()); + /** @type {unknown} */ + let nextData; - strictEqual(formDataEntries.length, 3); - strictEqual(formDataEntries[0][0], "operations"); - ok(typeof formDataEntries[0][1] === "string"); - deepStrictEqual(JSON.parse(formDataEntries[0][1]), { - query, - variables: { a: null }, - }); - strictEqual(formDataEntries[1][0], "map"); - ok(typeof formDataEntries[1][1] === "string"); - deepStrictEqual(JSON.parse(formDataEntries[1][1]), { - 1: ["variables.a"], - }); - strictEqual(formDataEntries[2][0], "1"); - ok(formDataEntries[2][1] instanceof File); - strictEqual(formDataEntries[2][1].name, fileName); - strictEqual(formDataEntries[2][1].type, fileType); - deepEqual(fetchOptionsRest, { - method: "POST", - headers: { accept: "*/*" }, - }); - deepStrictEqual(nextData, payload); - }, - ); - - tests.add( - "`createUploadLink` with option `useGETForQueries`, query, no files, unserializable variables.", - async () => { - let fetched = false; - - const query = "query($a: Boolean) {\n a(a: $a)\n}"; - const payload = { data: { a: true } }; - const parseError = new Error("Unserializable."); - const observerError = await timeLimitPromise( + const query = "mutation {\n a\n}"; + const payload = { data: { a: true } }; + + await timeLimitPromise( + /** @type {Promise} */ ( new Promise((resolve, reject) => { execute( createUploadLink({ useGETForQueries: true, - async fetch() { - fetched = true; + async fetch(input, options) { + fetchInput = input; + fetchOptions = options; return new Response( JSON.stringify(payload), @@ -574,58 +614,60 @@ export default (tests) => { }), { query: gql(query), - variables: { - // A circular reference would be a more realistic way to cause a - // `JSON.stringify` error, but unfortunately that triggers an - // `extractFiles` bug: - // https://github.com/jaydenseric/extract-files/issues/14 - toJSON() { - throw parseError; - }, - }, }, ).subscribe({ - next() { - reject(createUnexpectedCallError()); + next(data) { + nextData = data; }, - error(error) { - resolve(error); + error() { + reject(createUnexpectedCallError()); }, complete() { - reject(createUnexpectedCallError()); + resolve(); }, }); - }), - ); + }) + ), + ); - strictEqual(fetched, false); - ok(typeof observerError === "object"); - strictEqual(observerError.name, "Invariant Violation"); - strictEqual(observerError.parseError, parseError); - }, - ); + strictEqual(fetchInput, defaultUri); + ok(typeof fetchOptions === "object"); - tests.add( - "`createUploadLink` with option `useGETForQueries`, mutation, no files.", - async () => { - /** @type {unknown} */ - let fetchInput; + const { signal: fetchOptionsSignal, ...fetchOptionsRest } = fetchOptions; - /** @type {RequestInit | undefined} */ - let fetchOptions; + ok(fetchOptionsSignal instanceof AbortSignal); + deepEqual(fetchOptionsRest, { + method: "POST", + headers: { accept: "*/*", "content-type": "application/json" }, + body: JSON.stringify({ variables: {}, query }), + }); + deepStrictEqual(nextData, payload); + }); + + it("Context `clientAwareness`.", async () => { + /** @type {unknown} */ + let fetchInput; + + /** @type {RequestInit | undefined} */ + let fetchOptions; - /** @type {unknown} */ - let nextData; + /** @type {unknown} */ + let nextData; - const query = "mutation {\n a\n}"; - const payload = { data: { a: true } }; + const clientAwareness = { name: "a", version: "1.0.0" }; + const query = "{\n a\n}"; + const payload = { data: { a: true } }; - await timeLimitPromise( - /** @type {Promise} */ ( - new Promise((resolve, reject) => { - execute( + await timeLimitPromise( + /** @type {Promise} */ ( + new Promise((resolve, reject) => { + execute( + concat( + new ApolloLink((operation, forward) => { + operation.setContext({ clientAwareness }); + return forward(operation); + }), createUploadLink({ - useGETForQueries: true, async fetch(input, options) { fetchInput = input; fetchOptions = options; @@ -636,40 +678,45 @@ export default (tests) => { ); }, }), - { - query: gql(query), - }, - ).subscribe({ - next(data) { - nextData = data; - }, - error() { - reject(createUnexpectedCallError()); - }, - complete() { - resolve(); - }, - }); - }) - ), - ); + ), + { + query: gql(query), + }, + ).subscribe({ + next(data) { + nextData = data; + }, + error() { + reject(createUnexpectedCallError()); + }, + complete() { + resolve(); + }, + }); + }) + ), + ); - strictEqual(fetchInput, defaultUri); - ok(typeof fetchOptions === "object"); + strictEqual(fetchInput, defaultUri); + ok(typeof fetchOptions === "object"); - const { signal: fetchOptionsSignal, ...fetchOptionsRest } = fetchOptions; + const { signal: fetchOptionsSignal, ...fetchOptionsRest } = fetchOptions; - ok(fetchOptionsSignal instanceof AbortSignal); - deepEqual(fetchOptionsRest, { - method: "POST", - headers: { accept: "*/*", "content-type": "application/json" }, - body: JSON.stringify({ variables: {}, query }), - }); - deepStrictEqual(nextData, payload); - }, - ); + ok(fetchOptionsSignal instanceof AbortSignal); + deepEqual(fetchOptionsRest, { + method: "POST", + headers: { + accept: "*/*", + "content-type": "application/json", + "apollographql-client-name": clientAwareness.name, + "apollographql-client-version": clientAwareness.version, + }, + body: JSON.stringify({ variables: {}, query }), + }); + deepStrictEqual(nextData, payload); + }); - tests.add("`createUploadLink` with context `clientAwareness`.", async () => { + it("Context `clientAwareness`, overridden by context `headers`.", async () => { /** @type {unknown} */ let fetchInput; @@ -679,7 +726,8 @@ export default (tests) => { /** @type {unknown} */ let nextData; - const clientAwareness = { name: "a", version: "1.0.0" }; + const clientAwarenessOriginal = { name: "a", version: "1.0.0" }; + const clientAwarenessOverride = { name: "b", version: "2.0.0" }; const query = "{\n a\n}"; const payload = { data: { a: true } }; @@ -689,7 +737,14 @@ export default (tests) => { execute( concat( new ApolloLink((operation, forward) => { - operation.setContext({ clientAwareness }); + operation.setContext({ + clientAwareness: clientAwarenessOriginal, + headers: { + "apollographql-client-name": clientAwarenessOverride.name, + "apollographql-client-version": + clientAwarenessOverride.version, + }, + }); return forward(operation); }), createUploadLink({ @@ -697,15 +752,106 @@ export default (tests) => { fetchInput = input; fetchOptions = options; - return new Response( - JSON.stringify(payload), - graphqlResponseOptions, - ); - }, - }), - ), + return new Response( + JSON.stringify(payload), + graphqlResponseOptions, + ); + }, + }), + ), + { + query: gql(query), + }, + ).subscribe({ + next(data) { + nextData = data; + }, + error() { + reject(createUnexpectedCallError()); + }, + complete() { + resolve(); + }, + }); + }) + ), + ); + + strictEqual(fetchInput, defaultUri); + ok(typeof fetchOptions === "object"); + + const { signal: fetchOptionsSignal, ...fetchOptionsRest } = fetchOptions; + + ok(fetchOptionsSignal instanceof AbortSignal); + deepEqual(fetchOptionsRest, { + method: "POST", + headers: { + accept: "*/*", + "content-type": "application/json", + "apollographql-client-name": clientAwarenessOverride.name, + "apollographql-client-version": clientAwarenessOverride.version, + }, + body: JSON.stringify({ variables: {}, query }), + }); + deepStrictEqual(nextData, payload); + }); + + it("Options `isExtractableFile` and `formDataAppendFile`.", async () => { + /** @type {unknown} */ + let fetchInput; + + /** @type {RequestInit | undefined} */ + let fetchOptions; + + /** @type {unknown} */ + let nextData; + + const query = "mutation ($a: Upload!) {\n a(a: $a)\n}"; + const payload = { data: { a: true } }; + const fileName = "a.txt"; + const fileType = "text/plain"; + + class TextFile { + /** + * @param {string} text Text. + * @param {string} fileName File name. + */ + constructor(text, fileName) { + this.file = new File([text], fileName, { type: fileType }); + } + } + + await timeLimitPromise( + /** @type {Promise} */ ( + new Promise((resolve, reject) => { + execute( + createUploadLink({ + /** @returns {value is TextFile} */ + isExtractableFile(value) { + return value instanceof TextFile; + }, + formDataAppendFile(formData, fieldName, file) { + formData.append( + fieldName, + file instanceof TextFile ? file.file : file, + ); + }, + FormData, + async fetch(input, options) { + fetchInput = input; + fetchOptions = options; + + return new Response( + JSON.stringify(payload), + graphqlResponseOptions, + ); + }, + }), { query: gql(query), + variables: { + a: new TextFile("a", fileName), + }, }, ).subscribe({ next(data) { @@ -725,218 +871,41 @@ export default (tests) => { strictEqual(fetchInput, defaultUri); ok(typeof fetchOptions === "object"); - const { signal: fetchOptionsSignal, ...fetchOptionsRest } = fetchOptions; + const { + signal: fetchOptionsSignal, + body: fetchOptionsBody, + ...fetchOptionsRest + } = fetchOptions; ok(fetchOptionsSignal instanceof AbortSignal); + ok(fetchOptionsBody instanceof FormData); + + const formDataEntries = Array.from(fetchOptionsBody.entries()); + + strictEqual(formDataEntries.length, 3); + strictEqual(formDataEntries[0][0], "operations"); + ok(typeof formDataEntries[0][1] === "string"); + deepStrictEqual(JSON.parse(formDataEntries[0][1]), { + query, + variables: { a: null }, + }); + strictEqual(formDataEntries[1][0], "map"); + ok(typeof formDataEntries[1][1] === "string"); + deepStrictEqual(JSON.parse(formDataEntries[1][1]), { + 1: ["variables.a"], + }); + strictEqual(formDataEntries[2][0], "1"); + ok(formDataEntries[2][1] instanceof File); + strictEqual(formDataEntries[2][1].name, fileName); + strictEqual(formDataEntries[2][1].type, fileType); deepEqual(fetchOptionsRest, { method: "POST", - headers: { - accept: "*/*", - "content-type": "application/json", - "apollographql-client-name": clientAwareness.name, - "apollographql-client-version": clientAwareness.version, - }, - body: JSON.stringify({ variables: {}, query }), + headers: { accept: "*/*" }, }); deepStrictEqual(nextData, payload); }); - tests.add( - "`createUploadLink` with context `clientAwareness`, overridden by context `headers`.", - async () => { - /** @type {unknown} */ - let fetchInput; - - /** @type {RequestInit | undefined} */ - let fetchOptions; - - /** @type {unknown} */ - let nextData; - - const clientAwarenessOriginal = { name: "a", version: "1.0.0" }; - const clientAwarenessOverride = { name: "b", version: "2.0.0" }; - const query = "{\n a\n}"; - const payload = { data: { a: true } }; - - await timeLimitPromise( - /** @type {Promise} */ ( - new Promise((resolve, reject) => { - execute( - concat( - new ApolloLink((operation, forward) => { - operation.setContext({ - clientAwareness: clientAwarenessOriginal, - headers: { - "apollographql-client-name": clientAwarenessOverride.name, - "apollographql-client-version": - clientAwarenessOverride.version, - }, - }); - return forward(operation); - }), - createUploadLink({ - async fetch(input, options) { - fetchInput = input; - fetchOptions = options; - - return new Response( - JSON.stringify(payload), - graphqlResponseOptions, - ); - }, - }), - ), - { - query: gql(query), - }, - ).subscribe({ - next(data) { - nextData = data; - }, - error() { - reject(createUnexpectedCallError()); - }, - complete() { - resolve(); - }, - }); - }) - ), - ); - - strictEqual(fetchInput, defaultUri); - ok(typeof fetchOptions === "object"); - - const { signal: fetchOptionsSignal, ...fetchOptionsRest } = fetchOptions; - - ok(fetchOptionsSignal instanceof AbortSignal); - deepEqual(fetchOptionsRest, { - method: "POST", - headers: { - accept: "*/*", - "content-type": "application/json", - "apollographql-client-name": clientAwarenessOverride.name, - "apollographql-client-version": clientAwarenessOverride.version, - }, - body: JSON.stringify({ variables: {}, query }), - }); - deepStrictEqual(nextData, payload); - }, - ); - - tests.add( - "`createUploadLink` with options `isExtractableFile` and `formDataAppendFile`.", - async () => { - /** @type {unknown} */ - let fetchInput; - - /** @type {RequestInit | undefined} */ - let fetchOptions; - - /** @type {unknown} */ - let nextData; - - const query = "mutation ($a: Upload!) {\n a(a: $a)\n}"; - const payload = { data: { a: true } }; - const fileName = "a.txt"; - const fileType = "text/plain"; - - class TextFile { - /** - * @param {string} text Text. - * @param {string} fileName File name. - */ - constructor(text, fileName) { - this.file = new File([text], fileName, { type: fileType }); - } - } - - await timeLimitPromise( - /** @type {Promise} */ ( - new Promise((resolve, reject) => { - execute( - createUploadLink({ - /** @returns {value is TextFile} */ - isExtractableFile(value) { - return value instanceof TextFile; - }, - formDataAppendFile(formData, fieldName, file) { - formData.append( - fieldName, - file instanceof TextFile ? file.file : file, - ); - }, - FormData, - async fetch(input, options) { - fetchInput = input; - fetchOptions = options; - - return new Response( - JSON.stringify(payload), - graphqlResponseOptions, - ); - }, - }), - { - query: gql(query), - variables: { - a: new TextFile("a", fileName), - }, - }, - ).subscribe({ - next(data) { - nextData = data; - }, - error() { - reject(createUnexpectedCallError()); - }, - complete() { - resolve(); - }, - }); - }) - ), - ); - - strictEqual(fetchInput, defaultUri); - ok(typeof fetchOptions === "object"); - - const { - signal: fetchOptionsSignal, - body: fetchOptionsBody, - ...fetchOptionsRest - } = fetchOptions; - - ok(fetchOptionsSignal instanceof AbortSignal); - ok(fetchOptionsBody instanceof FormData); - - const formDataEntries = Array.from(fetchOptionsBody.entries()); - - strictEqual(formDataEntries.length, 3); - strictEqual(formDataEntries[0][0], "operations"); - ok(typeof formDataEntries[0][1] === "string"); - deepStrictEqual(JSON.parse(formDataEntries[0][1]), { - query, - variables: { a: null }, - }); - strictEqual(formDataEntries[1][0], "map"); - ok(typeof formDataEntries[1][1] === "string"); - deepStrictEqual(JSON.parse(formDataEntries[1][1]), { - 1: ["variables.a"], - }); - strictEqual(formDataEntries[2][0], "1"); - ok(formDataEntries[2][1] instanceof File); - strictEqual(formDataEntries[2][1].name, fileName); - strictEqual(formDataEntries[2][1].type, fileType); - deepEqual(fetchOptionsRest, { - method: "POST", - headers: { accept: "*/*" }, - }); - deepStrictEqual(nextData, payload); - }, - ); - - tests.add("`createUploadLink` with a HTTP error, data.", async () => { + it("HTTP error, data.", async () => { /** @type {Response | undefined} */ let fetchResponse; @@ -987,7 +956,7 @@ export default (tests) => { deepStrictEqual(nextData, payload); }); - tests.add("`createUploadLink` with a HTTP error, no data.", async () => { + it("HTTP error, no data.", async () => { /** @type {Response | undefined} */ let fetchResponse; @@ -1026,7 +995,7 @@ export default (tests) => { deepStrictEqual(observerError.result, payload); }); - tests.add("`createUploadLink` with a fetch error.", async () => { + it("Fetch error.", async () => { const fetchError = new Error("Expected."); const observerError = await timeLimitPromise( new Promise((resolve, reject) => { @@ -1056,142 +1025,136 @@ export default (tests) => { strictEqual(observerError, fetchError); }); - tests.add( - "`createUploadLink` with option `fetchOptions.signal`, not yet aborted.", - async () => { - /** @type {unknown} */ - let fetchInput; + it("Option `fetchOptions.signal`, not yet aborted.", async () => { + /** @type {unknown} */ + let fetchInput; - /** @type {RequestInit | undefined} */ - let fetchOptions; + /** @type {RequestInit | undefined} */ + let fetchOptions; - const query = "{\n a\n}"; - const payload = { data: { a: true } }; - const controller = new AbortController(); - const fetchError = new Error("The operation was aborted."); + const query = "{\n a\n}"; + const payload = { data: { a: true } }; + const controller = new AbortController(); + const fetchError = new Error("The operation was aborted."); - const observerErrorPromise = timeLimitPromise( - new Promise((resolve, reject) => { - execute( - createUploadLink({ - fetchOptions: { signal: controller.signal }, - fetch(input, options) { - fetchInput = input; - fetchOptions = options; + const observerErrorPromise = timeLimitPromise( + new Promise((resolve, reject) => { + execute( + createUploadLink({ + fetchOptions: { signal: controller.signal }, + fetch(input, options) { + fetchInput = input; + 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); - 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); - }); + 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(fetchInput, defaultUri); - deepEqual(fetchOptions, { - method: "POST", - headers: { accept: "*/*", "content-type": "application/json" }, - body: JSON.stringify({ variables: {}, query }), - signal: controller.signal, - }); - strictEqual(observerError, fetchError); - }, - ); + strictEqual(fetchInput, defaultUri); + deepEqual(fetchOptions, { + method: "POST", + headers: { accept: "*/*", "content-type": "application/json" }, + body: JSON.stringify({ variables: {}, query }), + signal: controller.signal, + }); + strictEqual(observerError, fetchError); + }); - tests.add( - "`createUploadLink` with option `fetchOptions.signal`, already aborted.", - async () => { - /** @type {unknown} */ - let fetchInput; + it("Option `fetchOptions.signal`, already aborted.", async () => { + /** @type {unknown} */ + let fetchInput; - /** @type {RequestInit | undefined} */ - let fetchOptions; + /** @type {RequestInit | undefined} */ + let fetchOptions; - const query = "{\n a\n}"; - const payload = { data: { a: true } }; + const query = "{\n a\n}"; + const payload = { data: { a: true } }; - const controller = new AbortController(); - controller.abort(); + const controller = new AbortController(); + controller.abort(); - const fetchError = new Error("The operation was aborted."); + const fetchError = new Error("The operation was aborted."); - const observerErrorPromise = timeLimitPromise( - new Promise((resolve, reject) => { - execute( - createUploadLink({ - fetchOptions: { signal: controller.signal }, - async fetch(input, options) { - fetchInput = input; - fetchOptions = options; + const observerErrorPromise = timeLimitPromise( + new Promise((resolve, reject) => { + execute( + createUploadLink({ + fetchOptions: { signal: controller.signal }, + async fetch(input, options) { + fetchInput = input; + 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(fetchInput, defaultUri); - deepEqual(fetchOptions, { - method: "POST", - headers: { accept: "*/*", "content-type": "application/json" }, - body: JSON.stringify({ variables: {}, query }), - signal: controller.signal, - }); - strictEqual(observerError, fetchError); - }, - ); -}; + strictEqual(fetchInput, 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 4c78df5..f034e18 100644 --- a/formDataAppendFile.test.mjs +++ b/formDataAppendFile.test.mjs @@ -3,23 +3,20 @@ import "./test/polyfillFile.mjs"; import { ok, strictEqual } from "node:assert"; +import { describe, it } from "node:test"; import formDataAppendFile from "./formDataAppendFile.mjs"; import assertBundleSize from "./test/assertBundleSize.mjs"; -/** - * Adds `formDataAppendFile` tests. - * @param {import("test-director").default} tests Test director. - */ -export default (tests) => { - tests.add("`formDataAppendFile` bundle size.", async () => { +describe("Function `formDataAppendFile`.", { concurrency: true }, () => { + it("Bundle size.", async () => { await assertBundleSize( new URL("./formDataAppendFile.mjs", import.meta.url), 100, ); }); - tests.add("`formDataAppendFile` functionality, `Blob` instance.", () => { + it("`Blob` instance.", () => { const formData = new FormData(); const fieldName = "a"; const fileType = "text/plain"; @@ -39,7 +36,7 @@ export default (tests) => { strictEqual(formDataEntries[0][1].type, fileType); }); - tests.add("`formDataAppendFile` functionality, `File` instance.", () => { + it("`File` instance.", () => { const formData = new FormData(); const fieldName = "a"; const fileName = "a.txt"; @@ -59,4 +56,4 @@ export default (tests) => { strictEqual(formDataEntries[0][1].name, fileName); strictEqual(formDataEntries[0][1].type, fileType); }); -}; +}); diff --git a/package.json b/package.json index d7913ea..35a2ccf 100644 --- a/package.json +++ b/package.json @@ -60,14 +60,13 @@ "gzip-size": "^7.0.0", "prettier": "^3.0.3", "revertable-globals": "^4.0.0", - "test-director": "^11.0.0", "typescript": "^5.2.2" }, "scripts": { "eslint": "eslint .", "prettier": "prettier -c .", "types": "tsc -p jsconfig.json", - "tests": "coverage-node test.mjs", + "tests": "coverage-node --test-reporter=spec --test", "test": "npm run eslint && npm run prettier && npm run types && npm run tests", "prepublishOnly": "npm test" } diff --git a/test.mjs b/test.mjs deleted file mode 100644 index fee28ca..0000000 --- a/test.mjs +++ /dev/null @@ -1,13 +0,0 @@ -// @ts-check - -import TestDirector from "test-director"; - -import test_createUploadLink from "./createUploadLink.test.mjs"; -import test_formDataAppendFile from "./formDataAppendFile.test.mjs"; - -const tests = new TestDirector(); - -test_createUploadLink(tests); -test_formDataAppendFile(tests); - -tests.run();