Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add integration tests for request structures #4829

Merged
merged 7 commits into from Dec 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/mighty-grapes-laugh.md
@@ -0,0 +1,6 @@
---
"@remix-run/server-runtime": patch
---

- Ensure `loader` `request`'s proxy headers on document `action` submissions
- Fix an issue where loader `request`'s reflected `method: "POST"` on document submissions
5 changes: 5 additions & 0 deletions .changeset/sharp-paws-nail.md
@@ -0,0 +1,5 @@
---
"@remix-run/server-runtime": patch
---

Fix error boundary tracking for multiple errors bubbling to the same boundary
40 changes: 39 additions & 1 deletion integration/catch-boundary-test.ts
Expand Up @@ -67,6 +67,9 @@ test.describe("CatchBoundary", () => {
<Link to="${HAS_BOUNDARY_LOADER}">
${HAS_BOUNDARY_LOADER}
</Link>
<Link to="${HAS_BOUNDARY_LOADER}/child">
${HAS_BOUNDARY_LOADER}/child
</Link>
<Link to="${NO_BOUNDARY_LOADER}">
${NO_BOUNDARY_LOADER}
</Link>
Expand Down Expand Up @@ -111,11 +114,27 @@ test.describe("CatchBoundary", () => {
`,

[`app/routes${HAS_BOUNDARY_LOADER}.jsx`]: js`
import { useCatch } from '@remix-run/react';
export function loader() {
throw new Response("", { status: 401 })
}
export function CatchBoundary() {
return <div id="boundary-loader">${OWN_BOUNDARY_TEXT}</div>
let caught = useCatch();
return (
<>
<div id="boundary-loader">${OWN_BOUNDARY_TEXT}</div>
<pre id="status">{caught.status}</pre>
</>
);
}
export default function Index() {
return <div/>
}
`,

[`app/routes${HAS_BOUNDARY_LOADER}/child.jsx`]: js`
export function loader() {
throw new Response("", { status: 404 })
}
export default function Index() {
return <div/>
Expand Down Expand Up @@ -292,4 +311,23 @@ test.describe("CatchBoundary", () => {
expect(await app.getHtml("#child-catch")).toMatch("400");
expect(await app.getHtml("#child-catch")).toMatch("Caught!");
});

test("prefers parent catch when child loader also bubbles, document request", async () => {
let res = await fixture.requestDocument(`${HAS_BOUNDARY_LOADER}/child`);
expect(res.status).toBe(401);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parent throws 401, child throws 404

let text = await res.text();
expect(text).toMatch(OWN_BOUNDARY_TEXT);
expect(text).toMatch('<pre id="status">401</pre>');
});

test("prefers parent catch when child loader also bubbles, client transition", async ({
page,
}) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/");
await app.clickLink(`${HAS_BOUNDARY_LOADER}/child`);
await page.waitForSelector("#boundary-loader");
expect(await app.getHtml("#boundary-loader")).toMatch(OWN_BOUNDARY_TEXT);
expect(await app.getHtml("#status")).toMatch("401");
});
});
162 changes: 162 additions & 0 deletions integration/request-test.ts
@@ -0,0 +1,162 @@
import { test, expect } from "@playwright/test";

import { PlaywrightFixture } from "./helpers/playwright-fixture";
import type { Fixture, AppFixture } from "./helpers/create-fixture";
import { createAppFixture, createFixture, js } from "./helpers/create-fixture";

let fixture: Fixture;
let appFixture: AppFixture;

test.beforeAll(async () => {
fixture = await createFixture({
files: {
"app/routes/index.jsx": js`
import { json } from "@remix-run/node";
import { Form, useLoaderData, useActionData } from "@remix-run/react";
export async function loader({ request }) {
let text = (await request.text());
return json({
method: request.method,
url: request.url,
headers: Object.fromEntries(request.headers.entries()),
text,
});
}
export async function action({ request }) {
let text = (await request.text());
return json({
method: request.method,
url: request.url,
headers: Object.fromEntries(request.headers.entries()),
text,
});
}
export default function Index() {
let loaderData = useLoaderData();
let actionData = useActionData();
return (
<div>
<button id="set-cookie" onClick={() => {
document.cookie = 'cookie=nomnom; path=/';
}}>
Set Cookie
</button>
<Form method="get" reloadDocument>
<button type="submit" id="submit-get-ssr" name="type" value="ssr" />
</Form>
<Form method="get">
<button type="submit" id="submit-get-csr" name="type" value="csr" />
</Form>
<Form method="post" reloadDocument>
<button type="submit" id="submit-post-ssr" name="type" value="ssr" />
</Form>
<Form method="post">
<button type="submit" id="submit-post-csr" name="type" value="csr" />
</Form>
<pre id="loader-data">{JSON.stringify(loaderData)}</pre>
{actionData ?
<pre id="action-data">{JSON.stringify(actionData)}</pre> :
null}
</div>
)
}
`,
},
});

appFixture = await createAppFixture(fixture);
});

test.afterAll(() => appFixture.close());

test("loader request on SSR GET requests", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/");
await app.clickElement("#set-cookie");

let loaderData = JSON.parse(await page.locator("#loader-data").innerHTML());
expect(loaderData.method).toEqual("GET");
expect(loaderData.url).toMatch(/^http:\/\/localhost:\d+\/$/);
expect(loaderData.headers.cookie).toEqual(undefined);
expect(loaderData.text).toEqual("");

await app.clickElement("#submit-get-ssr");

loaderData = JSON.parse(await page.locator("#loader-data").innerHTML());
expect(loaderData.method).toEqual("GET");
expect(loaderData.url).toMatch(/^http:\/\/localhost:\d+\/\?type=ssr$/);
expect(loaderData.headers.cookie).toEqual("cookie=nomnom");
expect(loaderData.text).toEqual("");
});

test("loader request on CSR GET requests", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/");
await app.clickElement("#set-cookie");

let loaderData = JSON.parse(await page.locator("#loader-data").innerHTML());
expect(loaderData.method).toEqual("GET");
expect(loaderData.url).toMatch(/^http:\/\/localhost:\d+\/$/);
expect(loaderData.headers.cookie).toEqual(undefined);
expect(loaderData.text).toEqual("");

await app.clickElement("#submit-get-csr");

loaderData = JSON.parse(await page.locator("#loader-data").innerHTML());
expect(loaderData.method).toEqual("GET");
expect(loaderData.url).toMatch(/^http:\/\/localhost:\d+\/\?type=csr$/);
expect(loaderData.headers.cookie).toEqual("cookie=nomnom");
expect(loaderData.text).toEqual("");
});

