Initial Commit
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Shared test utilities for the auth-context test split. Exposed under
|
||||
* `tests/setup/` so they are excluded from the istanbul coverage scope
|
||||
* (`include: src/**`) while still being available to multiple `.test.tsx`
|
||||
* files inside `src/context/`.
|
||||
*/
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import {
|
||||
useAuth,
|
||||
useIsAuthenticated,
|
||||
useUser,
|
||||
} from "../../src/context/auth-context.js";
|
||||
import type {
|
||||
AuthContextValue,
|
||||
AuthUser,
|
||||
} from "../../src/types/branding.types.js";
|
||||
|
||||
export interface FetchCall {
|
||||
url: string;
|
||||
init?: RequestInit;
|
||||
}
|
||||
|
||||
export interface AuthCaptureBag {
|
||||
value: AuthContextValue | null;
|
||||
}
|
||||
|
||||
export interface UserCaptureBag {
|
||||
value: AuthUser | null | unknown;
|
||||
}
|
||||
|
||||
export interface AuthedCaptureBag {
|
||||
value: boolean;
|
||||
}
|
||||
|
||||
export function makeFetch(
|
||||
handler: (call: FetchCall) => Response | Promise<Response>
|
||||
): {
|
||||
fetch: (
|
||||
input: string | URL | Request,
|
||||
init?: RequestInit
|
||||
) => Promise<Response>;
|
||||
calls: FetchCall[];
|
||||
} {
|
||||
const calls: FetchCall[] = [];
|
||||
return {
|
||||
calls,
|
||||
fetch: async (input, init) => {
|
||||
const url = typeof input === "string" ? input : input.toString();
|
||||
calls.push({ url, init });
|
||||
return handler({ url, init });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function jsonResponse(body: unknown, init?: ResponseInit): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
...init,
|
||||
});
|
||||
}
|
||||
|
||||
export function CaptureAuth({
|
||||
capture,
|
||||
}: {
|
||||
capture: AuthCaptureBag;
|
||||
}): ReactNode {
|
||||
capture.value = useAuth();
|
||||
return null;
|
||||
}
|
||||
|
||||
export function CaptureUser({
|
||||
capture,
|
||||
}: {
|
||||
capture: UserCaptureBag;
|
||||
}): ReactNode {
|
||||
capture.value = useUser();
|
||||
return null;
|
||||
}
|
||||
|
||||
export function CaptureAuthed({
|
||||
capture,
|
||||
}: {
|
||||
capture: AuthedCaptureBag;
|
||||
}): ReactNode {
|
||||
capture.value = useIsAuthenticated();
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* Shared helpers for the branding-context test split.
|
||||
*/
|
||||
|
||||
import { lightTheme, ThemeProvider } from "@suaveplan/react-ui/theme";
|
||||
import type { ReactNode } from "react";
|
||||
import {
|
||||
useBranding,
|
||||
useLogo,
|
||||
useTenantBranding,
|
||||
} from "../../src/context/branding-context.js";
|
||||
import type {
|
||||
BrandingContextValue,
|
||||
TenantBranding,
|
||||
} from "../../src/types/branding.types.js";
|
||||
|
||||
export interface Capture<T> {
|
||||
value: T;
|
||||
}
|
||||
|
||||
export function jsonResponse(body: unknown, status = 200): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
export const STATIC_BRANDING: TenantBranding = {
|
||||
tenantId: "acme",
|
||||
name: "Acme Co",
|
||||
primaryColor: "rgb(255, 0, 0)",
|
||||
secondaryColor: "rgb(0, 255, 0)",
|
||||
accentColor: "rgb(0, 0, 255)",
|
||||
fontFamily: "Inter, sans-serif",
|
||||
logo: "https://cdn/logo.svg",
|
||||
logoLight: "https://cdn/logo-light.svg",
|
||||
logoDark: "https://cdn/logo-dark.svg",
|
||||
favicon: "https://cdn/favicon.ico",
|
||||
customCss: ":root { --x: 1 }",
|
||||
};
|
||||
|
||||
export function CaptureBranding({
|
||||
capture,
|
||||
}: {
|
||||
capture: Capture<BrandingContextValue | null>;
|
||||
}): ReactNode {
|
||||
capture.value = useBranding();
|
||||
return null;
|
||||
}
|
||||
|
||||
export function CaptureTenant({
|
||||
capture,
|
||||
}: {
|
||||
capture: Capture<TenantBranding | null>;
|
||||
}): ReactNode {
|
||||
capture.value = useTenantBranding();
|
||||
return null;
|
||||
}
|
||||
|
||||
export function CaptureLogo({
|
||||
capture,
|
||||
preferDark,
|
||||
}: {
|
||||
capture: Capture<string | undefined>;
|
||||
preferDark?: boolean;
|
||||
}): ReactNode {
|
||||
capture.value = useLogo(preferDark);
|
||||
return null;
|
||||
}
|
||||
|
||||
export function wrapTheme(children: ReactNode): ReactNode {
|
||||
return (
|
||||
<ThemeProvider themes={[lightTheme]} defaultTheme="light">
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function resetBrandingDom(): void {
|
||||
const root = document.documentElement;
|
||||
for (const prop of [
|
||||
"--brand-primary",
|
||||
"--brand-secondary",
|
||||
"--brand-accent",
|
||||
"--brand-font",
|
||||
]) {
|
||||
root.style.removeProperty(prop);
|
||||
}
|
||||
document.title = "";
|
||||
const styleEl = document.getElementById("tenant-custom-css");
|
||||
if (styleEl !== null) {
|
||||
styleEl.remove();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Shared test utilities for the form-component tests. Each form needs an
|
||||
* `<AuthProvider>` + `<BrandingProvider>` wrap with controllable seams,
|
||||
* inside a `<TranslationProvider>` so `useTranslation()` resolves.
|
||||
*/
|
||||
|
||||
import { createI18n } from "@suaveplan/i18n/plugin";
|
||||
import { TranslationProvider } from "@suaveplan/react-i18n";
|
||||
import { lightTheme, ThemeProvider } from "@suaveplan/react-ui/theme";
|
||||
import type { ReactNode } from "react";
|
||||
import { AuthProvider } from "../../src/context/auth-context.js";
|
||||
import { BrandingProvider } from "../../src/context/branding-context.js";
|
||||
import type {
|
||||
AuthConfig,
|
||||
TenantBranding,
|
||||
} from "../../src/types/branding.types.js";
|
||||
|
||||
export interface FormFetchCall {
|
||||
url: string;
|
||||
init?: RequestInit;
|
||||
}
|
||||
|
||||
export function makeFetch(
|
||||
handler: (call: FormFetchCall) => Response | Promise<Response>
|
||||
): {
|
||||
fetch: (
|
||||
input: string | URL | Request,
|
||||
init?: RequestInit
|
||||
) => Promise<Response>;
|
||||
calls: FormFetchCall[];
|
||||
} {
|
||||
const calls: FormFetchCall[] = [];
|
||||
return {
|
||||
calls,
|
||||
fetch: async (input, init) => {
|
||||
const url = typeof input === "string" ? input : input.toString();
|
||||
calls.push({ url, init });
|
||||
return handler({ url, init });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function jsonResponse(body: unknown, status = 200): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
export const FORM_TEST_BRANDING: TenantBranding = {
|
||||
tenantId: "test",
|
||||
name: "Test Co",
|
||||
primaryColor: "#3B82F6",
|
||||
secondaryColor: "#10B981",
|
||||
logo: "https://cdn/logo.svg",
|
||||
logoLight: "https://cdn/logo-light.svg",
|
||||
};
|
||||
|
||||
export interface FormWrapperOptions {
|
||||
authConfig?: Partial<AuthConfig> | undefined;
|
||||
navigated?: string[] | undefined;
|
||||
}
|
||||
|
||||
const sharedI18n = createI18n({
|
||||
defaultLocale: "en-GB",
|
||||
messages: { "en-GB": {} },
|
||||
});
|
||||
|
||||
export function FormWrapper({
|
||||
children,
|
||||
options = {},
|
||||
}: {
|
||||
children: ReactNode;
|
||||
options?: FormWrapperOptions;
|
||||
}): ReactNode {
|
||||
const fetchImpl =
|
||||
options.authConfig?.fetch ?? makeFetch(() => jsonResponse({})).fetch;
|
||||
const navigate =
|
||||
options.authConfig?.navigate ??
|
||||
((url: string): void => {
|
||||
options.navigated?.push(url);
|
||||
});
|
||||
const config: AuthConfig = {
|
||||
authUrl: "https://auth.example.com",
|
||||
...options.authConfig,
|
||||
fetch: fetchImpl,
|
||||
navigate,
|
||||
};
|
||||
return (
|
||||
<TranslationProvider i18n={sharedI18n}>
|
||||
<ThemeProvider themes={[lightTheme]} defaultTheme="light">
|
||||
<BrandingProvider staticBranding={FORM_TEST_BRANDING}>
|
||||
<AuthProvider config={config} initialUser={null}>
|
||||
{children}
|
||||
</AuthProvider>
|
||||
</BrandingProvider>
|
||||
</ThemeProvider>
|
||||
</TranslationProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Minimal happy-dom polyfills for `@suaveplan/react-auth-whitelabel` tests.
|
||||
*
|
||||
* happy-dom from `catalog:testing` already provides a sufficient DOM for
|
||||
* the controlled-input forms and contexts in this package, but the
|
||||
* `BrandingProvider` redirects `document.title`, favicon link, and
|
||||
* `<style id="tenant-custom-css">` writes through APIs that happy-dom
|
||||
* implements partially. We do not need ResizeObserver or matchMedia.
|
||||
*
|
||||
* The file exists so the vitest setup pipeline can extend it later
|
||||
* without churning the config — currently it is intentionally a no-op.
|
||||
*/
|
||||
export {};
|
||||
Reference in New Issue
Block a user