Initial Commit
This commit is contained in:
101
packages/ui/react/react-ui/tests/setup/polyfills.ts
Normal file
101
packages/ui/react/react-ui/tests/setup/polyfills.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* happy-dom polyfills tailored for `@suaveplan/react-ui` tests.
|
||||
*
|
||||
* happy-dom (catalog:testing) provides a reasonably complete DOM, but a
|
||||
* handful of browser globals that the chassis touches are either missing
|
||||
* or ship stub implementations:
|
||||
*
|
||||
* - `matchMedia` — `useMobile` and the FOUC inline script both poll
|
||||
* `prefers-color-scheme` / `prefers-reduced-motion` / max-width queries
|
||||
* via `window.matchMedia`. happy-dom includes a partial implementation;
|
||||
* we install a controllable wrapper so individual tests can flip the
|
||||
* stored result.
|
||||
* - `ResizeObserver` / `IntersectionObserver` — Radix UI primitives we
|
||||
* re-export call these in their effect setup. happy-dom returns
|
||||
* `undefined` for both. We polyfill with no-op classes so component
|
||||
* mounts do not throw during render.
|
||||
*
|
||||
* The polyfills are idempotent: if `globalThis` already exposes a
|
||||
* functional implementation, we leave it alone.
|
||||
*/
|
||||
|
||||
interface MatchMediaState {
|
||||
readonly matches: boolean;
|
||||
readonly media: string;
|
||||
}
|
||||
|
||||
class FakeMediaQueryList {
|
||||
public readonly matches: boolean;
|
||||
public readonly media: string;
|
||||
private readonly listeners = new Set<(ev: MatchMediaState) => void>();
|
||||
|
||||
public constructor(media: string, matches: boolean) {
|
||||
this.media = media;
|
||||
this.matches = matches;
|
||||
}
|
||||
|
||||
public addEventListener(
|
||||
_evt: string,
|
||||
listener: (ev: MatchMediaState) => void
|
||||
): void {
|
||||
this.listeners.add(listener);
|
||||
}
|
||||
|
||||
public removeEventListener(
|
||||
_evt: string,
|
||||
listener: (ev: MatchMediaState) => void
|
||||
): void {
|
||||
this.listeners.delete(listener);
|
||||
}
|
||||
|
||||
/** Legacy alias used by some libraries. */
|
||||
public addListener(listener: (ev: MatchMediaState) => void): void {
|
||||
this.listeners.add(listener);
|
||||
}
|
||||
|
||||
public removeListener(listener: (ev: MatchMediaState) => void): void {
|
||||
this.listeners.delete(listener);
|
||||
}
|
||||
|
||||
public dispatchEvent(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__reactUiMatchMediaResult?: (query: string) => boolean;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof globalThis.window !== "undefined") {
|
||||
if (typeof globalThis.window.matchMedia !== "function") {
|
||||
globalThis.window.matchMedia = ((query: string) => {
|
||||
const evaluator = (globalThis.window.__reactUiMatchMediaResult ??
|
||||
(() => false)) as (q: string) => boolean;
|
||||
return new FakeMediaQueryList(query, evaluator(query));
|
||||
}) as Window["matchMedia"];
|
||||
}
|
||||
|
||||
if (typeof globalThis.ResizeObserver === "undefined") {
|
||||
globalThis.ResizeObserver = class FakeResizeObserver {
|
||||
public observe(): void {}
|
||||
public unobserve(): void {}
|
||||
public disconnect(): void {}
|
||||
} as unknown as typeof ResizeObserver;
|
||||
}
|
||||
|
||||
if (typeof globalThis.IntersectionObserver === "undefined") {
|
||||
globalThis.IntersectionObserver = class FakeIntersectionObserver {
|
||||
public readonly root: Element | null = null;
|
||||
public readonly rootMargin: string = "0px";
|
||||
public readonly thresholds: readonly number[] = [];
|
||||
public observe(): void {}
|
||||
public unobserve(): void {}
|
||||
public disconnect(): void {}
|
||||
public takeRecords(): readonly IntersectionObserverEntry[] {
|
||||
return [];
|
||||
}
|
||||
} as unknown as typeof IntersectionObserver;
|
||||
}
|
||||
}
|
||||
68
packages/ui/react/react-ui/tests/setup/test-i18n.ts
Normal file
68
packages/ui/react/react-ui/tests/setup/test-i18n.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Helper for spinning up a real `@suaveplan/i18n` `I18nInstance` plus
|
||||
* its bundled translations under a fresh `I18nInstance` per test. No
|
||||
* mocks — every consumer of an i18n instance in the test suite uses
|
||||
* this helper so the wiring matches production exactly.
|
||||
*
|
||||
* The bundled `react-ui` JSON files ship FLAT keys
|
||||
* (`"react-ui.button.iconOnly.fallback": "Action"`). The catalog's `t()`
|
||||
* walks dot-separated paths, so we nest the flat bag into a deep
|
||||
* record (`{ react-ui: { button: { iconOnly: { fallback: "Action" } } } }`)
|
||||
* before merging into the catalog. This mirrors the chassis
|
||||
* `ReactUiProvider`'s `nestFlatKeys` step exactly so production and
|
||||
* tests resolve keys via the same pathway.
|
||||
*/
|
||||
|
||||
import { createI18n, type I18nInstance } from "@suaveplan/i18n/plugin";
|
||||
import { reactUiBundles } from "../../src/locales/index.js";
|
||||
|
||||
export interface CreateTestI18nOptions {
|
||||
readonly locale?: "en-GB" | "zh-CN" | "ar-AE" | "cy-GB" | "ru-RU";
|
||||
}
|
||||
|
||||
type NestedTranslationRecord = {
|
||||
[key: string]: string | NestedTranslationRecord;
|
||||
};
|
||||
|
||||
function nestFlatKeys(bag: Record<string, string>): NestedTranslationRecord {
|
||||
const out: NestedTranslationRecord = {};
|
||||
for (const [flatKey, value] of Object.entries(bag)) {
|
||||
const parts = flatKey.split(".");
|
||||
let cursor: NestedTranslationRecord = out;
|
||||
for (let i = 0; i < parts.length - 1; i += 1) {
|
||||
const seg = parts[i] as string;
|
||||
const existing = cursor[seg];
|
||||
if (
|
||||
typeof existing !== "object" ||
|
||||
existing === null ||
|
||||
Array.isArray(existing)
|
||||
) {
|
||||
cursor[seg] = {};
|
||||
}
|
||||
cursor = cursor[seg] as NestedTranslationRecord;
|
||||
}
|
||||
cursor[parts[parts.length - 1] as string] = value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an `I18nInstance` seeded with the chassis locale bundles.
|
||||
* Useful for tests that need a Provider but want deterministic
|
||||
* translations.
|
||||
*/
|
||||
export async function createTestI18n(
|
||||
options: CreateTestI18nOptions = {}
|
||||
): Promise<I18nInstance> {
|
||||
const locale = options.locale ?? "en-GB";
|
||||
const i18n = createI18n({
|
||||
defaultLocale: locale,
|
||||
fallbackLocale: "en-GB",
|
||||
});
|
||||
for (const code of ["en-GB", "zh-CN", "ar-AE", "cy-GB", "ru-RU"] as const) {
|
||||
const bundle = await reactUiBundles[code]();
|
||||
const nested = nestFlatKeys(bundle as Record<string, string>);
|
||||
i18n.catalog.mergeMessages(code, nested as Record<string, unknown>);
|
||||
}
|
||||
return i18n;
|
||||
}
|
||||
Reference in New Issue
Block a user