test("action + loader requests SSR POST requests", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/");
await app.clickElement("#set-cookie");

let loaderData = JSON.parse(await page.locator("#loader-data").innerHTML());
expect(loaderData.method).toEqual("GET");
expect(loaderData.url).toMatch(/^http:\/\/localhost:\d+\/$/);
expect(loaderData.headers.cookie).toEqual(undefined);
expect(loaderData.text).toEqual("");

await app.clickElement("#submit-post-ssr");

let actionData = JSON.parse(await page.locator("#action-data").innerHTML());
expect(actionData.method).toEqual("POST");
expect(actionData.url).toMatch(/^http:\/\/localhost:\d+\/$/);
expect(actionData.headers.cookie).toEqual("cookie=nomnom");
expect(actionData.text).toEqual("type=ssr");

loaderData = JSON.parse(await page.locator("#loader-data").innerHTML());
expect(loaderData.method).toEqual("GET");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Loaders receive method:GET on SSR "revalidations" now. This fixes a bug in Remix 1.7.6 where the loader request was POST but without a body, and thus made for a different DX if you did a SSR/CSR submissions since request.method would be different in the loader.

expect(loaderData.url).toMatch(/^http:\/\/localhost:\d+\/$/);
expect(loaderData.headers.cookie).toEqual("cookie=nomnom");
expect(loaderData.text).toEqual("");
});

