async-params-layouts.test.tsx 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
  1. import type { ReactNode } from "react";
  2. import { beforeEach, describe, expect, test, vi } from "vitest";
  3. const authMocks = vi.hoisted(() => ({
  4. getSession: vi.fn(async () => null),
  5. }));
  6. vi.mock("@/lib/auth", () => authMocks);
  7. vi.mock("@/i18n/routing", () => ({
  8. Link: ({
  9. href,
  10. children,
  11. ...rest
  12. }: {
  13. href: string;
  14. children: ReactNode;
  15. className?: string;
  16. }) => (
  17. <a href={href} {...rest}>
  18. {children}
  19. </a>
  20. ),
  21. }));
  22. const intlServerMocks = vi.hoisted(() => ({
  23. getTranslations: vi.fn(async ({ locale, namespace }: { locale: string; namespace: string }) => {
  24. return (key: string) => `${namespace}.${key}.${locale}`;
  25. }),
  26. }));
  27. vi.mock("next-intl/server", () => intlServerMocks);
  28. function makeAsyncParams(locale: string) {
  29. const promise = Promise.resolve({ locale });
  30. Object.defineProperty(promise, "locale", {
  31. get() {
  32. throw new Error("sync access to params.locale is not allowed");
  33. },
  34. });
  35. return promise as Promise<{ locale: string }> & { locale: string };
  36. }
  37. describe("Next.js async params compatibility", () => {
  38. beforeEach(() => {
  39. authMocks.getSession.mockReset();
  40. authMocks.getSession.mockResolvedValue(null);
  41. intlServerMocks.getTranslations.mockClear();
  42. });
  43. test("usage-doc generateMetadata awaits params before reading locale", async () => {
  44. const { generateMetadata } = await import("@/app/[locale]/usage-doc/layout");
  45. const metadata = await generateMetadata({
  46. params: makeAsyncParams("en") as unknown as Promise<{ locale: string }>,
  47. });
  48. expect(metadata).toEqual({
  49. title: "usage.pageTitle.en",
  50. description: "usage.pageDescription.en",
  51. });
  52. });
  53. test("UsageDocLayout awaits params before reading locale (session/no-session branches)", async () => {
  54. const UsageDocLayoutModule = await import("@/app/[locale]/usage-doc/layout");
  55. authMocks.getSession.mockResolvedValueOnce(null);
  56. const noSession = await UsageDocLayoutModule.default({
  57. children: <div />,
  58. params: makeAsyncParams("en") as unknown as Promise<{ locale: string }>,
  59. });
  60. expect(noSession).toBeTruthy();
  61. authMocks.getSession.mockResolvedValueOnce({} as never);
  62. const hasSession = await UsageDocLayoutModule.default({
  63. children: <div />,
  64. params: makeAsyncParams("en") as unknown as Promise<{ locale: string }>,
  65. });
  66. expect(hasSession).toBeTruthy();
  67. });
  68. test("big-screen generateMetadata awaits params before reading locale", async () => {
  69. const BigScreenLayoutModule = await import(
  70. "@/app/[locale]/internal/dashboard/big-screen/layout"
  71. );
  72. const metadata = await BigScreenLayoutModule.generateMetadata({
  73. params: makeAsyncParams("en") as unknown as Promise<{ locale: string }>,
  74. });
  75. expect(metadata).toEqual({
  76. title: "bigScreen.pageTitle.en",
  77. description: "bigScreen.pageDescription.en",
  78. });
  79. const element = BigScreenLayoutModule.default({ children: <div /> });
  80. expect(element).toBeTruthy();
  81. });
  82. });