From ac1527aac17e5c1e670756d14873817a353076f1 Mon Sep 17 00:00:00 2001 From: Aaron Couch Date: Fri, 14 Jun 2024 20:58:26 -0400 Subject: [PATCH 1/3] Renable static rendering --- frontend/src/app/[locale]/layout.tsx | 54 +++++++++++++++++++ .../[locale]/newsletter/confirmation/page.tsx | 3 +- frontend/src/app/[locale]/newsletter/page.tsx | 3 +- .../[locale]/newsletter/unsubscribe/page.tsx | 3 +- frontend/src/app/[locale]/page.tsx | 4 +- frontend/src/app/[locale]/process/page.tsx | 3 +- frontend/src/app/[locale]/research/page.tsx | 4 +- frontend/src/app/layout.tsx | 44 ++------------- frontend/src/app/template.tsx | 17 ------ frontend/src/components/Layout.tsx | 3 ++ 10 files changed, 76 insertions(+), 62 deletions(-) create mode 100644 frontend/src/app/[locale]/layout.tsx delete mode 100644 frontend/src/app/template.tsx diff --git a/frontend/src/app/[locale]/layout.tsx b/frontend/src/app/[locale]/layout.tsx new file mode 100644 index 000000000..3edc9f9a0 --- /dev/null +++ b/frontend/src/app/[locale]/layout.tsx @@ -0,0 +1,54 @@ +import { NextIntlClientProvider } from "next-intl"; +import { getMessages, unstable_setRequestLocale } from "next-intl/server"; + +import { GoogleAnalytics } from "@next/third-parties/google"; +import { PUBLIC_ENV } from "src/constants/environments"; + +import Layout from "src/components/Layout"; + +/** + * Root layout component, wraps all pages. + * @see https://nextjs.org/docs/app/api-reference/file-conventions/layout + */ +import { Metadata } from "next"; + +export const metadata: Metadata = { + icons: [`${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/img/favicon.ico`], +}; + +interface Props { + children: React.ReactNode; + params: { + locale: string; + }; +} + +const locales = ["en", "es"]; + +export function generateStaticParams() { + return locales.map((locale) => ({ locale })); +} + +export default async function LocaleLayout({ children, params }: Props) { + const { locale } = params; + + // Enable static rendering + unstable_setRequestLocale(locale); + + // Providing all messages to the client + // side is the easiest way to get started + const messages = await getMessages(); + + return ( + + + + + + + {children} + + + + ); +} diff --git a/frontend/src/app/[locale]/newsletter/confirmation/page.tsx b/frontend/src/app/[locale]/newsletter/confirmation/page.tsx index e621b610a..5a9c2e2a0 100644 --- a/frontend/src/app/[locale]/newsletter/confirmation/page.tsx +++ b/frontend/src/app/[locale]/newsletter/confirmation/page.tsx @@ -8,7 +8,7 @@ import PageSEO from "src/components/PageSEO"; import BetaAlert from "src/components/BetaAlert"; import { useTranslations } from "next-intl"; import { Metadata } from "next"; -import { getTranslations } from "next-intl/server"; +import { getTranslations, unstable_setRequestLocale } from "next-intl/server"; export async function generateMetadata() { const t = await getTranslations({ locale: "en" }); @@ -21,6 +21,7 @@ export async function generateMetadata() { } export default function NewsletterConfirmation() { + unstable_setRequestLocale("en"); const t = useTranslations("Newsletter_confirmation"); return ( diff --git a/frontend/src/app/[locale]/newsletter/page.tsx b/frontend/src/app/[locale]/newsletter/page.tsx index 080aaed5a..f7b1be091 100644 --- a/frontend/src/app/[locale]/newsletter/page.tsx +++ b/frontend/src/app/[locale]/newsletter/page.tsx @@ -7,7 +7,7 @@ import PageSEO from "src/components/PageSEO"; import BetaAlert from "src/components/BetaAlert"; import NewsletterForm from "src/app/[locale]/newsletter/NewsletterForm"; import { Metadata } from "next"; -import { getTranslations } from "next-intl/server"; +import { getTranslations, unstable_setRequestLocale } from "next-intl/server"; import { useTranslations, useMessages, @@ -25,6 +25,7 @@ export async function generateMetadata() { } export default function Newsletter() { + unstable_setRequestLocale("en"); const t = useTranslations("Newsletter"); const messages = useMessages(); diff --git a/frontend/src/app/[locale]/newsletter/unsubscribe/page.tsx b/frontend/src/app/[locale]/newsletter/unsubscribe/page.tsx index 9a095211a..925f7f67f 100644 --- a/frontend/src/app/[locale]/newsletter/unsubscribe/page.tsx +++ b/frontend/src/app/[locale]/newsletter/unsubscribe/page.tsx @@ -8,7 +8,7 @@ import PageSEO from "src/components/PageSEO"; import BetaAlert from "src/components/BetaAlert"; import { useTranslations } from "next-intl"; import { Metadata } from "next"; -import { getTranslations } from "next-intl/server"; +import { getTranslations, unstable_setRequestLocale } from "next-intl/server"; export async function generateMetadata() { const t = await getTranslations({ locale: "en" }); @@ -21,6 +21,7 @@ export async function generateMetadata() { } export default function NewsletterUnsubscribe() { + unstable_setRequestLocale("en"); const t = useTranslations("Newsletter_unsubscribe"); return ( diff --git a/frontend/src/app/[locale]/page.tsx b/frontend/src/app/[locale]/page.tsx index 795c8d84d..07e4a7806 100644 --- a/frontend/src/app/[locale]/page.tsx +++ b/frontend/src/app/[locale]/page.tsx @@ -5,7 +5,7 @@ import IndexGoalContent from "src/components/content/IndexGoalContent"; import ProcessAndResearchContent from "src/components/content/ProcessAndResearchContent"; import { Metadata } from "next"; import { useTranslations } from "next-intl"; -import { getTranslations } from "next-intl/server"; +import { getTranslations, unstable_setRequestLocale } from "next-intl/server"; export async function generateMetadata() { const t = await getTranslations({ locale: "en" }); @@ -17,6 +17,8 @@ export async function generateMetadata() { } export default function Home() { + unstable_setRequestLocale("en"); + const t = useTranslations("Index"); return ( diff --git a/frontend/src/app/[locale]/process/page.tsx b/frontend/src/app/[locale]/process/page.tsx index 788627afa..eb5050e43 100644 --- a/frontend/src/app/[locale]/process/page.tsx +++ b/frontend/src/app/[locale]/process/page.tsx @@ -9,7 +9,7 @@ import ProcessIntro from "src/app/[locale]/process/ProcessIntro"; import ProcessInvolved from "src/app/[locale]/process/ProcessInvolved"; import ProcessMilestones from "src/app/[locale]/process/ProcessMilestones"; import { useTranslations } from "next-intl"; -import { getTranslations } from "next-intl/server"; +import { getTranslations, unstable_setRequestLocale } from "next-intl/server"; export async function generateMetadata() { const t = await getTranslations({ locale: "en" }); @@ -21,6 +21,7 @@ export async function generateMetadata() { } export default function Process() { + unstable_setRequestLocale("en"); const t = useTranslations("Process"); return ( diff --git a/frontend/src/app/[locale]/research/page.tsx b/frontend/src/app/[locale]/research/page.tsx index b6fc95076..2599e5402 100644 --- a/frontend/src/app/[locale]/research/page.tsx +++ b/frontend/src/app/[locale]/research/page.tsx @@ -10,7 +10,7 @@ import ResearchMethodology from "src/app/[locale]/research/ResearchMethodology"; import ResearchThemes from "src/app/[locale]/research/ResearchThemes"; import { Metadata } from "next"; import { useTranslations } from "next-intl"; -import { getTranslations } from "next-intl/server"; +import { getTranslations, unstable_setRequestLocale } from "next-intl/server"; export async function generateMetadata() { const t = await getTranslations({ locale: "en" }); @@ -22,6 +22,8 @@ export async function generateMetadata() { } export default function Research() { + unstable_setRequestLocale("en"); + const t = useTranslations("Research"); return ( diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index e45e82c42..e914be1af 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,44 +1,10 @@ import "src/styles/styles.scss"; -import { GoogleAnalytics } from "@next/third-parties/google"; -import { PUBLIC_ENV } from "src/constants/environments"; +import { ReactNode } from "react"; -import Layout from "src/components/Layout"; -import { unstable_setRequestLocale } from "next-intl/server"; -/** - * Root layout component, wraps all pages. - * @see https://nextjs.org/docs/app/api-reference/file-conventions/layout - */ -import { Metadata } from "next"; - -export const metadata: Metadata = { - icons: [`${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/img/favicon.ico`], +type Props = { + children: ReactNode; }; -interface LayoutProps { - children: React.ReactNode; - params: { - locale: string; - }; -} - -export default function RootLayout({ children, params }: LayoutProps) { - // Hardcoded until the [locale] routing is enabled. - const locale = params.locale ? params.locale : "en"; - // TODO: Remove when https://github.com/amannn/next-intl/issues/663 lands. - unstable_setRequestLocale(locale); - - return ( - - - - - - {/* Separate layout component for the inner-body UI elements since Storybook - and tests trip over the fact that this file renders an tag */} - - {/* TODO: Add locale="english" prop when ready for i18n */} - {children} - - - ); +export default function RootLayout({ children }: Props) { + return children; } diff --git a/frontend/src/app/template.tsx b/frontend/src/app/template.tsx deleted file mode 100644 index b7250859f..000000000 --- a/frontend/src/app/template.tsx +++ /dev/null @@ -1,17 +0,0 @@ -"use client"; - -import { useEffect } from "react"; -import { sendGAEvent } from "@next/third-parties/google"; -import { PUBLIC_ENV } from "src/constants/environments"; - -export default function Template({ children }: { children: React.ReactNode }) { - const isProd = process.env.NODE_ENV === "production"; - - useEffect(() => { - isProd && - PUBLIC_ENV.GOOGLE_ANALYTICS_ID && - sendGAEvent("event", "page_view"); - }, [isProd]); - - return
{children}
; -} diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 9cce89c04..54c6290af 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -6,6 +6,7 @@ import { useMessages, NextIntlClientProvider, } from "next-intl"; +import { unstable_setRequestLocale } from "next-intl/server"; import pick from "lodash/pick"; type Props = { @@ -14,6 +15,8 @@ type Props = { }; export default function Layout({ children, locale }: Props) { + unstable_setRequestLocale(locale); + const t = useTranslations(); const messages = useMessages(); From bd662978b65877071d4484bb09d19fdf275adbde Mon Sep 17 00:00:00 2001 From: acouch Date: Wed, 3 Jul 2024 16:44:07 -0400 Subject: [PATCH 2/3] Remove page tests --- frontend/tests/pages/index.test.tsx | 34 ------ .../pages/newsletter/confirmation.test.tsx | 12 -- .../tests/pages/newsletter/index.test.tsx | 111 ------------------ .../pages/newsletter/unsubscribe.test.tsx | 12 -- frontend/tests/pages/process.test.tsx | 24 ---- frontend/tests/pages/research.test.tsx | 23 ---- frontend/tests/pages/search.test.tsx | 96 --------------- 7 files changed, 312 deletions(-) delete mode 100644 frontend/tests/pages/index.test.tsx delete mode 100644 frontend/tests/pages/newsletter/confirmation.test.tsx delete mode 100644 frontend/tests/pages/newsletter/index.test.tsx delete mode 100644 frontend/tests/pages/newsletter/unsubscribe.test.tsx delete mode 100644 frontend/tests/pages/process.test.tsx delete mode 100644 frontend/tests/pages/research.test.tsx delete mode 100644 frontend/tests/pages/search.test.tsx diff --git a/frontend/tests/pages/index.test.tsx b/frontend/tests/pages/index.test.tsx deleted file mode 100644 index 6924cbbd0..000000000 --- a/frontend/tests/pages/index.test.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { waitFor, screen, render } from "tests/react-utils"; - -import { axe } from "jest-axe"; -import Index from "src/app/[locale]/page"; - -describe("Index", () => { - it("renders alert with grants.gov link", () => { - render(); - - const alert = screen.getByTestId("alert"); - const link = screen.getByRole("link", { name: /grants\.gov/i }); - - expect(alert).toBeInTheDocument(); - expect(link).toHaveAttribute("href", "https://www.grants.gov"); - }); - - it("renders the goals section", () => { - render(); - - const goalH2 = screen.getByRole("heading", { - level: 2, - name: /The goal?/i, - }); - - expect(goalH2).toBeInTheDocument(); - }); - - it("passes accessibility scan", async () => { - const { container } = render(); - const results = await waitFor(() => axe(container)); - - expect(results).toHaveNoViolations(); - }); -}); diff --git a/frontend/tests/pages/newsletter/confirmation.test.tsx b/frontend/tests/pages/newsletter/confirmation.test.tsx deleted file mode 100644 index 7324e9c7c..000000000 --- a/frontend/tests/pages/newsletter/confirmation.test.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { render, waitFor } from "tests/react-utils"; -import { axe } from "jest-axe"; -import NewsletterConfirmation from "src/app/[locale]/newsletter/confirmation/page"; - -describe("Newsletter", () => { - it("passes accessibility scan", async () => { - const { container } = render(); - const results = await waitFor(() => axe(container)); - - expect(results).toHaveNoViolations(); - }); -}); diff --git a/frontend/tests/pages/newsletter/index.test.tsx b/frontend/tests/pages/newsletter/index.test.tsx deleted file mode 100644 index 9f430ef2b..000000000 --- a/frontend/tests/pages/newsletter/index.test.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { fireEvent, render, screen, waitFor } from "tests/react-utils"; - -import userEvent from "@testing-library/user-event"; -import { axe } from "jest-axe"; -import Newsletter from "src/app/[locale]/newsletter/page"; - -import { useRouter } from "next/navigation"; - -jest.mock("next/navigation"); - -describe("Newsletter", () => { - it("renders signup form with a submit button", () => { - render(); - - const sendyform = screen.getByTestId("sendy-form"); - - expect(sendyform).toBeInTheDocument(); - }); - - it("submits the form successfully", async () => { - const mockRouter = { - push: jest.fn(), // the component uses `router.push` only - }; - (useRouter as jest.Mock).mockReturnValue(mockRouter); - render(); - - // Mock the fetch function to return a successful response - global.fetch = jest.fn().mockResolvedValue({ - ok: true, - json: jest.fn().mockResolvedValue({ message: "Success" }), - }); - - // Fill out the form - await userEvent.type(screen.getByLabelText(/First Name/i), "John"); - await userEvent.type(screen.getByLabelText(/Last Name/i), "Doe"); - await userEvent.type( - screen.getByLabelText(/Email/i), - "john.doe@example.com", - ); - - // Submit the form - fireEvent.click(screen.getByRole("button", { name: /subscribe/i })); - - // Wait for the form submission - await waitFor(() => { - expect(mockRouter.push).toHaveBeenCalledWith( - "/newsletter/confirmation/?sendy=Success", - ); - }); - }); - - it("shows alert when recieving an error from Sendy", async () => { - render(); - - // Mock the fetch function to return a successful response - global.fetch = jest.fn().mockResolvedValue({ - ok: false, - json: jest.fn().mockResolvedValue({ error: "Already subscribed." }), - }); - jest.spyOn(global.console, "error"); - - // Fill out the form - await userEvent.type(screen.getByLabelText(/First Name/i), "John"); - await userEvent.type(screen.getByLabelText(/Last Name/i), "Doe"); - await userEvent.type( - screen.getByLabelText(/Email/i), - "john.doe@example.com", - ); - - // Submit the form - fireEvent.click(screen.getByRole("button", { name: /subscribe/i })); - - // Wait for the form submission - await waitFor(() => { - const alert = screen.getByRole("heading", { - level: 3, - name: /You’re already signed up!?/i, - }); - expect(alert).toBeInTheDocument(); - }); - await waitFor(() => { - expect(console.error).toHaveBeenCalledWith( - "client error", - "Already subscribed.", - ); - }); - }); - - it("prevents the form from submitting when incomplete", async () => { - render(); - - // Fill out the form - await userEvent.type(screen.getByLabelText(/First Name/i), "John"); - await userEvent.type(screen.getByLabelText(/Last Name/i), "Doe"); - - // Submit the form - fireEvent.click(screen.getByRole("button", { name: /subscribe/i })); - - // Wait for the form submission - await waitFor(() => { - expect(screen.getByText("Enter your email address.")).toBeInTheDocument(); - }); - }); - - it("passes accessibility scan", async () => { - const { container } = render(); - const results = await waitFor(() => axe(container)); - - expect(results).toHaveNoViolations(); - }); -}); diff --git a/frontend/tests/pages/newsletter/unsubscribe.test.tsx b/frontend/tests/pages/newsletter/unsubscribe.test.tsx deleted file mode 100644 index b544f941f..000000000 --- a/frontend/tests/pages/newsletter/unsubscribe.test.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { render, waitFor } from "tests/react-utils"; -import { axe } from "jest-axe"; -import NewsletterUnsubscribe from "src/app/[locale]/newsletter/unsubscribe/page"; - -describe("Newsletter", () => { - it("passes accessibility scan", async () => { - const { container } = render(); - const results = await waitFor(() => axe(container)); - - expect(results).toHaveNoViolations(); - }); -}); diff --git a/frontend/tests/pages/process.test.tsx b/frontend/tests/pages/process.test.tsx deleted file mode 100644 index bd9d43929..000000000 --- a/frontend/tests/pages/process.test.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { render, screen, waitFor } from "tests/react-utils"; - -import { axe } from "jest-axe"; -import Process from "src/app/[locale]/process/page"; - -describe("Process", () => { - it("renders alert with grants.gov link", () => { - render(); - - const alert = screen.getByTestId("alert"); - // There are multiple links to grants.gov - const link = screen.getAllByText("www.grants.gov")[0]; - - expect(alert).toBeInTheDocument(); - expect(link).toHaveAttribute("href", "https://www.grants.gov"); - }); - - it("passes accessibility scan", async () => { - const { container } = render(); - const results = await waitFor(() => axe(container)); - - expect(results).toHaveNoViolations(); - }); -}); diff --git a/frontend/tests/pages/research.test.tsx b/frontend/tests/pages/research.test.tsx deleted file mode 100644 index c4b55fed6..000000000 --- a/frontend/tests/pages/research.test.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { render, screen, waitFor } from "tests/react-utils"; - -import { axe } from "jest-axe"; -import Research from "src/app/[locale]/research/page"; - -describe("Research", () => { - it("renders alert with grants.gov link", () => { - render(); - - const alert = screen.getByTestId("alert"); - const link = screen.getByRole("link", { name: /www\.grants\.gov/i }); - - expect(alert).toBeInTheDocument(); - expect(link).toHaveAttribute("href", "https://www.grants.gov"); - }); - - it("passes accessibility scan", async () => { - const { container } = render(); - const results = await waitFor(() => axe(container)); - - expect(results).toHaveNoViolations(); - }); -}); diff --git a/frontend/tests/pages/search.test.tsx b/frontend/tests/pages/search.test.tsx deleted file mode 100644 index db4550732..000000000 --- a/frontend/tests/pages/search.test.tsx +++ /dev/null @@ -1,96 +0,0 @@ -/* eslint-disable jest/no-commented-out-tests */ - -import { useFeatureFlags } from "src/hooks/useFeatureFlags"; - -jest.mock("src/hooks/useFeatureFlags"); - -const setFeatureFlag = (flag: string, value: boolean) => { - (useFeatureFlags as jest.Mock).mockReturnValue({ - featureFlagsManager: { - isFeatureEnabled: jest.fn((featureName: string) => - featureName === flag ? value : false, - ), - }, - mounted: true, - }); -}; - -const mockData = [ - { - agency: "firstagency", - category: "firstcategory", - opportunity_title: "firsttitle", - }, - { - agency: "secondagency2", - category: "secondcategory", - opportunity_title: "secondtitle", - }, -]; - -// Mock both search fetchers in case we switch -// Could also switch on a feature flag -jest.mock("../../src/services/search/searchfetcher/APISearchFetcher", () => { - return { - APISearchFetcher: jest.fn().mockImplementation(() => { - return { - fetchOpportunities: jest.fn().mockImplementation(() => { - return Promise.resolve(mockData); - }), - }; - }), - }; -}); - -jest.mock("../../src/services/search/searchfetcher/MockSearchFetcher", () => { - return { - MockSearchFetcher: jest.fn().mockImplementation(() => { - return { - fetchOpportunities: jest.fn().mockImplementation(() => { - return Promise.resolve(mockData); - }), - }; - }), - }; -}); - -describe("Search", () => { - it("should pass", () => { - setFeatureFlag("showSearchV0", true); - expect(1).toBe(1); - }); - // TODO (Issue #1393): Redo tests after converting search to server component. Save below for reference - - // it("passes accessibility scan", async () => { - // setFeatureFlag("showSearchV0", true); - // const { container } = render(); - // const results = await waitFor(() => axe(container)); - // expect(results).toHaveNoViolations(); - // }); - - // describe("Search feature flag", () => { - // it("renders search results when feature flag is on", async () => { - // setFeatureFlag("showSearchV0", true); - // render(); - // fireEvent.click(screen.getByRole("button", { name: /update results/i })); - - // await waitFor(() => { - // expect(screen.getByText(/firstcategory/i)).toBeInTheDocument(); - // }); - // expect(screen.getByText(/secondcategory/i)).toBeInTheDocument(); - // }); - - // it("renders PageNotFound when feature flag is off", async () => { - // setFeatureFlag("showSearchV0", false); - // render(); - - // await waitFor(() => { - // expect( - // screen.getByText( - // /The page you have requested cannot be displayed because it does not exist, has been moved/i, - // ), - // ).toBeInTheDocument(); - // }); - // }); - // }); -}); From 6435c62648cbba727af20a4785e13c3d2a626f8a Mon Sep 17 00:00:00 2001 From: acouch Date: Wed, 3 Jul 2024 17:00:23 -0400 Subject: [PATCH 3/3] Remove Layout test --- frontend/tests/components/Layout.test.tsx | 29 ----------------------- 1 file changed, 29 deletions(-) delete mode 100644 frontend/tests/components/Layout.test.tsx diff --git a/frontend/tests/components/Layout.test.tsx b/frontend/tests/components/Layout.test.tsx deleted file mode 100644 index e9d06997f..000000000 --- a/frontend/tests/components/Layout.test.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { render, screen } from "tests/react-utils"; -import { axe } from "jest-axe"; - -import Layout from "src/components/Layout"; - -describe("AppLayout", () => { - it("renders children in main section", () => { - render( - -

child

-
, - ); - - const header = screen.getByRole("heading", { name: /child/i, level: 1 }); - - expect(header).toBeInTheDocument(); - }); - - it("passes accessibility scan", async () => { - const { container } = render( - -

child

-
, - ); - const results = await axe(container); - - expect(results).toHaveNoViolations(); - }); -});