diff --git a/.changeset/remix-testing-meta-links.md b/.changeset/remix-testing-meta-links.md new file mode 100644 index 00000000000..3613d5b55d2 --- /dev/null +++ b/.changeset/remix-testing-meta-links.md @@ -0,0 +1,6 @@ +--- +"@remix-run/testing": minor +--- + +* `unstable_createRemixStub` now supports adding `meta`/`links` functions on stubbed Remix routes +* ⚠️ `unstable_createRemixStub` no longer supports the `element`/`errorElement` properties on routes. You must use `Component`/`ErrorBoundary` to match what you would export from a Remix route module. diff --git a/integration/meta-test.ts b/integration/meta-test.ts deleted file mode 100644 index 40e1321741f..00000000000 --- a/integration/meta-test.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { test, expect } from "@playwright/test"; - -import { - createAppFixture, - createFixture, - js, -} from "./helpers/create-fixture.js"; -import type { Fixture, AppFixture } from "./helpers/create-fixture.js"; -import { PlaywrightFixture } from "./helpers/playwright-fixture.js"; - -test.describe("meta", () => { - let fixture: Fixture; - let appFixture: AppFixture; - - // disable JS for all tests in this file - // to only disable them for some, add another test.describe() - // and move the following line there - test.use({ javaScriptEnabled: false }); - - test.beforeAll(async () => { - fixture = await createFixture({ - config: { - ignoredRouteFiles: ["**/.*"], - }, - files: { - "app/root.tsx": js` - import { json } from "@remix-run/node"; - import { Meta, Links, Outlet, Scripts } from "@remix-run/react"; - - export const loader = async () => - json({ - description: "This is a meta page", - title: "Meta Page", - }); - - export const meta = ({ data }) => [ - { charSet: "utf-8" }, - { name: "description", content: data.description }, - { property: "og:image", content: "https://picsum.photos/200/200" }, - { property: "og:type", content: data.contentType }, // undefined - { title: data.title }, - ]; - - export default function Root() { - return ( - - - - - - - - - - - ); - } - `, - - "app/routes/_index.tsx": js` - export const meta = ({ data, matches }) => - matches.flatMap((match) => match.meta); - - export default function Index() { - return
This is the index file
; - } - `, - - "app/routes/no-meta-export.tsx": js` - export default function NoMetaExport() { - return
Parent meta here!
; - } - `, - - "app/routes/empty-meta-function.tsx": js` - export const meta = () => []; - export default function EmptyMetaFunction() { - return
No meta here!
; - } - `, - - "app/routes/authors.$authorId.tsx": js` - import { json } from "@remix-run/node"; - - export async function loader({ params }) { - return json({ - author: { - id: params.authorId, - name: "Sonny Day", - address: { - streetAddress: "123 Sunset Cliffs Blvd", - city: "San Diego", - state: "CA", - zip: "92107", - }, - emails: [ - "sonnyday@fancymail.com", - "surfergal@veryprofessional.org", - ], - }, - }); - } - - export function meta({ data }) { - let { author } = data; - return [ - { title: data.name + " Profile" }, - { - tagName: "link", - rel: "canonical", - href: "https://website.com/authors/" + author.id, - }, - { - "script:ld+json": { - "@context": "http://schema.org", - "@type": "Person", - "name": author.name, - "address": { - "@type": "PostalAddress", - "streetAddress": author.address.streetAddress, - "addressLocality": author.address.city, - "addressRegion": author.address.state, - "postalCode": author.address.zip, - }, - "email": author.emails, - }, - }, - ]; - } - export default function AuthorBio() { - return
Bio here!
; - } - `, - - "app/routes/music.tsx": js` - export function meta({ data, matches }) { - let rootModule = matches.find(match => match.id === "root"); - let rootCharSet = rootModule.meta.find(meta => meta.charSet); - return [ - rootCharSet, - { title: "What's My Age Again?" }, - { property: "og:type", content: "music.song" }, - { property: "music:musician", content: "https://www.blink182.com/" }, - { property: "music:duration", content: 182 }, - ]; - } - - export default function Music() { - return

Music

; - } - `, - - "app/routes/error.tsx": js` - import { Link, useRouteError } from '@remix-run/react' - - export function loader() { - throw new Error('lol oops') - } - - export const meta = (args) => { - return [{ title: args.error ? "Oops!" : "Home"}] - } - - export default function Error() { - return

Error

- } - - export function ErrorBoundary() { - return

Error boundary

- } - `, - }, - }); - appFixture = await createAppFixture(fixture); - }); - - test.afterAll(() => { - appFixture.close(); - }); - - test("no meta export renders meta from nearest route meta in the tree", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/no-meta-export"); - expect(await app.getHtml('meta[name="description"]')).toBeTruthy(); - }); - - test("empty meta array does not render a tag", async ({ page }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/empty-meta-function"); - await expect(app.getHtml("title")).rejects.toThrowError( - 'No element matches selector "title"' - ); - }); - - test("meta from `matches` renders meta tags", async ({ page }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/music"); - expect(await app.getHtml('meta[charset="utf-8"]')).toBeTruthy(); - }); - - test("{ charSet } adds a ", async ({ page }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - expect(await app.getHtml('meta[charset="utf-8"]')).toBeTruthy(); - }); - - test("{ title } adds a ", async ({ page }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - expect(await app.getHtml("title")).toBeTruthy(); - }); - - test("{ property: 'og:*', content: '*' } adds a <meta property='og:*' />", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/"); - expect(await app.getHtml('meta[property="og:image"]')).toBeTruthy(); - }); - - test("{ 'script:ld+json': {} } adds a <script type='application/ld+json' />", async ({ - page, - }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/authors/1"); - let scriptTag = await app.getHtml('script[type="application/ld+json"]'); - let scriptContents = scriptTag - .replace('<script type="application/ld+json">', "") - .replace("</script>", "") - .trim(); - - expect(JSON.parse(scriptContents)).toEqual({ - "@context": "http://schema.org", - "@type": "Person", - name: "Sonny Day", - address: { - "@type": "PostalAddress", - streetAddress: "123 Sunset Cliffs Blvd", - addressLocality: "San Diego", - addressRegion: "CA", - postalCode: "92107", - }, - email: ["sonnyday@fancymail.com", "surfergal@veryprofessional.org"], - }); - }); - - test("{ tagName: 'link' } adds a <link />", async ({ page }) => { - let app = new PlaywrightFixture(appFixture, page); - await app.goto("/authors/1"); - expect(await app.getHtml('link[rel="canonical"]')).toBeTruthy(); - }); - - test("loader errors are passed to meta", async ({ page }) => { - let restoreErrors = hideErrors(); - - new PlaywrightFixture(appFixture, page); - let response = await fixture.requestDocument("/error"); - expect(await response.text()).toMatch("<title>Oops!"); - - restoreErrors(); - }); -}); - -function hideErrors() { - let oldConsoleError: any; - oldConsoleError = console.error; - console.error = () => {}; - return () => { - console.error = oldConsoleError; - }; -} diff --git a/packages/remix-react/__tests__/integration/meta-test.tsx b/packages/remix-react/__tests__/integration/meta-test.tsx new file mode 100644 index 00000000000..39ce5e06aca --- /dev/null +++ b/packages/remix-react/__tests__/integration/meta-test.tsx @@ -0,0 +1,317 @@ +import * as React from "react"; +import { Meta, Outlet } from "@remix-run/react"; +import { unstable_createRemixStub } from "@remix-run/testing"; +import { prettyDOM, render } from "@testing-library/react"; + +const getHtml = (c: HTMLElement) => + prettyDOM(c, undefined, { highlight: false }); + +describe("meta", () => { + it("no meta export renders meta from nearest route meta in the tree", () => { + let RemixStub = unstable_createRemixStub([ + { + id: "root", + path: "/", + meta: ({ data }) => [ + { name: "description", content: data.description }, + { title: data.title }, + ], + Component() { + return ( + <> + + + + ); + }, + children: [ + { + index: true, + Component() { + return
Parent meta here!
; + }, + }, + ], + }, + ]); + + let { container } = render( + + ); + + expect(getHtml(container)).toMatchInlineSnapshot(` + "
+ + + Meta Page + +
+ Parent meta here! +
+
" + `); + }); + + it("empty meta array does not render a tag", () => { + let RemixStub = unstable_createRemixStub([ + { + path: "/", + meta: () => [], + Component() { + return ( + <> + +

No meta here!

+ + ); + }, + }, + ]); + + let { container } = render(); + + expect(getHtml(container)).toMatchInlineSnapshot(` + "
+

+ No meta here! +

+
" + `); + }); + + it("meta from `matches` renders meta tags", () => { + let RemixStub = unstable_createRemixStub([ + { + id: "root", + path: "/", + meta: () => [{ charSet: "utf-8" }], + Component() { + return ( + <> + + + + ); + }, + children: [ + { + index: true, + meta({ matches }) { + let rootModule = matches.find((match) => match.id === "root"); + // @ts-expect-error + let rootCharSet = rootModule?.meta.find((meta) => meta.charSet); + return [rootCharSet, { title: "Child title" }]; + }, + Component() { + return

Matches Meta

; + }, + }, + ], + }, + ]); + + let { container } = render(); + + expect(getHtml(container)).toMatchInlineSnapshot(` + "
+ + + Child title + +

+ Matches Meta +

+
" + `); + }); + + it("{ charSet } adds a ", () => { + let RemixStub = unstable_createRemixStub([ + { + path: "/", + meta: () => [{ charSet: "utf-8" }], + Component: Meta, + }, + ]); + + let { container } = render(); + + expect(getHtml(container)).toMatchInlineSnapshot(` + "
+ +
" + `); + }); + + it("{ title } adds a ", () => { + let RemixStub = unstable_createRemixStub([ + { + path: "/", + meta: () => [{ title: "Document Title" }], + Component: Meta, + }, + ]); + + let { container } = render(<RemixStub />); + + expect(getHtml(container)).toMatchInlineSnapshot(` + "<div> + <title> + Document Title + + " + `); + }); + + it("{ property: 'og:*', content: '*' } adds a ", () => { + let RemixStub = unstable_createRemixStub([ + { + path: "/", + meta: () => [ + { property: "og:image", content: "https://picsum.photos/200/200" }, + { property: "og:type", content: undefined }, + ], + Component: Meta, + }, + ]); + + let { container } = render(); + expect(getHtml(container)).toMatchInlineSnapshot(` + "
+ + +
" + `); + }); + + it("{ 'script:ld+json': {} } adds a