|
| 1 | +import { describe, expect, it, vi } from "vitest"; |
| 2 | +import type { Metadata } from "next"; |
| 3 | +import { SITE_OPEN_GRAPH } from "@/lib/siteMetadata"; |
| 4 | +import { guideMetadata } from "@/components/guide/meta"; |
| 5 | +import { PAGES, pagePath } from "@/components/guide/pages"; |
| 6 | +import { builds } from "@/components/sections/InsideGlobe/builds"; |
| 7 | +import type { GuideScreen } from "@/components/guide/store"; |
| 8 | + |
| 9 | +// A link pasted into a chat shows the page's own title, address and a |
| 10 | +// picture. Next merges metadata one key deep, so the root layout's openGraph |
| 11 | +// and twitter decide what every page without its own carries, and a page that |
| 12 | +// sets openGraph keeps the picture only by spreading SITE_OPEN_GRAPH in. |
| 13 | + |
| 14 | +// The pages render nothing here: only their metadata is read. |
| 15 | +vi.mock("@/components/HomeView", () => ({ default: () => null })); |
| 16 | +vi.mock("next/font/google", () => { |
| 17 | + const font = () => ({ variable: "", className: "" }); |
| 18 | + return { Inter: font, JetBrains_Mono: font, DM_Sans: font, Silkscreen: font, Newsreader: font }; |
| 19 | +}); |
| 20 | + |
| 21 | +type OpenGraph = NonNullable<Metadata["openGraph"]>; |
| 22 | + |
| 23 | +function urlOf(og: OpenGraph): string { |
| 24 | + return String(og.url ?? ""); |
| 25 | +} |
| 26 | + |
| 27 | +describe("site metadata", () => { |
| 28 | + it("leaves each page's title and address to the page, in the root layout", async () => { |
| 29 | + const { metadata } = await import("@/app/layout"); |
| 30 | + const og = metadata.openGraph as OpenGraph; |
| 31 | + expect(og).toEqual(SITE_OPEN_GRAPH); |
| 32 | + expect(og).not.toHaveProperty("title"); |
| 33 | + expect(og).not.toHaveProperty("url"); |
| 34 | + // Next copies a page's og:title into twitter:title only when no layout |
| 35 | + // set one. |
| 36 | + expect(metadata.twitter).toEqual({ card: "summary_large_image" }); |
| 37 | + }); |
| 38 | + |
| 39 | + it.each([ |
| 40 | + ["/", () => import("@/app/page")], |
| 41 | + ["/pattern", () => import("@/app/pattern/page")], |
| 42 | + ["/build", () => import("@/app/build/page")], |
| 43 | + ["/build/breadboard", () => import("@/app/build/breadboard/layout")], |
| 44 | + ["/inside", () => import("@/app/inside/page")], |
| 45 | + ["/features", () => import("@/app/features/page")], |
| 46 | + ["/contact", () => import("@/app/contact/page")], |
| 47 | + ["/terms", () => import("@/app/terms/page")], |
| 48 | + ["/update", () => import("@/app/update/page")], |
| 49 | + ] as const)("%s previews with its own address and the site's picture", async (path, load) => { |
| 50 | + const { metadata } = (await load()) as { metadata: Metadata }; |
| 51 | + const og = metadata.openGraph as OpenGraph; |
| 52 | + expect(og.images).toEqual(SITE_OPEN_GRAPH.images); |
| 53 | + expect(urlOf(og).replace("https://patternflow.work", "") || "/").toBe(path); |
| 54 | + }); |
| 55 | + |
| 56 | + it("gives /build/breadboard a title of its own", async () => { |
| 57 | + const { metadata } = await import("@/app/build/breadboard/layout"); |
| 58 | + expect(metadata.title).toBe("Breadboard Build Guide / Patternflow"); |
| 59 | + }); |
| 60 | + |
| 61 | + it("gives every guide page, in both languages, the site's picture", () => { |
| 62 | + const screens: GuideScreen[] = ["hub", ...(Object.keys(PAGES) as GuideScreen[])]; |
| 63 | + for (const page of screens) { |
| 64 | + for (const lang of ["en", "ko"] as const) { |
| 65 | + const og = guideMetadata(page, lang).openGraph as OpenGraph; |
| 66 | + expect(og.images).toEqual(SITE_OPEN_GRAPH.images); |
| 67 | + expect(urlOf(og)).toBe(pagePath(page, lang)); |
| 68 | + } |
| 69 | + } |
| 70 | + }); |
| 71 | + |
| 72 | + it("previews a build on the map with its photo, or the site's picture without one", async () => { |
| 73 | + const { generateMetadata } = await import("@/app/inside/[build]/page"); |
| 74 | + const ogOf = async (slug: string) => |
| 75 | + (await generateMetadata({ params: Promise.resolve({ build: slug }) })).openGraph as OpenGraph; |
| 76 | + |
| 77 | + const withPhoto = builds.find((b) => b.images?.length); |
| 78 | + expect(withPhoto).toBeDefined(); |
| 79 | + expect(await ogOf(withPhoto!.slug)).toMatchObject({ |
| 80 | + images: [{ url: `https://patternflow.work${withPhoto!.images![0].src}` }], |
| 81 | + url: `https://patternflow.work/inside/${withPhoto!.slug}`, |
| 82 | + }); |
| 83 | + |
| 84 | + const withoutPhoto = builds.find((b) => !b.images?.length); |
| 85 | + if (withoutPhoto) { |
| 86 | + expect((await ogOf(withoutPhoto.slug)).images).toEqual(SITE_OPEN_GRAPH.images); |
| 87 | + } |
| 88 | + }); |
| 89 | +}); |
0 commit comments