Compare commits
2 Commits
abb6e4235f
...
4c512d323e
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c512d323e | |||
| 7f271ef2e0 |
Binary file not shown.
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 35 KiB |
@@ -1,6 +1,6 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { renderToStaticMarkup } from "react-dom/server";
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { getCharacterBySlug } from "@/data/constants/character";
|
import { getCharacterBySlug } from "@/data/constants/character";
|
||||||
import { CharacterProvider } from "@/providers/character-provider";
|
import { CharacterProvider } from "@/providers/character-provider";
|
||||||
@@ -18,13 +18,27 @@ import { PrivateMessageCard } from "../private-message-card";
|
|||||||
import { PwaInstallDialog } from "../pwa-install-dialog";
|
import { PwaInstallDialog } from "../pwa-install-dialog";
|
||||||
import { TextBubble } from "../text-bubble";
|
import { TextBubble } from "../text-bubble";
|
||||||
|
|
||||||
|
const userMocks = vi.hoisted(() => ({
|
||||||
|
avatarUrl: null as string | null,
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("@/router/use-app-navigator", () => ({
|
vi.mock("@/router/use-app-navigator", () => ({
|
||||||
useAppNavigator: () => ({
|
useAppNavigator: () => ({
|
||||||
openSubscription: vi.fn(),
|
openSubscription: vi.fn(),
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/stores/user/user-context", () => ({
|
||||||
|
useUserSelector: (
|
||||||
|
selector: (state: { context: { avatarUrl: string | null } }) => unknown,
|
||||||
|
) => selector({ context: { avatarUrl: userMocks.avatarUrl } }),
|
||||||
|
}));
|
||||||
|
|
||||||
describe("chat Tailwind components", () => {
|
describe("chat Tailwind components", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
userMocks.avatarUrl = null;
|
||||||
|
});
|
||||||
|
|
||||||
it("renders MessageAvatar AI and user branches with image utilities", () => {
|
it("renders MessageAvatar AI and user branches with image utilities", () => {
|
||||||
const aiHtml = renderWithCharacter(<MessageAvatar isFromAI={true} />);
|
const aiHtml = renderWithCharacter(<MessageAvatar isFromAI={true} />);
|
||||||
const userHtml = renderWithCharacter(
|
const userHtml = renderWithCharacter(
|
||||||
@@ -228,6 +242,7 @@ describe("chat Tailwind components", () => {
|
|||||||
expect(memberHtml).not.toContain("px-(--spacing-md,12px)");
|
expect(memberHtml).not.toContain("px-(--spacing-md,12px)");
|
||||||
expect(memberHtml).toContain("bg-[rgba(13,11,20,0.7)]");
|
expect(memberHtml).toContain("bg-[rgba(13,11,20,0.7)]");
|
||||||
expect(memberHtml).toContain("size-(--responsive-icon-button-size,42px)");
|
expect(memberHtml).toContain("size-(--responsive-icon-button-size,42px)");
|
||||||
|
expect(memberHtml).toContain("lucide-user-round");
|
||||||
expect(memberWithHintHtml).toContain(
|
expect(memberWithHintHtml).toContain(
|
||||||
'data-analytics-key="chat.external_browser_hint"',
|
'data-analytics-key="chat.external_browser_hint"',
|
||||||
);
|
);
|
||||||
@@ -249,6 +264,20 @@ describe("chat Tailwind components", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders the user avatar in the ChatHeader Profile action", () => {
|
||||||
|
userMocks.avatarUrl = "/images/avatar/profile-user.png";
|
||||||
|
|
||||||
|
const html = renderWithCharacter(<ChatHeader isGuest={false} />);
|
||||||
|
|
||||||
|
expect(html).toContain("%2Fimages%2Favatar%2Fprofile-user.png");
|
||||||
|
expect(html).toContain('aria-label="Profile"');
|
||||||
|
expect(html).toContain('data-analytics-key="chat.open_profile"');
|
||||||
|
expect(html).toContain(
|
||||||
|
"var(--responsive-icon-button-size, 42px)",
|
||||||
|
);
|
||||||
|
expect(html).not.toContain("lucide-user-round");
|
||||||
|
});
|
||||||
|
|
||||||
it("links guest chat back to the active character splash", () => {
|
it("links guest chat back to the active character splash", () => {
|
||||||
const maya = getCharacterBySlug("maya");
|
const maya = getCharacterBySlug("maya");
|
||||||
if (!maya) throw new Error("Missing Maya character fixture");
|
if (!maya) throw new Error("Missing Maya character fixture");
|
||||||
|
|||||||
@@ -2,17 +2,17 @@
|
|||||||
/**
|
/**
|
||||||
* ChatHeader 顶部栏
|
* ChatHeader 顶部栏
|
||||||
*
|
*
|
||||||
* 图标:lucide-react <Lock />(游客 banner)+ <UserRound />(Profile)
|
* Profile 优先展示用户头像;头像缺失时回退到 lucide-react <UserRound />。
|
||||||
* tree-shakable,currentColor 继承父 color
|
|
||||||
*/
|
*/
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { Lock, UserRound } from "lucide-react";
|
import { Lock, UserRound } from "lucide-react";
|
||||||
|
|
||||||
import { BackButton } from "@/app/_components";
|
import { BackButton, UserMessageAvatar } from "@/app/_components";
|
||||||
import { useActiveCharacterRoutes } from "@/providers/character-provider";
|
import { useActiveCharacterRoutes } from "@/providers/character-provider";
|
||||||
import { buildGlobalPageUrl } from "@/router/global-route-context";
|
import { buildGlobalPageUrl } from "@/router/global-route-context";
|
||||||
import { ROUTES } from "@/router/routes";
|
import { ROUTES } from "@/router/routes";
|
||||||
import { useAppNavigator } from "@/router/use-app-navigator";
|
import { useAppNavigator } from "@/router/use-app-navigator";
|
||||||
|
import { useUserSelector } from "@/stores/user/user-context";
|
||||||
|
|
||||||
import { BrowserHintOverlay } from "./browser-hint-overlay";
|
import { BrowserHintOverlay } from "./browser-hint-overlay";
|
||||||
|
|
||||||
@@ -29,6 +29,10 @@ export function ChatHeader({
|
|||||||
}: ChatHeaderProps) {
|
}: ChatHeaderProps) {
|
||||||
const navigator = useAppNavigator();
|
const navigator = useAppNavigator();
|
||||||
const characterRoutes = useActiveCharacterRoutes();
|
const characterRoutes = useActiveCharacterRoutes();
|
||||||
|
const avatarUrl = useUserSelector((state) => state.context.avatarUrl);
|
||||||
|
const handleOpenProfile = () => {
|
||||||
|
navigator.push(buildGlobalPageUrl(ROUTES.profile, characterRoutes.chat));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="flex shrink-0 flex-col items-stretch bg-transparent p-0">
|
<header className="flex shrink-0 flex-col items-stretch bg-transparent p-0">
|
||||||
@@ -72,20 +76,26 @@ export function ChatHeader({
|
|||||||
{showBrowserHint ? <BrowserHintOverlay /> : null}
|
{showBrowserHint ? <BrowserHintOverlay /> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{avatarUrl ? (
|
||||||
|
<UserMessageAvatar
|
||||||
|
avatarUrl={avatarUrl}
|
||||||
|
size="var(--responsive-icon-button-size, 42px)"
|
||||||
|
actionLabel="Profile"
|
||||||
|
analyticsKey="chat.open_profile"
|
||||||
|
onClick={handleOpenProfile}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-analytics-key="chat.open_profile"
|
data-analytics-key="chat.open_profile"
|
||||||
data-analytics-label="Open profile"
|
data-analytics-label="Open profile"
|
||||||
className="flex size-(--responsive-icon-button-size,42px) cursor-pointer items-center justify-center rounded-full border border-[rgba(255,255,255,0.12)] bg-[rgba(13,11,20,0.7)] text-(--color-text-primary,#fff) shadow-[0_10px_24px_rgba(0,0,0,0.2)] backdrop-blur-md transition-[background,transform] duration-150 hover:bg-[rgba(28,24,39,0.82)] active:scale-96 focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-[#f657a0]"
|
className="flex size-(--responsive-icon-button-size,42px) cursor-pointer items-center justify-center rounded-full border border-[rgba(255,255,255,0.12)] bg-[rgba(13,11,20,0.7)] text-(--color-text-primary,#fff) shadow-[0_10px_24px_rgba(0,0,0,0.2)] backdrop-blur-md transition-[background,transform] duration-150 hover:bg-[rgba(28,24,39,0.82)] active:scale-96 focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-[#f657a0]"
|
||||||
onClick={() =>
|
onClick={handleOpenProfile}
|
||||||
navigator.push(
|
|
||||||
buildGlobalPageUrl(ROUTES.profile, characterRoutes.chat),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
aria-label="Profile"
|
aria-label="Profile"
|
||||||
>
|
>
|
||||||
<UserRound size={24} aria-hidden="true" />
|
<UserRound size={24} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user