// @vitest-environment happy-dom
import React, { act, useEffect } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
ComposerRuntimeAdaptersProvider,
useComposerRuntimeAdapters,
type ComposerRuntimeAdapters,
} from "./runtime-adapters.js";
let container: HTMLDivElement;
let root: Root;
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
});
afterEach(() => {
act(() => root.unmount());
container.remove();
});
describe("ComposerRuntimeAdaptersProvider", () => {
it("interpolates fallback translations without a provider", () => {
let translated = "";
function Consumer() {
const runtime = useComposerRuntimeAdapters();
translated = runtime.translate!("attachment.tooLarge", {
defaultValue: '"{{name}}" is {{size}} MB (max {{maxSize}} MB)',
name: "report.pdf",
size: 12.5,
maxSize: 10,
});
return null;
}
act(() => root.render());
expect(translated).toBe('"report.pdf" is 12.5 MB (max 10 MB)');
});
it("keeps the context value stable so consumer effects do not refire", () => {
const readAppState = vi.fn(() => undefined);
const adapters: ComposerRuntimeAdapters = { voice: { readAppState } };
const seen: ComposerRuntimeAdapters[] = [];
function Consumer() {
const runtime = useComposerRuntimeAdapters();
seen.push(runtime);
// Mirrors VoiceButton's voice-input-preference read.
useEffect(() => {
void runtime.voice!.readAppState!("voice-input-preference");
}, [runtime]);
return null;
}
function Tree({ tick }: { tick: number }) {
return (
{tick}
);
}
act(() => root.render());
act(() => root.render());
act(() => root.render());
expect(seen.length).toBeGreaterThan(1);
expect(new Set(seen).size).toBe(1);
expect(readAppState).toHaveBeenCalledTimes(1);
});
it("rebuilds the context value when the adapters prop changes", () => {
const seen: ComposerRuntimeAdapters[] = [];
function Consumer() {
seen.push(useComposerRuntimeAdapters());
return null;
}
const render = (adapters: ComposerRuntimeAdapters) =>
act(() =>
root.render(
,
),
);
render({ resolvePath: (path) => `/a${path}` });
render({ resolvePath: (path) => `/b${path}` });
expect(seen.at(-1)!.resolvePath!("/x")).toBe("/b/x");
});
});