diff --git a/changelog.md b/changelog.md index 69c9c9d..5c8c72f 100644 --- a/changelog.md +++ b/changelog.md @@ -22,7 +22,8 @@ - Updated `actions/checkout` to v4. - Updated `actions/setup-node` to v4. - Fixed the types for test fixture Next.js config. -- In tests: +- For the function `withGraphQLReact` tests: + - Temporarily disabled the tests for Node.js v18 due to the Node.js test runner bug [nodejs/node#48845](https://github.com/nodejs/node/issues/48845) that will be fixed in a future Node.js v18 release. - Use the new Puppeteer headless mode. - For the client side page load test: - Attempt to wait until the JS has loaded and the React app has mounted before clicking the navigation link. diff --git a/withGraphQLReact.test.mjs b/withGraphQLReact.test.mjs index efd1dd0..0e951ad 100644 --- a/withGraphQLReact.test.mjs +++ b/withGraphQLReact.test.mjs @@ -12,356 +12,361 @@ import execFilePromise from "./test/execFilePromise.mjs"; import listen from "./test/listen.mjs"; import startNext from "./test/startNext.mjs"; -describe("Function `withGraphQLReact`.", { concurrency: true }, async () => { - const markerA = "MARKER_A"; - const markerB = "MARKER_B"; - - /** - * Dummy GraphQL server with a hardcoded response. The URL query string - * parameter `linkHeader` can be used to set an arbitrary `Link` header in the - * response. - */ - const graphqlSever = createServer((request, response) => { - /** @type {{ [key: string]: string }} */ - const responseHeaders = { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Headers": - "Origin, X-Requested-With, Content-Type, Accept", - "Content-Type": "application/json", - }; - - const { searchParams } = new URL( - /** @type {string} */ (request.url), - `http://${request.headers.host}`, - ); - - const linkHeader = searchParams.get("linkHeader"); - - if (linkHeader) responseHeaders.Link = linkHeader; - - response.writeHead(200, responseHeaders); - response.write( - JSON.stringify({ - data: { - a: markerA, - b: markerB, - }, - }), - ); - response.end(); - }); - - const { port: portGraphqlSever, close: closeGraphqlSever } = - await listen(graphqlSever); - - after(() => { - closeGraphqlSever(); - }); +// TODO: Re-enable these tests for Node.js v18 once the fix for this Node.js +// test runner bug is published in a v18 release: +// https://github.com/nodejs/node/issues/48845 The Node.js v18.19.0 release is +// scheduled for 2023-11-28: https://github.com/nodejs/Release/issues/737 +if (!process.version.startsWith("v18.")) + describe("Function `withGraphQLReact`.", { concurrency: true }, async () => { + const markerA = "MARKER_A"; + const markerB = "MARKER_B"; + + /** + * Dummy GraphQL server with a hardcoded response. The URL query string + * parameter `linkHeader` can be used to set an arbitrary `Link` header in + * the response. + */ + const graphqlSever = createServer((request, response) => { + /** @type {{ [key: string]: string }} */ + const responseHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": + "Origin, X-Requested-With, Content-Type, Accept", + "Content-Type": "application/json", + }; + + const { searchParams } = new URL( + /** @type {string} */ (request.url), + `http://${request.headers.host}`, + ); - process.env.NEXT_PUBLIC_GRAPHQL_URL = `http://localhost:${portGraphqlSever}`; + const linkHeader = searchParams.get("linkHeader"); - const nextProjectUrl = new URL( - "./test/fixtures/next-project/", - import.meta.url, - ); - const nextProjectPath = fileURLToPath(nextProjectUrl); - const buildOutput = await execFilePromise("npx", ["next", "build"], { - cwd: nextProjectPath, - }); + if (linkHeader) responseHeaders.Link = linkHeader; - ok(buildOutput.stdout.includes("Compiled successfully")); - - after(async () => { - await rm(new URL(".next", nextProjectUrl), { - force: true, - recursive: true, + response.writeHead(200, responseHeaders); + response.write( + JSON.stringify({ + data: { + a: markerA, + b: markerB, + }, + }), + ); + response.end(); }); - }); - describe("Served.", { concurrency: true }, async () => { - const { port: portNext, close: closeNext } = - await startNext(nextProjectPath); + const { port: portGraphqlSever, close: closeGraphqlSever } = + await listen(graphqlSever); after(() => { - closeNext(); + closeGraphqlSever(); }); - const browser = await puppeteer.launch({ - headless: "new", - }); + process.env.NEXT_PUBLIC_GRAPHQL_URL = `http://localhost:${portGraphqlSever}`; - after(async () => { - await browser.close(); + const nextProjectUrl = new URL( + "./test/fixtures/next-project/", + import.meta.url, + ); + const nextProjectPath = fileURLToPath(nextProjectUrl); + const buildOutput = await execFilePromise("npx", ["next", "build"], { + cwd: nextProjectPath, }); - const nextServerUrl = `http://localhost:${portNext}`; - - describe("Server side page loads.", { concurrency: true }, () => { - const linkHeaderGraphqlForwardable = - "; rel=dns-prefetch, ; rel=preconnect, ; rel=prefetch, ; rel=preload, ; rel=modulepreload, ; rel=prerender"; - const linkHeaderGraphQLUnforwardable = - "; rel=nonsense"; + ok(buildOutput.stdout.includes("Compiled successfully")); - it("Next.js original response `Link` header absent, GraphQL response `Link` header absent.", async () => { - const page = await browser.newPage(); - - try { - const response = await page.goto(nextServerUrl); - - ok(response); - ok(response.ok()); - strictEqual(response.headers().link, undefined); - ok(await page.$(`#${markerA}`)); - } finally { - await page.close(); - } + after(async () => { + await rm(new URL(".next", nextProjectUrl), { + force: true, + recursive: true, }); + }); - it("Next.js original response `Link` header absent, GraphQL response `Link` header parsable.", async () => { - const page = await browser.newPage(); + describe("Served.", { concurrency: true }, async () => { + const { port: portNext, close: closeNext } = + await startNext(nextProjectPath); - try { - const response = await page.goto( - `${nextServerUrl}?linkHeaderGraphql=${encodeURIComponent( - `${linkHeaderGraphqlForwardable}, ${linkHeaderGraphQLUnforwardable}`, - )}`, - ); - - ok(response); - ok(response.ok()); - strictEqual(response.headers().link, linkHeaderGraphqlForwardable); - ok(await page.$(`#${markerA}`)); - } finally { - await page.close(); - } + after(() => { + closeNext(); }); - it("Next.js original response `Link` header absent, GraphQL response `Link` header unparsable.", async () => { - const page = await browser.newPage(); - - try { - const response = await page.goto( - `${nextServerUrl}?linkHeaderGraphql=.`, - ); - - ok(response); - ok(response.ok()); - strictEqual(response.headers().link, undefined); - ok(await page.$(`#${markerA}`)); - } finally { - await page.close(); - } + const browser = await puppeteer.launch({ + headless: "new", }); - it("Next.js original response `Link` header parsable, GraphQL response `Link` header absent.", async () => { - const page = await browser.newPage(); + after(async () => { + await browser.close(); + }); - try { - const linkHeaderNext = - "; rel=preconnect, ; rel=nonsense"; - const response = await page.goto( - `${nextServerUrl}?linkHeaderNext=${encodeURIComponent( - linkHeaderNext, - )}`, - ); + const nextServerUrl = `http://localhost:${portNext}`; - ok(response); - ok(response.ok()); - strictEqual(response.headers().link, linkHeaderNext); - ok(await page.$(`#${markerA}`)); - } finally { - await page.close(); - } - }); + describe("Server side page loads.", { concurrency: true }, () => { + const linkHeaderGraphqlForwardable = + "; rel=dns-prefetch, ; rel=preconnect, ; rel=prefetch, ; rel=preload, ; rel=modulepreload, ; rel=prerender"; + const linkHeaderGraphQLUnforwardable = + "; rel=nonsense"; - it("Next.js original response `Link` header parsable, GraphQL response `Link` header parsable, different.", async () => { - const page = await browser.newPage(); + it("Next.js original response `Link` header absent, GraphQL response `Link` header absent.", async () => { + const page = await browser.newPage(); - try { - const linkHeaderNext = - "; rel=preconnect, ; rel=nonsense"; - const response = await page.goto( - `${nextServerUrl}?linkHeaderNext=${encodeURIComponent( - linkHeaderNext, - )}&linkHeaderGraphql=${encodeURIComponent( - `${linkHeaderGraphqlForwardable}, ${linkHeaderGraphQLUnforwardable}`, - )}`, - ); + try { + const response = await page.goto(nextServerUrl); - ok(response); - ok(response.ok()); - strictEqual( - response.headers().link, - `${linkHeaderNext}, ${linkHeaderGraphqlForwardable}`, - ); - ok(await page.$(`#${markerA}`)); - } finally { - await page.close(); - } - }); + ok(response); + ok(response.ok()); + strictEqual(response.headers().link, undefined); + ok(await page.$(`#${markerA}`)); + } finally { + await page.close(); + } + }); - it("Next.js original response `Link` header parsable, GraphQL response `Link` header parsable, similar.", async () => { - const page = await browser.newPage(); + it("Next.js original response `Link` header absent, GraphQL response `Link` header parsable.", async () => { + const page = await browser.newPage(); + + try { + const response = await page.goto( + `${nextServerUrl}?linkHeaderGraphql=${encodeURIComponent( + `${linkHeaderGraphqlForwardable}, ${linkHeaderGraphQLUnforwardable}`, + )}`, + ); + + ok(response); + ok(response.ok()); + strictEqual(response.headers().link, linkHeaderGraphqlForwardable); + ok(await page.$(`#${markerA}`)); + } finally { + await page.close(); + } + }); - try { - const linkHeader = - "; rel=preconnect, ; rel=nonsense"; - const linkHeaderEncoded = encodeURIComponent(linkHeader); - const response = await page.goto( - `${nextServerUrl}?linkHeaderNext=${linkHeaderEncoded}&linkHeaderGraphql=${linkHeaderEncoded}`, - ); + it("Next.js original response `Link` header absent, GraphQL response `Link` header unparsable.", async () => { + const page = await browser.newPage(); + + try { + const response = await page.goto( + `${nextServerUrl}?linkHeaderGraphql=.`, + ); + + ok(response); + ok(response.ok()); + strictEqual(response.headers().link, undefined); + ok(await page.$(`#${markerA}`)); + } finally { + await page.close(); + } + }); - ok(response); - ok(response.ok()); - strictEqual(response.headers().link, linkHeader); - ok(await page.$(`#${markerA}`)); - } finally { - await page.close(); - } - }); + it("Next.js original response `Link` header parsable, GraphQL response `Link` header absent.", async () => { + const page = await browser.newPage(); + + try { + const linkHeaderNext = + "; rel=preconnect, ; rel=nonsense"; + const response = await page.goto( + `${nextServerUrl}?linkHeaderNext=${encodeURIComponent( + linkHeaderNext, + )}`, + ); + + ok(response); + ok(response.ok()); + strictEqual(response.headers().link, linkHeaderNext); + ok(await page.$(`#${markerA}`)); + } finally { + await page.close(); + } + }); - it("Next.js original response `Link` header parsable, GraphQL response `Link` header unparsable.", async () => { - const page = await browser.newPage(); + it("Next.js original response `Link` header parsable, GraphQL response `Link` header parsable, different.", async () => { + const page = await browser.newPage(); + + try { + const linkHeaderNext = + "; rel=preconnect, ; rel=nonsense"; + const response = await page.goto( + `${nextServerUrl}?linkHeaderNext=${encodeURIComponent( + linkHeaderNext, + )}&linkHeaderGraphql=${encodeURIComponent( + `${linkHeaderGraphqlForwardable}, ${linkHeaderGraphQLUnforwardable}`, + )}`, + ); + + ok(response); + ok(response.ok()); + strictEqual( + response.headers().link, + `${linkHeaderNext}, ${linkHeaderGraphqlForwardable}`, + ); + ok(await page.$(`#${markerA}`)); + } finally { + await page.close(); + } + }); - try { - const linkHeaderNext = - "; rel=preconnect, ; rel=nonsense"; - const response = await page.goto( - `${nextServerUrl}?linkHeaderNext=${encodeURIComponent( - linkHeaderNext, - )}&linkHeaderGraphql=.`, - ); + it("Next.js original response `Link` header parsable, GraphQL response `Link` header parsable, similar.", async () => { + const page = await browser.newPage(); + + try { + const linkHeader = + "; rel=preconnect, ; rel=nonsense"; + const linkHeaderEncoded = encodeURIComponent(linkHeader); + const response = await page.goto( + `${nextServerUrl}?linkHeaderNext=${linkHeaderEncoded}&linkHeaderGraphql=${linkHeaderEncoded}`, + ); + + ok(response); + ok(response.ok()); + strictEqual(response.headers().link, linkHeader); + ok(await page.$(`#${markerA}`)); + } finally { + await page.close(); + } + }); - ok(response); - ok(response.ok()); - strictEqual(response.headers().link, linkHeaderNext); - ok(await page.$(`#${markerA}`)); - } finally { - await page.close(); - } - }); + it("Next.js original response `Link` header parsable, GraphQL response `Link` header unparsable.", async () => { + const page = await browser.newPage(); + + try { + const linkHeaderNext = + "; rel=preconnect, ; rel=nonsense"; + const response = await page.goto( + `${nextServerUrl}?linkHeaderNext=${encodeURIComponent( + linkHeaderNext, + )}&linkHeaderGraphql=.`, + ); + + ok(response); + ok(response.ok()); + strictEqual(response.headers().link, linkHeaderNext); + ok(await page.$(`#${markerA}`)); + } finally { + await page.close(); + } + }); - it("Next.js original response `Link` header unparsable, GraphQL response `Link` header absent.", async () => { - const page = await browser.newPage(); + it("Next.js original response `Link` header unparsable, GraphQL response `Link` header absent.", async () => { + const page = await browser.newPage(); + + try { + const linkHeaderNext = "."; + const response = await page.goto( + `${nextServerUrl}?linkHeaderNext=${encodeURIComponent( + linkHeaderNext, + )}`, + ); + + ok(response); + ok(response.ok()); + strictEqual(response.headers().link, linkHeaderNext); + ok(await page.$(`#${markerA}`)); + } finally { + await page.close(); + } + }); - try { - const linkHeaderNext = "."; - const response = await page.goto( - `${nextServerUrl}?linkHeaderNext=${encodeURIComponent( - linkHeaderNext, - )}`, - ); + it("Next.js original response `Link` header unparsable, GraphQL response `Link` header parsable.", async () => { + const page = await browser.newPage(); + + try { + const response = await page.goto( + `${nextServerUrl}?linkHeaderNext=.&linkHeaderGraphql=${encodeURIComponent( + `${linkHeaderGraphqlForwardable}, ${linkHeaderGraphQLUnforwardable}`, + )}`, + ); + + ok(response); + ok(response.ok()); + strictEqual(response.headers().link, linkHeaderGraphqlForwardable); + ok(await page.$(`#${markerA}`)); + } finally { + await page.close(); + } + }); - ok(response); - ok(response.ok()); - strictEqual(response.headers().link, linkHeaderNext); - ok(await page.$(`#${markerA}`)); - } finally { - await page.close(); - } + it("Next.js original response `Link` header unparsable, GraphQL response `Link` header unparsable.", async () => { + const page = await browser.newPage(); + + try { + const response = await page.goto( + // The unparsable values have to be different so the can be + // separately identified in the final response. + `${nextServerUrl}?linkHeaderNext=.&linkHeaderGraphql=-`, + ); + + ok(response); + ok(response.ok()); + strictEqual( + response.headers().link, + // Because there wasn’t a parsable `Link` header to forward from + // the GraphQL response, the unparsable original Next.js one + // shouldn’t have been replaced in the final response. + ".", + ); + ok(await page.$(`#${markerA}`)); + } finally { + await page.close(); + } + }); }); - it("Next.js original response `Link` header unparsable, GraphQL response `Link` header parsable.", async () => { + it("Client side page load.", async () => { const page = await browser.newPage(); try { - const response = await page.goto( - `${nextServerUrl}?linkHeaderNext=.&linkHeaderGraphql=${encodeURIComponent( - `${linkHeaderGraphqlForwardable}, ${linkHeaderGraphQLUnforwardable}`, - )}`, - ); + const response = await page.goto(nextServerUrl, { + // Wait until the JS has loaded and the React app has mounted. + waitUntil: "networkidle0", + }); ok(response); ok(response.ok()); - strictEqual(response.headers().link, linkHeaderGraphqlForwardable); - ok(await page.$(`#${markerA}`)); - } finally { - await page.close(); - } - }); - - it("Next.js original response `Link` header unparsable, GraphQL response `Link` header unparsable.", async () => { - const page = await browser.newPage(); - try { - const response = await page.goto( - // The unparsable values have to be different so the can be - // separately identified in the final response. - `${nextServerUrl}?linkHeaderNext=.&linkHeaderGraphql=-`, + // Simulate fast 3G network conditions for just this headless browser + // page, so when the second page is navigated to client side, the + // page’s GraphQL query loading state can render and be asserted. + await page.emulateNetworkConditions( + PredefinedNetworkConditions["Fast 3G"], ); - ok(response); - ok(response.ok()); - strictEqual( - response.headers().link, - // Because there wasn’t a parsable `Link` header to forward from - // the GraphQL response, the unparsable original Next.js one - // shouldn’t have been replaced in the final response. - ".", - ); - ok(await page.$(`#${markerA}`)); + await Promise.all([ + page.click('[href="/second"]'), + page.waitForNavigation(), + page.waitForSelector("#loading", { timeout: 10000 }), + page.waitForSelector(`#${markerB}`, { timeout: 20000 }), + ]); } finally { await page.close(); } }); }); - it("Client side page load.", async () => { - const page = await browser.newPage(); + it("Static HTML export.", async () => { + const nextExportOutput = await execFilePromise("npx", ["next", "build"], { + cwd: nextProjectPath, + env: { + ...process.env, + TEST_FIXTURE_NEXT_CONFIG_OUTPUT: "export", + }, + }); - try { - const response = await page.goto(nextServerUrl, { - // Wait until the JS has loaded and the React app has mounted. - waitUntil: "networkidle0", - }); + ok(nextExportOutput.stdout.includes("Compiled successfully")); - ok(response); - ok(response.ok()); + const nextExportOutDirUrl = new URL("out/", nextProjectUrl); - // Simulate fast 3G network conditions for just this headless browser - // page, so when the second page is navigated to client side, the page’s - // GraphQL query loading state can render and be asserted. - await page.emulateNetworkConditions( - PredefinedNetworkConditions["Fast 3G"], + try { + const html = await readFile( + new URL(`index.html`, nextExportOutDirUrl), + "utf8", ); - await Promise.all([ - page.click('[href="/second"]'), - page.waitForNavigation(), - page.waitForSelector("#loading", { timeout: 10000 }), - page.waitForSelector(`#${markerB}`, { timeout: 20000 }), - ]); + ok(html.includes(`id="${markerA}"`)); } finally { - await page.close(); + await rm(nextExportOutDirUrl, { + force: true, + recursive: true, + }); } }); }); - - it("Static HTML export.", async () => { - const nextExportOutput = await execFilePromise("npx", ["next", "build"], { - cwd: nextProjectPath, - env: { - ...process.env, - TEST_FIXTURE_NEXT_CONFIG_OUTPUT: "export", - }, - }); - - ok(nextExportOutput.stdout.includes("Compiled successfully")); - - const nextExportOutDirUrl = new URL("out/", nextProjectUrl); - - try { - const html = await readFile( - new URL(`index.html`, nextExportOutDirUrl), - "utf8", - ); - - ok(html.includes(`id="${markerA}"`)); - } finally { - await rm(nextExportOutDirUrl, { - force: true, - recursive: true, - }); - } - }); -});