test("action + loader requests on CSR POST requests", async ({ page }) => {
let app = new PlaywrightFixture(appFixture, page);
await app.goto("/");
await app.clickElement("#set-cookie");

let loaderData = JSON.parse(await page.locator("#loader-data").innerHTML());
expect(loaderData.method).toEqual("GET");
expect(loaderData.url).toMatch(/^http:\/\/localhost:\d+\/$/);
expect(loaderData.headers.cookie).toEqual(undefined);
expect(loaderData.text).toEqual("");

await app.clickElement("#submit-post-csr");

let actionData = JSON.parse(await page.locator("#action-data").innerHTML());
expect(actionData.method).toEqual("POST");
expect(actionData.url).toMatch(/^http:\/\/localhost:\d+\/$/);
expect(actionData.headers.cookie).toEqual("cookie=nomnom");
expect(actionData.text).toEqual("type=csr");

loaderData = JSON.parse(await page.locator("#loader-data").innerHTML());
expect(loaderData.method).toEqual("GET");
expect(loaderData.url).toMatch(/^http:\/\/localhost:\d+\/$/);
expect(loaderData.headers.cookie).toEqual("cookie=nomnom");
expect(loaderData.text).toEqual("");
Comment on lines +157 to +161
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

During SPA submissions, the loader is called through separate revalidation GET requests, so the assertions here differ on the method.

I think this all makes sense given how Remix is executing loaders differently in MPA/SPA calls, even if it introduces a slight different between them from a DX standpoint. Users shouldn't be branching on request.method in loaders anyway?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic is implemented in the router here: remix-run/react-router#9721

});
4 changes: 2 additions & 2 deletions packages/remix-react/package.json
Expand Up @@ -16,8 +16,8 @@
"typings": "dist/index.d.ts",
"module": "dist/esm/index.js",
"dependencies": {
"@remix-run/router": "1.0.5",
"react-router-dom": "6.4.5"
"@remix-run/router": "1.1.0-pre.0",
"react-router-dom": "6.5.0-pre.0"
},
"devDependencies": {
"@remix-run/server-runtime": "1.8.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/remix-server-runtime/package.json
Expand Up @@ -16,7 +16,7 @@
"typings": "dist/index.d.ts",
"module": "dist/esm/index.js",
"dependencies": {
"@remix-run/router": "1.0.5",
"@remix-run/router": "1.1.0-pre.0",
"@types/cookie": "^0.4.0",
"@web3-storage/multipart-parser": "^1.0.0",
"cookie": "^0.4.1",
Expand Down
27 changes: 16 additions & 11 deletions yarn.lock
Expand Up @@ -2174,6 +2174,11 @@
resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.0.5.tgz#d5c65626add4c3c185a89aa5bd38b1e42daec075"
integrity sha512-my0Mycd+jruq/1lQuO5LBB6WTlL/e8DTCYWp44DfMTDcXz8DcTlgF0ISaLsGewt+ctHN+yA8xMq3q/N7uWJPug==

"@remix-run/router@1.1.0-pre.0":
version "1.1.0-pre.0"
resolved "https://registry.npmjs.org/@remix-run/router/-/router-1.1.0-pre.0.tgz#c33db6bd13c6d3847f73f618008ebecdaec237c7"
integrity sha512-FFa1aGAYUZm+PdOO2a7r/4gxLqSIOq1Qav3g6wwRgWj28PNyO7CjdUNER4O4DSKVVyob21mhZHvTe25z+2xnZw==

"@remix-run/web-blob@^3.0.3", "@remix-run/web-blob@^3.0.4":
version "3.0.4"
resolved "https://registry.npmjs.org/@remix-run/web-blob/-/web-blob-3.0.4.tgz"
Expand Down Expand Up @@ -10722,20 +10727,20 @@ react-is@^17.0.1:
resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==

react-router-dom@6.4.5:
version "6.4.5"
resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.4.5.tgz#4fdb12efef4f3848c693a76afbeaed1f6ca02047"
integrity sha512-a7HsgikBR0wNfroBHcZUCd9+mLRqZS8R5U1Z1mzLWxFXEkUT3vR1XXmSIVoVpxVX8Bar0nQYYYc9Yipq8dWwAA==
react-router-dom@6.5.0-pre.0:
version "6.5.0-pre.0"
resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.5.0-pre.0.tgz#73dc3861f43b1bc83a854bd5d3d3ab4d7beb1710"
integrity sha512-Hq8lhsAqLH4LDewj6OeDCiEel5RP6El1DsxohxwgOctt+xq4Q6kGiiuVnLA+eHl2wXFCEF8ZSK/kOddGKfLWYA==
dependencies:
"@remix-run/router" "1.0.5"
react-router "6.4.5"
"@remix-run/router" "1.1.0-pre.0"
react-router "6.5.0-pre.0"

react-router@6.4.5:
version "6.4.5"
resolved "https://registry.npmjs.org/react-router/-/react-router-6.4.5.tgz#73f382af2c8b9a86d74e761a7c5fc3ce7cb0024d"
integrity sha512-1RQJ8bM70YEumHIlNUYc6mFfUDoWa5EgPDenK/fq0bxD8DYpQUi/S6Zoft+9DBrh2xmtg92N5HMAJgGWDhKJ5Q==
react-router@6.5.0-pre.0:
version "6.5.0-pre.0"
resolved "https://registry.npmjs.org/react-router/-/react-router-6.5.0-pre.0.tgz#513b2fe858736443082a33a7a9d2f728b2bd7fe4"
integrity sha512-JqRGYSGzySjcqVMkgCa+1i3zvXiH2IHUfEPac71sJn5ArH5tvR5GmraWUn+D5t7Yzq2dz11is4u2SzHiT0pGdg==
dependencies:
"@remix-run/router" "1.0.5"
"@remix-run/router" "1.1.0-pre.0"

react@^18.2.0:
version "18.2.0"
Expand Down