Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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>
);
}

View File

@@ -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 {};