refactor(characters): drive shared UI from active character

This commit is contained in:
2026-07-17 19:33:59 +08:00
parent 8bb1e21886
commit 7bd5defa5e
42 changed files with 335 additions and 118 deletions
@@ -1,3 +1,4 @@
import type { ReactNode } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it, vi } from "vitest";
@@ -25,8 +26,8 @@ vi.mock("@/router/use-app-navigator", () => ({
describe("chat Tailwind components", () => {
it("renders MessageAvatar AI and user branches with image utilities", () => {
const aiHtml = renderToStaticMarkup(<MessageAvatar isFromAI={true} />);
const userHtml = renderToStaticMarkup(
const aiHtml = renderWithCharacter(<MessageAvatar isFromAI={true} />);
const userHtml = renderWithCharacter(
<MessageAvatar isFromAI={false} userAvatarUrl="/user-avatar.png" />,
);
@@ -71,8 +72,8 @@ describe("chat Tailwind components", () => {
});
it("renders PrivateMessageCard with fallback and loading states", () => {
const fallbackHtml = renderToStaticMarkup(<PrivateMessageCard />);
const unlockingHtml = renderToStaticMarkup(
const fallbackHtml = renderWithCharacter(<PrivateMessageCard />);
const unlockingHtml = renderWithCharacter(
<PrivateMessageCard hint="Premium secret" isUnlocking onUnlock={() => undefined} />,
);
@@ -126,13 +127,18 @@ describe("chat Tailwind components", () => {
it("renders ImageBubble openable and paywalled states", () => {
const openableHtml = renderToStaticMarkup(
<ImageBubble
characterId="character_elio"
messageId="message-1"
imageUrl="/chat-image.png"
onOpenImage={() => undefined}
/>,
);
const paywalledHtml = renderToStaticMarkup(
<ImageBubble imageUrl="/locked-image.png" imagePaywalled />,
<ImageBubble
characterId="character_elio"
imageUrl="/locked-image.png"
imagePaywalled
/>,
);
expect(openableHtml).toContain('role="button"');
@@ -170,14 +176,14 @@ describe("chat Tailwind components", () => {
});
it("renders ChatHeader guest and member branches", () => {
const guestHtml = renderToStaticMarkup(<ChatHeader isGuest={true} />);
const memberHtml = renderToStaticMarkup(
const guestHtml = renderWithCharacter(<ChatHeader isGuest={true} />);
const memberHtml = renderWithCharacter(
<ChatHeader isGuest={false} offerBanner={<div>Offer</div>} />,
);
const memberWithHintHtml = renderToStaticMarkup(
const memberWithHintHtml = renderWithCharacter(
<ChatHeader isGuest={false} showBrowserHint />,
);
const guestWithHintHtml = renderToStaticMarkup(
const guestWithHintHtml = renderWithCharacter(
<ChatHeader isGuest={true} showBrowserHint />,
);
@@ -369,3 +375,11 @@ describe("chat Tailwind components", () => {
});
});
function renderWithCharacter(node: ReactNode, slug = "elio"): string {
const character = getCharacterBySlug(slug);
if (!character) throw new Error(`Missing ${slug} character fixture`);
return renderToStaticMarkup(
<CharacterProvider character={character}>{node}</CharacterProvider>,
);
}