diff --git a/.github/workflows/vitest.yml b/.github/workflows/vitest.yml index f3bcd54264..7d26bb49ce 100644 --- a/.github/workflows/vitest.yml +++ b/.github/workflows/vitest.yml @@ -30,5 +30,11 @@ jobs: - name: "Install dependencies" run: yarn workspaces focus gcforms + - name: Install Playwright Browsers + run: npx playwright install --with-deps chromium + - name: Vitest Tests run: yarn test:vitest + + - name: Vitest Browser Tests + run: yarn test:vitest:browser diff --git a/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/ContentWrapper.tsx b/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/ContentWrapper.tsx index 17644ac621..139d441161 100644 --- a/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/ContentWrapper.tsx +++ b/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/ContentWrapper.tsx @@ -1,15 +1,13 @@ "use client"; import Link from "next/link"; -import { useTranslation } from "react-i18next"; import { useResponsesContext } from "./context/ResponsesContext"; -import { useRouter } from "next/navigation"; +import { useResponsesApp } from "./context"; import { disableResponsesPilotMode } from "../responses/actions"; export const ContentWrapper = ({ children }: { children: React.ReactNode }) => { const { formId } = useResponsesContext(); - const { t, i18n } = useTranslation(["form-builder-responses"]); - const router = useRouter(); + const { t, i18n, router } = useResponsesApp(); const handleSwitchBack = async (e: React.MouseEvent) => { e.preventDefault(); diff --git a/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/Responses.tsx b/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/Responses.tsx index f2b3f94b74..b83f9a98c4 100644 --- a/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/Responses.tsx +++ b/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/Responses.tsx @@ -10,7 +10,10 @@ export const Responses = ({ actions }: { actions?: React.ReactNode }) => { if (newFormSubmissions === null) { return ( -
+
@@ -24,9 +27,11 @@ export const Responses = ({ actions }: { actions?: React.ReactNode }) => { } return newFormSubmissions && newFormSubmissions.length > 0 ? ( -
+
-

{t("loadKeyPage.newResponsesAvailable")}

+

+ {t("loadKeyPage.newResponsesAvailable")} +

{actions}
@@ -39,9 +44,11 @@ export const Responses = ({ actions }: { actions?: React.ReactNode }) => {
) : ( -
+
-

{t("loadKeyPage.noNewResponsesAvailable")}

+

+ {t("loadKeyPage.noNewResponsesAvailable")} +

{actions}
diff --git a/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/context/BrowserResponsesAppProvider.tsx b/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/context/BrowserResponsesAppProvider.tsx new file mode 100644 index 0000000000..ef8744d3f1 --- /dev/null +++ b/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/context/BrowserResponsesAppProvider.tsx @@ -0,0 +1,112 @@ +"use client"; + +import { createContext, useContext, ReactNode } from "react"; +import { showOpenFilePicker } from "native-file-system-adapter"; +import { getAccessTokenFromApiKey } from "../lib/utils"; +import i18next from "i18next"; +// Import to trigger i18next initialization +import "@root/i18n/client"; + +interface ResponsesAppContextType { + // Navigation + router: { + push: (href: string) => void; + replace: (href: string) => void; + back: () => void; + forward: () => void; + refresh: () => void; + prefetch: (href: string) => Promise; + }; + searchParams: URLSearchParams; + + // i18n + t: (key: string, options?: Record) => string; + i18n: { + language: string; + changeLanguage: (lang: string) => Promise; + }; + + // File system + showOpenFilePicker: typeof showOpenFilePicker; + + // API utilities + getAccessTokenFromApiKey: typeof getAccessTokenFromApiKey; + + // Environment + apiUrl: string; + isDevelopment: boolean; +} + +const BrowserResponsesAppContext = createContext(null); + +interface BrowserResponsesAppProviderProps { + children: ReactNode; + overrides?: Partial; +} + +/** + * Browser-only version of ResponsesAppProvider that uses real i18n. + * Use this for Vitest browser mode testing. + */ +export const BrowserResponsesAppProvider = ({ + children, + overrides = {}, +}: BrowserResponsesAppProviderProps) => { + // Create a translation function that uses response-api namespace + const t = (key: string, options?: Record) => { + return i18next.t(key, { ...options, ns: "response-api" }); + }; + + // Default mock implementations for browser testing + const defaultRouter = { + push: () => {}, + replace: () => {}, + back: () => {}, + forward: () => {}, + refresh: () => {}, + prefetch: () => Promise.resolve(), + }; + + const defaultSearchParams = new URLSearchParams(); + + // Create i18n object + const wrappedI18n = { + language: i18next.language || "en", + changeLanguage: async (lang: string) => { + await i18next.changeLanguage(lang); + }, + }; + + const value: ResponsesAppContextType = { + router: defaultRouter, + searchParams: defaultSearchParams, + t, + i18n: wrappedI18n, + showOpenFilePicker, + getAccessTokenFromApiKey, + apiUrl: "http://localhost:3000/api", + isDevelopment: true, + ...overrides, // Allow custom overrides for specific test needs + }; + + return ( + + {children} + + ); +}; + +/** + * Hook to access the browser responses app context. + * Must be used within BrowserResponsesAppProvider. + */ +export const useBrowserResponsesApp = () => { + const context = useContext(BrowserResponsesAppContext); + if (!context) { + throw new Error("useBrowserResponsesApp must be used within BrowserResponsesAppProvider"); + } + return context; +}; + +// Also export as useResponsesApp for compatibility with components expecting that name +export const useResponsesApp = useBrowserResponsesApp; diff --git a/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/context/ResponsesAppProvider.tsx b/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/context/ResponsesAppProvider.tsx new file mode 100644 index 0000000000..16e78eb698 --- /dev/null +++ b/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/context/ResponsesAppProvider.tsx @@ -0,0 +1,70 @@ +"use client"; + +import { createContext, useContext, ReactNode } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useTranslation } from "@i18n/client"; +import { showOpenFilePicker } from "native-file-system-adapter"; +import { getAccessTokenFromApiKey } from "../lib/utils"; + +interface ResponsesAppContextType { + // Navigation + router: ReturnType; + searchParams: ReturnType; + + // i18n + t: ReturnType["t"]; + i18n: ReturnType["i18n"]; + + // File system + showOpenFilePicker: typeof showOpenFilePicker; + + // API utilities + getAccessTokenFromApiKey: typeof getAccessTokenFromApiKey; + + // Environment + apiUrl: string; + isDevelopment: boolean; +} + +const ResponsesAppContext = createContext(null); + +interface ResponsesAppProviderProps { + children: ReactNode; + _locale: string; // Passed for future use, currently unused + namespace?: string; + // Optional overrides for testing + overrides?: Partial; +} + +export const ResponsesAppProvider = ({ + children, + _locale, + namespace = "response-api", + overrides = {}, +}: ResponsesAppProviderProps) => { + const router = useRouter(); + const searchParams = useSearchParams(); + const { t, i18n } = useTranslation(namespace); + + const value: ResponsesAppContextType = { + router, + searchParams, + t, + i18n, + showOpenFilePicker, + getAccessTokenFromApiKey, + apiUrl: process.env.NEXT_PUBLIC_API_URL ?? "", + isDevelopment: process.env.NODE_ENV === "development", + ...overrides, // Allow test overrides + }; + + return {children}; +}; + +export const useResponsesApp = () => { + const context = useContext(ResponsesAppContext); + if (!context) { + throw new Error("useResponsesApp must be used within ResponsesAppProvider"); + } + return context; +}; diff --git a/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/context/index.ts b/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/context/index.ts new file mode 100644 index 0000000000..8e5da13198 --- /dev/null +++ b/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/context/index.ts @@ -0,0 +1,22 @@ +/** + * Barrel export for ResponsesApp context + * In Vitest browser test environments, exports the Browser version + * In production, exports the real ResponsesAppProvider with Next.js hooks + */ + +// Check if running in Vitest browser mode +const isBrowserTest = typeof process !== "undefined" && process.env.VITEST_BROWSER === "true"; + +// Import from both +import { + useResponsesApp as useResponsesAppProd, + ResponsesAppProvider as ProdProvider, +} from "./ResponsesAppProvider"; +import { + useResponsesApp as useResponsesAppBrowser, + BrowserResponsesAppProvider, +} from "./BrowserResponsesAppProvider"; + +// Export the appropriate version based on environment +export const useResponsesApp = isBrowserTest ? useResponsesAppBrowser : useResponsesAppProd; +export const ResponsesAppProvider = isBrowserTest ? BrowserResponsesAppProvider : ProdProvider; diff --git a/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/layout.tsx b/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/layout.tsx index c0cd54e3f1..7f1f58afea 100644 --- a/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/layout.tsx +++ b/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/layout.tsx @@ -5,6 +5,7 @@ import { FeatureFlags } from "@lib/cache/types"; import { featureFlagAllowedForUser } from "@lib/userFeatureFlags"; import { redirect } from "next/navigation"; import { ResponsesProvider } from "./context/ResponsesContext"; +import { ResponsesAppProvider } from "./context/ResponsesAppProvider"; import { ContentWrapper } from "./ContentWrapper"; import { PilotBadge } from "@clientComponents/globals/PilotBadge"; import { CompatibilityGuard } from "./guards/CompatibilityGuard"; @@ -43,12 +44,14 @@ export default async function ResponsesLayout(props: { } return ( - - -

{t("section-title")}

- - {props.children} -
-
+ + + +

{t("section-title")}

+ + {props.children} +
+
+
); } diff --git a/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/load-key/LoadKey.tsx b/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/load-key/LoadKey.tsx index 552da00819..6ecfbd9c90 100644 --- a/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/load-key/LoadKey.tsx +++ b/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/load-key/LoadKey.tsx @@ -35,6 +35,7 @@ export const LoadKey = ({ onLoadKey }: LoadKeyProps) => {

@@ -29,8 +33,11 @@ export const LostKeyPopover = ({ locale, id }: { locale: string; id: string }) = id="api-key-popover" popover="auto" className="gc-lost-key-popover-content rounded-xl border-1 border-gray-500 p-10" + data-testid="lost-key-popover" > -

{t("loadKeyPage.lostKey.lostKeyTip.title")}

+

+ {t("loadKeyPage.lostKey.lostKeyTip.title")} +

{t("loadKeyPage.lostKey.lostKeyTip.text1")}

{t("loadKeyPage.lostKey.lostKeyTip.text2")}{" "} diff --git a/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/load-key/ResponseActionButtons.tsx b/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/load-key/ResponseActionButtons.tsx index 82c4887cbf..2caab6002c 100644 --- a/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/load-key/ResponseActionButtons.tsx +++ b/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/load-key/ResponseActionButtons.tsx @@ -1,13 +1,10 @@ import { Button } from "@clientComponents/globals"; -import { useRouter } from "next/navigation"; - -import { useTranslation } from "@i18n/client"; +import { useResponsesApp } from "../context"; import { useResponsesContext } from "../context/ResponsesContext"; import { CheckForResponsesButton } from "../components/CheckForResponsesButton"; export const ResponseActionButtons = () => { - const { t } = useTranslation(["response-api", "common"]); - const router = useRouter(); + const { t, router } = useResponsesApp(); const { apiClient, newFormSubmissions, resetState, locale, formId } = useResponsesContext(); const handleBack = () => { @@ -22,7 +19,7 @@ export const ResponseActionButtons = () => { return (

- @@ -33,6 +30,7 @@ export const ResponseActionButtons = () => { theme="primary" disabled={Boolean(!apiClient || (newFormSubmissions && newFormSubmissions.length === 0))} onClick={handleNext} + data-testid="continue-button" > {t("continueButton")} diff --git a/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/load-key/SelectApiKey.tsx b/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/load-key/SelectApiKey.tsx index 1c237c94b6..fe76e118c7 100644 --- a/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/load-key/SelectApiKey.tsx +++ b/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/load-key/SelectApiKey.tsx @@ -1,23 +1,25 @@ "use client"; import { useCallback, useEffect } from "react"; -import { useRouter, useSearchParams } from "next/navigation"; -import { useTranslation } from "@i18n/client"; - +import { useResponsesApp } from "../context"; import { useResponsesContext } from "../context/ResponsesContext"; import { LoadKey } from "./LoadKey"; -import { getAccessTokenFromApiKey } from "../lib/utils"; -import { showOpenFilePicker } from "native-file-system-adapter"; import { GCFormsApiClient } from "../lib/apiClient"; import { Responses } from "../Responses"; import { LostKeyLink, LostKeyPopover } from "./LostKeyPopover"; import { ResponseActionButtons } from "./ResponseActionButtons"; export const SelectApiKey = ({ locale, id }: { locale: string; id: string }) => { - const { t } = useTranslation("response-api"); + const { + t, + router, + searchParams, + showOpenFilePicker, + getAccessTokenFromApiKey, + apiUrl, + isDevelopment, + } = useResponsesApp(); - const router = useRouter(); - const searchParams = useSearchParams(); const { apiClient, retrieveResponses, setApiClient, setPrivateApiKey, resetState } = useResponsesContext(); @@ -56,7 +58,7 @@ export const SelectApiKey = ({ locale, id }: { locale: string; id: string }) => const token = await getAccessTokenFromApiKey(keyFile); // Ensure the key's formId matches the current form id - unless in local development mode - if (keyFile.formId !== id && process.env.NODE_ENV !== "development") { + if (keyFile.formId !== id && !isDevelopment) { throw new Error("API key form ID does not match the current form ID."); } @@ -64,9 +66,7 @@ export const SelectApiKey = ({ locale, id }: { locale: string; id: string }) => return false; } - setApiClient( - new GCFormsApiClient(keyFile.formId, process.env.NEXT_PUBLIC_API_URL ?? "", keyFile, token) - ); + setApiClient(new GCFormsApiClient(keyFile.formId, apiUrl, keyFile, token)); setPrivateApiKey(keyFile); @@ -81,14 +81,22 @@ export const SelectApiKey = ({ locale, id }: { locale: string; id: string }) => // no-op return false; } - }, [setApiClient, setPrivateApiKey, id]); + }, [ + setApiClient, + setPrivateApiKey, + id, + showOpenFilePicker, + getAccessTokenFromApiKey, + apiUrl, + isDevelopment, + ]); return (
{!apiClient && (
{t("stepOf", { current: 1, total: 3 })}
-

{t("loadKeyPage.title")}

+

{t("loadKeyPage.title")}

{t("loadKeyPage.detail")}

diff --git a/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/location/DirectoryPicker.tsx b/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/location/DirectoryPicker.tsx index 8306f5f3a4..4bc8a305ad 100644 --- a/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/location/DirectoryPicker.tsx +++ b/app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/location/DirectoryPicker.tsx @@ -13,6 +13,7 @@ export const DirectoryPicker = ({ return (
diff --git a/components/serverComponents/globals/Buttons/LinkButton.tsx b/components/serverComponents/globals/Buttons/LinkButton.tsx index 38775bcc34..a0d5fb5241 100644 --- a/components/serverComponents/globals/Buttons/LinkButton.tsx +++ b/components/serverComponents/globals/Buttons/LinkButton.tsx @@ -28,6 +28,7 @@ type LinkButtonProps = { isActive?: boolean; testid?: string; target?: string; + "data-testid"?: string; }; export const Default = ({ href, children, className, scroll }: LinkButtonProps) => { @@ -38,20 +39,35 @@ export const Default = ({ href, children, className, scroll }: LinkButtonProps) ); }; -export const Primary = ({ href, children, className, scroll, target }: LinkButtonProps) => { +export const Primary = ({ + href, + children, + className, + scroll, + target, + "data-testid": dataTestId, +}: LinkButtonProps) => { return ( {children} ); }; -export const Secondary = ({ href, className, children, scroll, target }: LinkButtonProps) => { +export const Secondary = ({ + href, + className, + children, + scroll, + target, + "data-testid": dataTestId, +}: LinkButtonProps) => { return ( {children} diff --git a/i18n/translations/en/form-builder-responses.json b/i18n/translations/en/form-builder-responses.json index aba8748f82..2c28ad70ce 100644 --- a/i18n/translations/en/form-builder-responses.json +++ b/i18n/translations/en/form-builder-responses.json @@ -79,11 +79,6 @@ }, "viewAllProblemResponses": "View reported responses" }, - "responsesPilot": { - "pageTitle": "Responses (Pilot)", - "responsesPilotLink": "Try out the new Responses (Pilot)", - "responsesSwitchLink": "Switch back to the classic Responses view" - }, "supportWillContact": "Support will contact you", "reportedAsProblem": "Response reported as a problem", "downloadSuccess": { diff --git a/i18n/translations/en/response-api.json b/i18n/translations/en/response-api.json index 737d91abe3..bfb33e0a12 100644 --- a/i18n/translations/en/response-api.json +++ b/i18n/translations/en/response-api.json @@ -1,6 +1,9 @@ { "section-title": "Responses (Pilot)", "stepOf": "Step {{current}} of {{total}}", + "responsesPilot": { + "responsesSwitchLink": "Switch back to the classic Responses view" + }, "not-supported": { "title": "Web browser not supported", "body": "This browser cannot use the File System Access API. Try a different browser or give your browser permission to save files to your device." diff --git a/i18n/translations/fr/form-builder-responses.json b/i18n/translations/fr/form-builder-responses.json index 492b39b42d..8210bc43e4 100644 --- a/i18n/translations/fr/form-builder-responses.json +++ b/i18n/translations/fr/form-builder-responses.json @@ -79,13 +79,7 @@ }, "viewAllProblemResponses": "Afficher les réponses signalées" }, - - "responsesPilot": { - "pageTitle": "Réponses (Pilote)", - "responsesPilotLink": "Essayer la nouvelle fonctionnalité Réponses (Pilote)", - "responsesSwitchLink": "Revenir à l'affichage classique des Réponses" - }, - "supportWillContact": "L’équipe de soutien vous contactera", + "supportWillContact": "L'équipe de soutien vous contactera", "reportedAsProblem": "Réponse signalée avec problème", "downloadSuccess": { "title": "Réponses déplacées vers l'onglet « Téléchargements »", diff --git a/i18n/translations/fr/response-api.json b/i18n/translations/fr/response-api.json index 2405f26391..3f85524cde 100644 --- a/i18n/translations/fr/response-api.json +++ b/i18n/translations/fr/response-api.json @@ -1,6 +1,9 @@ { "section-title": "Réponses (Pilote)", "stepOf": "Étape {{current}} de {{total}}", + "responsesPilot": { + "responsesSwitchLink": "Revenir à l'affichage classique des Réponses" + }, "not-supported": { "title": "Navigateur web non compatible", "body": "Ce navigateur ne peut pas utiliser l'API d'accès au système de fichiers. Essayez un autre navigateur ou autorisez votre navigateur à enregistrer des fichiers sur votre appareil." diff --git a/package.json b/package.json index dab7540094..97b5ddbf14 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,8 @@ "format:check": "prettier --check .", "test": "jest", "test:vitest": "vitest run", + "test:vitest:browser": "VITEST_BROWSER=true vitest --run", + "test:vitest:browser:watch": "VITEST_BROWSER=true vitest --watch", "test:watch:vitest": "vitest --watch", "test:watch": "DEBUG_PRINT_LIMIT=10000 jest --watch", "cypress": "cypress open", @@ -173,6 +175,10 @@ "@types/react-transition-group": "^4", "@types/unorm": "^1", "@types/uuid": "^8.3.4", + "@vitejs/plugin-react": "^5.1.1", + "@vitest/browser": "^4.0.13", + "@vitest/browser-playwright": "^4.0.13", + "@vitest/ui": "^4.0.13", "autoprefixer": "^10.4.21", "aws-sdk-client-mock": "^3.0.0", "axe-core": "^4.10.3", @@ -201,6 +207,7 @@ "jest-mock-extended": "^4.0.0", "lint-staged": "^15.2.2", "pino-pretty": "^11.0.0", + "playwright": "^1.56.1", "postcss": "^8.4.32", "postcss-import": "^15.1.0", "postcss-loader": "^7.3.4", @@ -213,7 +220,8 @@ "ts-node": "^10.9.2", "typescript": "^5.9.3", "vite-tsconfig-paths": "^5.1.4", - "vitest": "^4.0.6", + "vitest": "4.0.13", + "vitest-browser-react": "^2.0.2", "webpack": "^5.102.0" }, "browserslist": [ diff --git a/tests/browser/responses-pilot/AplClientSetter.tsx b/tests/browser/responses-pilot/AplClientSetter.tsx new file mode 100644 index 0000000000..9883f58a53 --- /dev/null +++ b/tests/browser/responses-pilot/AplClientSetter.tsx @@ -0,0 +1,14 @@ +import { useEffect } from "react"; +import { GCFormsApiClient } from "@responses-pilot/lib/apiClient"; +import { useResponsesContext } from "@responses-pilot/context/ResponsesContext"; + +// Test helper that sets API client on mount +export function ApiClientSetter({ mockClient }: { mockClient: GCFormsApiClient }) { + const { setApiClient } = useResponsesContext(); + + useEffect(() => { + setApiClient(mockClient); + }, [mockClient, setApiClient]); + + return null; +} diff --git a/tests/browser/responses-pilot/SelectApiKey.browser.vitest.tsx b/tests/browser/responses-pilot/SelectApiKey.browser.vitest.tsx new file mode 100644 index 0000000000..d47a2542be --- /dev/null +++ b/tests/browser/responses-pilot/SelectApiKey.browser.vitest.tsx @@ -0,0 +1,128 @@ +import { describe, it, expect, beforeAll } from "vitest"; +import { page } from "@vitest/browser/context"; +import { SelectApiKey } from "@responses-pilot/load-key/SelectApiKey"; +import { render } from "./testUtils"; +import { GCFormsApiClient } from "@responses-pilot/lib/apiClient"; +import { setupFonts } from "./testHelpers"; + +import "@root/styles/app.scss"; + +describe("SelectApiKey - Browser Mode", () => { + beforeAll(() => { + setupFonts(); + }); + + it("should verify switch link exists", async () => { + await render(); + + // Check that the switch back link shows translated text, not the key + const switchLink = page.getByTestId("responses-pilot-switch-back-link"); + await expect.element(switchLink).toBeInTheDocument(); + + // Get the actual text content + const linkElement = await switchLink.element(); + const textContent = linkElement.textContent; + + // Should show translated text + expect(textContent).toBe("Switch back to the classic Responses view"); + // Should NOT show the translation key + expect(textContent).not.toBe("responsesPilot.responsesSwitchLink"); + }); + + it("should render the load key page", async () => { + await render(); + + await expect.element(page.getByTestId("load-key-heading")).toBeInTheDocument(); + await expect + .element(page.getByTestId("load-key-heading")) + .toHaveTextContent("Upload your API key file"); + }); + + it("should have Continue button disabled initially", async () => { + await render(); + + await expect.element(page.getByTestId("continue-button")).toBeDisabled(); + }); + + it("should show 'Lost your key?' link and open popover on click", async () => { + await render(); + + await expect.element(page.getByTestId("lost-key-link")).toBeInTheDocument(); + await page.getByTestId("lost-key-link").click(); + + await expect.element(page.getByTestId("lost-key-popover")).toBeInTheDocument(); + await expect + .element(page.getByTestId("lost-key-popover-title")) + .toHaveTextContent("Don't have your API key?"); + }); + + it("should open file picker when clicking Upload API Key", async () => { + let filePickerOpened = false; + const mockShowOpenFilePicker = async () => { + filePickerOpened = true; + throw new DOMException("User cancelled", "AbortError"); + }; + + await render(, { + overrides: { showOpenFilePicker: mockShowOpenFilePicker }, + }); + + await page.getByTestId("load-api-key-button").click(); + + expect(filePickerOpened).toBe(true); + }); + + describe("When API client exists", () => { + it("should hide load key UI when API client is set", async () => { + const mockApiClient = { + getNewFormSubmissions: async () => [], + formId: "test-form", + } as unknown as GCFormsApiClient; + + await render(, { mockApiClient }); + + // Load key button should not be visible + const loadKeyButton = document.querySelector('[data-testid="load-api-key-button"]'); + expect(loadKeyButton).toBeNull(); + + await expect.element(page.getByTestId("no-responses")).toBeInTheDocument(); + }); + + it("should show 'No new responses' when there are no submissions", async () => { + const mockApiClient = { + getNewFormSubmissions: async () => [], + formId: "test-form", + } as unknown as GCFormsApiClient; + + await render(, { mockApiClient }); + + await expect.element(page.getByTestId("no-responses")).toBeInTheDocument(); + await expect.element(page.getByTestId("no-responses-heading")).toBeInTheDocument(); + }); + + it("should show 'New responses available' when there are submissions", async () => { + const mockApiClient = { + getNewFormSubmissions: async () => { + return [ + { + confirmationCode: "TEST-123", + createdAt: new Date().toISOString(), + name: "Test Submission", + }, + { + confirmationCode: "TEST-456", + createdAt: new Date().toISOString(), + name: "Test Submission 2", + }, + ]; + }, + formId: "test-form", + } as unknown as GCFormsApiClient; + + await render(, { mockApiClient }); + + await expect.element(page.getByTestId("responses-available")).toBeInTheDocument(); + await expect.element(page.getByTestId("new-responses-heading")).toBeInTheDocument(); + }); + }); +}); diff --git a/tests/browser/responses-pilot/SelectLocation.browser.vitest.tsx b/tests/browser/responses-pilot/SelectLocation.browser.vitest.tsx new file mode 100644 index 0000000000..70ac93ef55 --- /dev/null +++ b/tests/browser/responses-pilot/SelectLocation.browser.vitest.tsx @@ -0,0 +1,84 @@ +import { describe, it, expect, beforeAll, vi } from "vitest"; +import { page } from "@vitest/browser/context"; +import { SelectLocation } from "@responses-pilot/location/SelectLocation"; +import { render } from "./testUtils"; +import { setupFonts } from "./testHelpers"; + +import "@root/styles/app.scss"; + +// Mock the native-file-system-adapter module +vi.mock("native-file-system-adapter", async () => { + const actual = await vi.importActual("native-file-system-adapter"); + return { + ...actual, + showDirectoryPicker: vi.fn(), + }; +}); + +describe("SelectLocation - Browser Mode", () => { + beforeAll(() => { + setupFonts(); + }); + + it("should render the location selection page", async () => { + await render(); + + // Check for step indicator + const stepIndicator = page.getByTestId("step-indicator"); + await expect.element(stepIndicator).toBeInTheDocument(); + await expect.element(stepIndicator).toHaveTextContent("Step 2 of 3"); + + // Check for title + const title = page.getByTestId("location-page-title"); + await expect.element(title).toBeInTheDocument(); + }); + + it("should display the directory picker when no directory is selected", async () => { + await render(); + + // Check for directory picker button + const pickerButton = page.getByTestId("choose-location-button"); + await expect.element(pickerButton).toBeInTheDocument(); + }); + + it("should have Continue button disabled initially", async () => { + await render(); + + const continueButton = page.getByTestId("continue-button"); + await expect.element(continueButton).toBeInTheDocument(); + await expect.element(continueButton).toBeDisabled(); + }); + + it("should have a Back button that links to load-key with reset", async () => { + await render(); + + const backButton = page.getByTestId("back-button"); + await expect.element(backButton).toBeInTheDocument(); + await expect + .element(backButton) + .toHaveAttribute("href", "/en/form-builder/test-form/responses-pilot/load-key?reset=true"); + }); + + it("should show toast when directory is selected", async () => { + const { showDirectoryPicker } = await import("native-file-system-adapter"); + + // Mock the directory picker to return a mock handle + vi.mocked(showDirectoryPicker).mockResolvedValueOnce({ + name: "test-directory", + kind: "directory", + getDirectoryHandle: vi.fn().mockResolvedValue({}), + } as never); + + await render(); + + const pickerButton = page.getByTestId("choose-location-button"); + await pickerButton.click(); + + // Wait for toast to appear + await new Promise((resolve) => setTimeout(resolve, 500)); + + // Check for toast message + const toast = page.getByText(/test-directory/i); + await expect.element(toast).toBeInTheDocument(); + }); +}); diff --git a/tests/browser/responses-pilot/testHelpers.ts b/tests/browser/responses-pilot/testHelpers.ts new file mode 100644 index 0000000000..8f79737054 --- /dev/null +++ b/tests/browser/responses-pilot/testHelpers.ts @@ -0,0 +1,19 @@ +/** + * Test helpers for browser mode tests + */ + +/** + * Loads Google Fonts and sets up CSS variables for Noto Sans and Lato fonts. + * Call this in browser mode tests to ensure proper font rendering. + */ +export function setupFonts() { + const fontLink = document.createElement("link"); + fontLink.href = + "https://fonts.googleapis.com/css2?family=Noto+Sans:wght@100;200;300;400;500;600;700;800;900&family=Lato:wght@400;700&display=swap"; + fontLink.rel = "stylesheet"; + document.head.appendChild(fontLink); + + // Add font CSS variables to html element + document.documentElement.style.setProperty("--font-noto-sans", "'Noto Sans', sans-serif"); + document.documentElement.style.setProperty("--font-lato", "'Lato', sans-serif"); +} diff --git a/tests/browser/responses-pilot/testUtils.tsx b/tests/browser/responses-pilot/testUtils.tsx new file mode 100644 index 0000000000..a17f6448d9 --- /dev/null +++ b/tests/browser/responses-pilot/testUtils.tsx @@ -0,0 +1,99 @@ +import { ReactNode } from "react"; +import { render as vitestRender, cleanup } from "vitest-browser-react"; +import { BrowserResponsesAppProvider } from "@responses-pilot/context/BrowserResponsesAppProvider"; +import { ResponsesProvider } from "@responses-pilot/context/ResponsesContext"; +import { ContentWrapper } from "@responses-pilot/ContentWrapper"; +import { PilotBadge } from "@clientComponents/globals/PilotBadge"; +import { ApiClientSetter } from "./AplClientSetter"; +import { GCFormsApiClient } from "@responses-pilot/lib/apiClient"; +import { showOpenFilePicker } from "native-file-system-adapter"; +import { ToastContainer } from "@formBuilder/components/shared/Toast"; + +// Import to trigger i18next initialization +import "@root/i18n/client"; +// Import i18next instance +import i18next from "i18next"; + +interface RenderWithProvidersOptions { + locale?: string; + formId?: string; + mockApiClient?: GCFormsApiClient; + overrides?: { + showOpenFilePicker?: typeof showOpenFilePicker; + }; + children: ReactNode; +} + +/** + * Wraps component with standard test providers and layout + */ +export function TestWrapper({ + locale = "en", + formId = "test-form", + mockApiClient, + overrides, + children, +}: RenderWithProvidersOptions) { + return ( + + + {mockApiClient && } +

Responses

+ + {children} + + + +
+
+ ); +} + +/** + * Custom render function that wraps component with TestWrapper + * + * Note: Use `page` from 'vitest/browser' for locators with proper types: + * import { page } from 'vitest/browser' + * const element = page.getByTestId('my-id') + */ +export async function render( + children: ReactNode, + options?: Omit +) { + // Wait for i18next to load translations + await waitForI18next(options?.locale || "en"); + + // Render the component + await vitestRender({children}); + + // Wait a bit more for React to re-render with translations + await new Promise((resolve) => setTimeout(resolve, 200)); +} + +/** + * Wait for i18next to finish loading translations + */ +async function waitForI18next(locale: string) { + // Ensure i18next is initialized + if (!i18next.isInitialized) { + await new Promise((resolve) => { + i18next.on("initialized", resolve); + // Timeout after 5 seconds + setTimeout(resolve, 5000); + }); + } + + // Change language to the test locale + if (i18next.language !== locale) { + await i18next.changeLanguage(locale); + } + + // Load the required namespaces + const namespaces = ["response-api", "common"]; + await i18next.loadNamespaces(namespaces); + + // Translations are now loaded and ready +} + +// Re-export cleanup for manual use in tests +export { cleanup }; diff --git a/tsconfig.json b/tsconfig.json index 2500a3fd6d..65159db675 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,6 +21,9 @@ "@lib/*": ["lib/*"], "@api/*": ["app/api/*"], "@formBuilder/*": ["app/(gcforms)/[locale]/(form administration)/form-builder/*"], + "@responses-pilot/*": [ + "app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot/*" + ], "@serverComponents/*": ["components/serverComponents/*"], "@clientComponents/*": ["components/clientComponents/*"], "@jestFixtures/*": ["__fixtures__/*"], diff --git a/vitest.config.mts b/vitest.config.mts index 157cc0d2d9..165f24e091 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -1,16 +1,61 @@ import { defineConfig } from "vitest/config"; import tsconfigPaths from "vite-tsconfig-paths"; +import react from "@vitejs/plugin-react"; +import { playwright } from "@vitest/browser-playwright"; +import path from "path"; export default defineConfig({ - plugins: [tsconfigPaths()], + plugins: [react(), tsconfigPaths()], + resolve: { + alias: { + "@responses-pilot": path.resolve(__dirname, "app/(gcforms)/[locale]/(form administration)/form-builder/[id]/responses-pilot"), + }, + }, + define: { + "process.env.VITEST_BROWSER": JSON.stringify(process.env.VITEST_BROWSER || "false"), + "process.env.VITEST_WATCH": JSON.stringify(process.env.VITEST_WATCH || "false"), + global: "globalThis", + }, + css: { + postcss: "./postcss.config.js", + }, + optimizeDeps: { + include: [ + "@testing-library/react", + "react", + "react-dom", + "next/navigation", + "native-file-system-adapter", + ], + }, test: { - globals: true, // migration from Jest - By default, vitest does not provide global APIs for explicitness + globals: true, environment: "node", - // Note: The following pattern .vitest.ts has been added to avoid conflicts with jest tests co-located with the source code - include: [ - "__vitests__/**/*.test.ts", - "lib/vitests/**/*.test.ts", - "**/*.vitest.+(ts|tsx|js|jsx)", + include: + process.env.VITEST_BROWSER === "true" + ? ["tests/browser/**/*.browser.vitest.+(ts|tsx|js|jsx)"] + : ["__vitests__/**/*.test.ts", "lib/vitests/**/*.test.ts", "**/*.vitest.+(ts|tsx|js|jsx)"], + exclude: [ + "**/node_modules/**", + "**/dist/**", + "**/cypress/**", + ...(process.env.VITEST_BROWSER === "true" ? [] : ["**/*.browser.vitest.+(ts|tsx|js|jsx)"]), ], + browser: { + enabled: process.env.VITEST_BROWSER === "true", + provider: playwright({ + launchOptions: { + slowMo: 250, // Slow down by 250ms for better visibility + }, + }), + instances: [{ browser: "chromium" }], + headless: process.env.CI === "true", // Headless in CI, headed locally + viewport: { + width: 1920, + height: 1080, + }, + }, + isolate: true, + fileParallelism: false, }, }); diff --git a/yarn.lock b/yarn.lock index cdcd731eff..83dd36d93c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1144,7 +1144,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.23.9, @babel/core@npm:^7.27.4, @babel/core@npm:^7.28.0": +"@babel/core@npm:^7.23.9, @babel/core@npm:^7.27.4, @babel/core@npm:^7.28.0, @babel/core@npm:^7.28.5": version: 7.28.5 resolution: "@babel/core@npm:7.28.5" dependencies: @@ -1539,6 +1539,28 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-react-jsx-self@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-transform-react-jsx-self@npm:7.27.1" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.27.1" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/00a4f917b70a608f9aca2fb39aabe04a60aa33165a7e0105fd44b3a8531630eb85bf5572e9f242f51e6ad2fa38c2e7e780902176c863556c58b5ba6f6e164031 + languageName: node + linkType: hard + +"@babel/plugin-transform-react-jsx-source@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-transform-react-jsx-source@npm:7.27.1" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.27.1" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10c0/5e67b56c39c4d03e59e03ba80692b24c5a921472079b63af711b1d250fc37c1733a17069b63537f750f3e937ec44a42b1ee6a46cd23b1a0df5163b17f741f7f2 + languageName: node + linkType: hard + "@babel/plugin-transform-typescript@npm:^7.28.5": version: 7.28.5 resolution: "@babel/plugin-transform-typescript@npm:7.28.5" @@ -4665,6 +4687,13 @@ __metadata: languageName: node linkType: hard +"@polka/url@npm:^1.0.0-next.24": + version: 1.0.0-next.29 + resolution: "@polka/url@npm:1.0.0-next.29" + checksum: 10c0/0d58e081844095cb029d3c19a659bfefd09d5d51a2f791bc61eba7ea826f13d6ee204a8a448c2f5a855c17df07b37517373ff916dd05801063c0568ae9937684 + languageName: node + linkType: hard + "@preact/signals-core@npm:^1.11.0": version: 1.12.1 resolution: "@preact/signals-core@npm:1.12.1" @@ -7141,6 +7170,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/pluginutils@npm:1.0.0-beta.47": + version: 1.0.0-beta.47 + resolution: "@rolldown/pluginutils@npm:1.0.0-beta.47" + checksum: 10c0/eb0cfa7334d66f090c47eaac612174936b05f26e789352428cb6e03575b590f355de30d26b42576ea4e613d8887b587119d19b2e4b3a8909ceb232ca1cf746c8 + languageName: node + linkType: hard + "@rollup/rollup-android-arm-eabi@npm:4.53.2": version: 4.53.2 resolution: "@rollup/rollup-android-arm-eabi@npm:4.53.2" @@ -9244,6 +9280,57 @@ __metadata: languageName: node linkType: hard +"@vitejs/plugin-react@npm:^5.1.1": + version: 5.1.1 + resolution: "@vitejs/plugin-react@npm:5.1.1" + dependencies: + "@babel/core": "npm:^7.28.5" + "@babel/plugin-transform-react-jsx-self": "npm:^7.27.1" + "@babel/plugin-transform-react-jsx-source": "npm:^7.27.1" + "@rolldown/pluginutils": "npm:1.0.0-beta.47" + "@types/babel__core": "npm:^7.20.5" + react-refresh: "npm:^0.18.0" + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + checksum: 10c0/e590efaea1eabfbb1beb6e8c9fac0742fd299808e3368e63b2825ce24740adb8a28fcb2668b14b7ca1bdb42890cfefe94d02dd358dcbbf8a27ddf377b9a82abf + languageName: node + linkType: hard + +"@vitest/browser-playwright@npm:^4.0.13": + version: 4.0.13 + resolution: "@vitest/browser-playwright@npm:4.0.13" + dependencies: + "@vitest/browser": "npm:4.0.13" + "@vitest/mocker": "npm:4.0.13" + tinyrainbow: "npm:^3.0.3" + peerDependencies: + playwright: "*" + vitest: 4.0.13 + peerDependenciesMeta: + playwright: + optional: false + checksum: 10c0/5a387eb02534736a25cfff442e66e8c41ef97f0db744ffe8360e484af61d66db793cb44ba8681471b8c21ba509db1775f1ba688bc7f50325eee76918773848cb + languageName: node + linkType: hard + +"@vitest/browser@npm:4.0.13, @vitest/browser@npm:^4.0.13": + version: 4.0.13 + resolution: "@vitest/browser@npm:4.0.13" + dependencies: + "@vitest/mocker": "npm:4.0.13" + "@vitest/utils": "npm:4.0.13" + magic-string: "npm:^0.30.21" + pixelmatch: "npm:7.1.0" + pngjs: "npm:^7.0.0" + sirv: "npm:^3.0.2" + tinyrainbow: "npm:^3.0.3" + ws: "npm:^8.18.3" + peerDependencies: + vitest: 4.0.13 + checksum: 10c0/22c9297888a7288717cad706ca08159b3af05337a2f9b8da98fe74e683d534c8d816e40fece96f218d223a54c06762c5aa2a5db23ce8565c174ab9a70aade7f0 + languageName: node + linkType: hard + "@vitest/expect@npm:1.6.1": version: 1.6.1 resolution: "@vitest/expect@npm:1.6.1" @@ -9255,25 +9342,25 @@ __metadata: languageName: node linkType: hard -"@vitest/expect@npm:4.0.10": - version: 4.0.10 - resolution: "@vitest/expect@npm:4.0.10" +"@vitest/expect@npm:4.0.13": + version: 4.0.13 + resolution: "@vitest/expect@npm:4.0.13" dependencies: "@standard-schema/spec": "npm:^1.0.0" "@types/chai": "npm:^5.2.2" - "@vitest/spy": "npm:4.0.10" - "@vitest/utils": "npm:4.0.10" + "@vitest/spy": "npm:4.0.13" + "@vitest/utils": "npm:4.0.13" chai: "npm:^6.2.1" tinyrainbow: "npm:^3.0.3" - checksum: 10c0/b8d2f1872d32e2288861b4ae0a530671460b5bed15ffb0544e4efd56e824445bd99aa364fd10332a0dd045fdd4314f8c5a3ab184eb55a43ea8c7f4e8991c8500 + checksum: 10c0/1cd7dc02cb650d024826f2e20260d23c2b9ab6733341045ffb59be7af73402eecd2422198d7e4eac609710730b6d11f0faf22af0c074d65445ab88d9da7f6556 languageName: node linkType: hard -"@vitest/mocker@npm:4.0.10": - version: 4.0.10 - resolution: "@vitest/mocker@npm:4.0.10" +"@vitest/mocker@npm:4.0.13": + version: 4.0.13 + resolution: "@vitest/mocker@npm:4.0.13" dependencies: - "@vitest/spy": "npm:4.0.10" + "@vitest/spy": "npm:4.0.13" estree-walker: "npm:^3.0.3" magic-string: "npm:^0.30.21" peerDependencies: @@ -9284,16 +9371,16 @@ __metadata: optional: true vite: optional: true - checksum: 10c0/be72c1f1aa8b22f7f55e91f8e42ec39eb19e46a0d85b9f36745d14b831d370136ca4e20433934a0f0a076a07a1c9e54e309a3994e31a4f91115de8dc91615c8c + checksum: 10c0/667ec4fbb77a28ede1b055b9d962beed92c2dd2d83b7bab1ed22239578a7b399180a978e26ef136301c0bc7c57c75ca178cda55ec94081856437e3b4be4a3e19 languageName: node linkType: hard -"@vitest/pretty-format@npm:4.0.10": - version: 4.0.10 - resolution: "@vitest/pretty-format@npm:4.0.10" +"@vitest/pretty-format@npm:4.0.13": + version: 4.0.13 + resolution: "@vitest/pretty-format@npm:4.0.13" dependencies: tinyrainbow: "npm:^3.0.3" - checksum: 10c0/7a7d44aad921cad8b9049cb94be3e9ba695678489bfd1b2e049bb661d70f722c7657c95589ed0094976eeca878ddfa7e3f8dbc7c9de3ef9b281bcc77d0f01b0d + checksum: 10c0/c32ebd3457fd4b92fa89800b0ddaa2ca7de84df75be3c64f87ace006f3a3ec546a6ffd4c06f88e3161e80f9e10c83dfee61150e682eaa5a1871240d98c7ef0eb languageName: node linkType: hard @@ -9308,13 +9395,13 @@ __metadata: languageName: node linkType: hard -"@vitest/runner@npm:4.0.10": - version: 4.0.10 - resolution: "@vitest/runner@npm:4.0.10" +"@vitest/runner@npm:4.0.13": + version: 4.0.13 + resolution: "@vitest/runner@npm:4.0.13" dependencies: - "@vitest/utils": "npm:4.0.10" + "@vitest/utils": "npm:4.0.13" pathe: "npm:^2.0.3" - checksum: 10c0/bbd1bfabae5efb8e3b528b96312334a9be9af1a4ff792b1aa710209c2694ee2198498c654319e32215433fa7152a056ec22fa0766545a94fd77050ca6a0f5e2d + checksum: 10c0/e9f95b8a413f875123e5c32322dd92bd523d6e3ba25b054f0e865f42e01f82666b847535fe5ea2ff3238faa2df16cefc7e5845d3d5ccfecb3a96ab924d31e760 languageName: node linkType: hard @@ -9329,14 +9416,14 @@ __metadata: languageName: node linkType: hard -"@vitest/snapshot@npm:4.0.10": - version: 4.0.10 - resolution: "@vitest/snapshot@npm:4.0.10" +"@vitest/snapshot@npm:4.0.13": + version: 4.0.13 + resolution: "@vitest/snapshot@npm:4.0.13" dependencies: - "@vitest/pretty-format": "npm:4.0.10" + "@vitest/pretty-format": "npm:4.0.13" magic-string: "npm:^0.30.21" pathe: "npm:^2.0.3" - checksum: 10c0/21ca6098468175b8f766033ef7eb701027321c6c249b95ff0f96566b5d394e12d69a6b17049dec5b6af08f59a3e4ecf46cdb00e365dc26745099da086119db22 + checksum: 10c0/ad3fbe9ff30bc294811556f958e0014cb03888ea06ac7c05ab41e20c582fe8e27d8f4176aaf8a8e230fc6377124af30f5622173fb459b70a30ff9dd622664be2 languageName: node linkType: hard @@ -9349,10 +9436,27 @@ __metadata: languageName: node linkType: hard -"@vitest/spy@npm:4.0.10": - version: 4.0.10 - resolution: "@vitest/spy@npm:4.0.10" - checksum: 10c0/e6950fea42e5886e7ae6a991647060c84bcd1817262a1c0a73435e66bbbcc4f2078e29079822ccc2a884396fcfd0759036c14d2e23e062e52cb66b7c4cc7e347 +"@vitest/spy@npm:4.0.13": + version: 4.0.13 + resolution: "@vitest/spy@npm:4.0.13" + checksum: 10c0/64dc4c496eb9aacd3137beedccdb3265c895f8cd2626b3f76d7324ad944be5b1567ede2652eee407991796879270a63abdec4453c73185e637a1d7ff9cd1a009 + languageName: node + linkType: hard + +"@vitest/ui@npm:^4.0.13": + version: 4.0.13 + resolution: "@vitest/ui@npm:4.0.13" + dependencies: + "@vitest/utils": "npm:4.0.13" + fflate: "npm:^0.8.2" + flatted: "npm:^3.3.3" + pathe: "npm:^2.0.3" + sirv: "npm:^3.0.2" + tinyglobby: "npm:^0.2.15" + tinyrainbow: "npm:^3.0.3" + peerDependencies: + vitest: 4.0.13 + checksum: 10c0/7656762bc6a9c99850639d0809ada53ad4b842e4d9a8c7b82987b60bcf1675c98c077516a3777fce9580255538d0d050c92cb1e6f6296af6365f2387d7a972b9 languageName: node linkType: hard @@ -9368,13 +9472,13 @@ __metadata: languageName: node linkType: hard -"@vitest/utils@npm:4.0.10": - version: 4.0.10 - resolution: "@vitest/utils@npm:4.0.10" +"@vitest/utils@npm:4.0.13": + version: 4.0.13 + resolution: "@vitest/utils@npm:4.0.13" dependencies: - "@vitest/pretty-format": "npm:4.0.10" + "@vitest/pretty-format": "npm:4.0.13" tinyrainbow: "npm:^3.0.3" - checksum: 10c0/cfca5d33ac7b609e6ada34c17dfbe02322eb6c5016d17a9dc8dd1b6db3d7962bd39ca4f9e40f127cd3c5bb8a6193df8b7c99a3c3f30b3ae051e52fbe6bedd3e4 + checksum: 10c0/1b64872e82a652f11bfd813c0140eaae9b6e4ece39fc0e460ab2b3111b925892f1128f3b27f3a280471cfc404bb9c9289c59f8ca5387950ab35d024d154e9ec1 languageName: node linkType: hard @@ -13440,7 +13544,7 @@ __metadata: languageName: node linkType: hard -"flatted@npm:^3.2.9": +"flatted@npm:^3.2.9, flatted@npm:^3.3.3": version: 3.3.3 resolution: "flatted@npm:3.3.3" checksum: 10c0/e957a1c6b0254aa15b8cce8533e24165abd98fadc98575db082b786b5da1b7d72062b81bfdcd1da2f4d46b6ed93bec2434e62333e9b4261d79ef2e75a10dd538 @@ -13713,6 +13817,10 @@ __metadata: "@types/unorm": "npm:^1" "@types/uuid": "npm:^8.3.4" "@vercel/otel": "npm:^1.13.0" + "@vitejs/plugin-react": "npm:^5.1.1" + "@vitest/browser": "npm:^4.0.13" + "@vitest/browser-playwright": "npm:^4.0.13" + "@vitest/ui": "npm:^4.0.13" autoprefixer: "npm:^10.4.21" aws-sdk-client-mock: "npm:^3.0.0" axe-core: "npm:^4.10.3" @@ -13772,6 +13880,7 @@ __metadata: node-fetch: "npm:^3.3.2" pino: "npm:10.0.0" pino-pretty: "npm:^11.0.0" + playwright: "npm:^1.56.1" postcss: "npm:^8.4.32" postcss-import: "npm:^15.1.0" postcss-loader: "npm:^7.3.4" @@ -13805,7 +13914,8 @@ __metadata: uuid: "npm:9.0.1" valibot: "npm:^1.1.0" vite-tsconfig-paths: "npm:^5.1.4" - vitest: "npm:^4.0.6" + vitest: "npm:4.0.13" + vitest-browser-react: "npm:^2.0.2" webpack: "npm:^5.102.0" zustand: "npm:5.0.8" languageName: unknown @@ -16540,6 +16650,13 @@ __metadata: languageName: node linkType: hard +"mrmime@npm:^2.0.0": + version: 2.0.1 + resolution: "mrmime@npm:2.0.1" + checksum: 10c0/af05afd95af202fdd620422f976ad67dc18e6ee29beb03dd1ce950ea6ef664de378e44197246df4c7cdd73d47f2e7143a6e26e473084b9e4aa2095c0ad1e1761 + languageName: node + linkType: hard + "ms@npm:^2.1.1, ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" @@ -17434,6 +17551,17 @@ __metadata: languageName: node linkType: hard +"pixelmatch@npm:7.1.0": + version: 7.1.0 + resolution: "pixelmatch@npm:7.1.0" + dependencies: + pngjs: "npm:^7.0.0" + bin: + pixelmatch: bin/pixelmatch + checksum: 10c0/ff069f92edaa841ac9b58b0ab74e1afa1f3b5e770eea0218c96bac1da4e752f5f6b79a0f9c4ba6b02afb955d39b8c78bcc3cc884f8122b67a1f2efbbccbe1a73 + languageName: node + linkType: hard + "pkg-dir@npm:^4.1.0, pkg-dir@npm:^4.2.0": version: 4.2.0 resolution: "pkg-dir@npm:4.2.0" @@ -17481,7 +17609,7 @@ __metadata: languageName: node linkType: hard -"playwright@npm:1.56.1": +"playwright@npm:1.56.1, playwright@npm:^1.56.1": version: 1.56.1 resolution: "playwright@npm:1.56.1" dependencies: @@ -17496,6 +17624,13 @@ __metadata: languageName: node linkType: hard +"pngjs@npm:^7.0.0": + version: 7.0.0 + resolution: "pngjs@npm:7.0.0" + checksum: 10c0/0d4c7a0fd476a9c33df7d0a2a73e1d56537628a668841f6995c2bca070cf30819f9254a64363266bc14ef2fee47659dd3b4f2b18eec7ab65143015139f497b38 + languageName: node + linkType: hard + "possible-typed-array-names@npm:^1.0.0": version: 1.1.0 resolution: "possible-typed-array-names@npm:1.1.0" @@ -18468,6 +18603,13 @@ __metadata: languageName: node linkType: hard +"react-refresh@npm:^0.18.0": + version: 0.18.0 + resolution: "react-refresh@npm:0.18.0" + checksum: 10c0/34a262f7fd803433a534f50deb27a148112a81adcae440c7d1cbae7ef14d21ea8f2b3d783e858cb7698968183b77755a38b4d4b5b1d79b4f4689c2f6d358fff2 + languageName: node + linkType: hard + "react-remove-scroll-bar@npm:^2.3.7": version: 2.3.8 resolution: "react-remove-scroll-bar@npm:2.3.8" @@ -19569,6 +19711,17 @@ __metadata: languageName: node linkType: hard +"sirv@npm:^3.0.2": + version: 3.0.2 + resolution: "sirv@npm:3.0.2" + dependencies: + "@polka/url": "npm:^1.0.0-next.24" + mrmime: "npm:^2.0.0" + totalist: "npm:^3.0.0" + checksum: 10c0/5930e4397afdb14fbae13751c3be983af4bda5c9aadec832607dc2af15a7162f7d518c71b30e83ae3644b9a24cea041543cc969e5fe2b80af6ce8ea3174b2d04 + languageName: node + linkType: hard + "slash@npm:^3.0.0": version: 3.0.0 resolution: "slash@npm:3.0.0" @@ -20525,6 +20678,13 @@ __metadata: languageName: node linkType: hard +"totalist@npm:^3.0.0": + version: 3.0.1 + resolution: "totalist@npm:3.0.1" + checksum: 10c0/4bb1fadb69c3edbef91c73ebef9d25b33bbf69afe1e37ce544d5f7d13854cda15e47132f3e0dc4cafe300ddb8578c77c50a65004d8b6e97e77934a69aa924863 + languageName: node + linkType: hard + "tough-cookie@npm:^5.0.0, tough-cookie@npm:^5.1.1": version: 5.1.2 resolution: "tough-cookie@npm:5.1.2" @@ -21424,67 +21584,35 @@ __metadata: languageName: node linkType: hard -"vitest@npm:^1.0.0": - version: 1.6.1 - resolution: "vitest@npm:1.6.1" - dependencies: - "@vitest/expect": "npm:1.6.1" - "@vitest/runner": "npm:1.6.1" - "@vitest/snapshot": "npm:1.6.1" - "@vitest/spy": "npm:1.6.1" - "@vitest/utils": "npm:1.6.1" - acorn-walk: "npm:^8.3.2" - chai: "npm:^4.3.10" - debug: "npm:^4.3.4" - execa: "npm:^8.0.1" - local-pkg: "npm:^0.5.0" - magic-string: "npm:^0.30.5" - pathe: "npm:^1.1.1" - picocolors: "npm:^1.0.0" - std-env: "npm:^3.5.0" - strip-literal: "npm:^2.0.0" - tinybench: "npm:^2.5.1" - tinypool: "npm:^0.8.3" - vite: "npm:^5.0.0" - vite-node: "npm:1.6.1" - why-is-node-running: "npm:^2.2.2" +"vitest-browser-react@npm:^2.0.2": + version: 2.0.2 + resolution: "vitest-browser-react@npm:2.0.2" peerDependencies: - "@edge-runtime/vm": "*" - "@types/node": ^18.0.0 || >=20.0.0 - "@vitest/browser": 1.6.1 - "@vitest/ui": 1.6.1 - happy-dom: "*" - jsdom: "*" + "@types/react": ^18.0.0 || ^19.0.0 + "@types/react-dom": ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + vitest: ^4.0.0 peerDependenciesMeta: - "@edge-runtime/vm": - optional: true - "@types/node": - optional: true - "@vitest/browser": - optional: true - "@vitest/ui": - optional: true - happy-dom: + "@types/react": optional: true - jsdom: + "@types/react-dom": optional: true - bin: - vitest: vitest.mjs - checksum: 10c0/511d27d7f697683964826db2fad7ac303f9bc7eeb59d9422111dc488371ccf1f9eed47ac3a80eb47ca86b7242228ba5ca9cc3613290830d0e916973768cac215 + checksum: 10c0/95941efb09fa26533974029c70f5bc295b35de0ddef36e8852ce2e97221e10c660d4251af837f875a264a5b08ced965145a2d90a67d11dc595e91b16adbf3808 languageName: node linkType: hard -"vitest@npm:^4.0.6": - version: 4.0.10 - resolution: "vitest@npm:4.0.10" - dependencies: - "@vitest/expect": "npm:4.0.10" - "@vitest/mocker": "npm:4.0.10" - "@vitest/pretty-format": "npm:4.0.10" - "@vitest/runner": "npm:4.0.10" - "@vitest/snapshot": "npm:4.0.10" - "@vitest/spy": "npm:4.0.10" - "@vitest/utils": "npm:4.0.10" +"vitest@npm:4.0.13": + version: 4.0.13 + resolution: "vitest@npm:4.0.13" + dependencies: + "@vitest/expect": "npm:4.0.13" + "@vitest/mocker": "npm:4.0.13" + "@vitest/pretty-format": "npm:4.0.13" + "@vitest/runner": "npm:4.0.13" + "@vitest/snapshot": "npm:4.0.13" + "@vitest/spy": "npm:4.0.13" + "@vitest/utils": "npm:4.0.13" debug: "npm:^4.4.3" es-module-lexer: "npm:^1.7.0" expect-type: "npm:^1.2.2" @@ -21500,17 +21628,20 @@ __metadata: why-is-node-running: "npm:^2.3.0" peerDependencies: "@edge-runtime/vm": "*" + "@opentelemetry/api": ^1.9.0 "@types/debug": ^4.1.12 "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 - "@vitest/browser-playwright": 4.0.10 - "@vitest/browser-preview": 4.0.10 - "@vitest/browser-webdriverio": 4.0.10 - "@vitest/ui": 4.0.10 + "@vitest/browser-playwright": 4.0.13 + "@vitest/browser-preview": 4.0.13 + "@vitest/browser-webdriverio": 4.0.13 + "@vitest/ui": 4.0.13 happy-dom: "*" jsdom: "*" peerDependenciesMeta: "@edge-runtime/vm": optional: true + "@opentelemetry/api": + optional: true "@types/debug": optional: true "@types/node": @@ -21529,7 +21660,57 @@ __metadata: optional: true bin: vitest: vitest.mjs - checksum: 10c0/5da26cc0c2db1a905615e89eecb0bcc6da376088db77de41c2bf8a6037488671f3e03674659bde1ee806c8dcd822a1e327f7e74413313bdf1f2fff64ecd3b993 + checksum: 10c0/8582ab1848d5d7dbbac0b3a5eae2625f44d0db887f73da2ee8f588fb13c66fe8ea26dac05c26ebb43673b735bc246764f52969f7c7e25455dfb7c6274659ae2c + languageName: node + linkType: hard + +"vitest@npm:^1.0.0": + version: 1.6.1 + resolution: "vitest@npm:1.6.1" + dependencies: + "@vitest/expect": "npm:1.6.1" + "@vitest/runner": "npm:1.6.1" + "@vitest/snapshot": "npm:1.6.1" + "@vitest/spy": "npm:1.6.1" + "@vitest/utils": "npm:1.6.1" + acorn-walk: "npm:^8.3.2" + chai: "npm:^4.3.10" + debug: "npm:^4.3.4" + execa: "npm:^8.0.1" + local-pkg: "npm:^0.5.0" + magic-string: "npm:^0.30.5" + pathe: "npm:^1.1.1" + picocolors: "npm:^1.0.0" + std-env: "npm:^3.5.0" + strip-literal: "npm:^2.0.0" + tinybench: "npm:^2.5.1" + tinypool: "npm:^0.8.3" + vite: "npm:^5.0.0" + vite-node: "npm:1.6.1" + why-is-node-running: "npm:^2.2.2" + peerDependencies: + "@edge-runtime/vm": "*" + "@types/node": ^18.0.0 || >=20.0.0 + "@vitest/browser": 1.6.1 + "@vitest/ui": 1.6.1 + happy-dom: "*" + jsdom: "*" + peerDependenciesMeta: + "@edge-runtime/vm": + optional: true + "@types/node": + optional: true + "@vitest/browser": + optional: true + "@vitest/ui": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + bin: + vitest: vitest.mjs + checksum: 10c0/511d27d7f697683964826db2fad7ac303f9bc7eeb59d9422111dc488371ccf1f9eed47ac3a80eb47ca86b7242228ba5ca9cc3613290830d0e916973768cac215 languageName: node linkType: hard @@ -21849,7 +22030,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.18.0": +"ws@npm:^8.18.0, ws@npm:^8.18.3": version: 8.18.3 resolution: "ws@npm:8.18.3" peerDependencies: