feat(FN-2529): merge fusion/fn-2529
This commit is contained in:
@@ -1627,7 +1627,11 @@ export function ModelOnboardingModal({
|
||||
data-testid={`onboarding-provider-card-${provider.id}`}
|
||||
className={`onboarding-provider-card${provider.authenticated ? " onboarding-provider-card--connected" : ""}`}
|
||||
>
|
||||
<div className="onboarding-provider-card__icon">
|
||||
<div
|
||||
className="onboarding-provider-card__icon"
|
||||
data-testid={`onboarding-provider-icon-${provider.id}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<ProviderIcon provider={provider.id} size="md" />
|
||||
</div>
|
||||
<div className="onboarding-provider-card__body">
|
||||
@@ -1667,7 +1671,11 @@ export function ModelOnboardingModal({
|
||||
data-testid={`onboarding-provider-card-${provider.id}`}
|
||||
className={`onboarding-provider-card${provider.authenticated ? " onboarding-provider-card--connected" : ""}`}
|
||||
>
|
||||
<div className="onboarding-provider-card__icon">
|
||||
<div
|
||||
className="onboarding-provider-card__icon"
|
||||
data-testid={`onboarding-provider-icon-${provider.id}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<ProviderIcon provider={provider.id} size="md" />
|
||||
</div>
|
||||
<div className="onboarding-provider-card__body">
|
||||
|
||||
@@ -155,6 +155,46 @@ function KimiIcon({ size, color, label = "Kimi" }: { size: number; color: string
|
||||
);
|
||||
}
|
||||
|
||||
// OpenRouter ring mark — simplified geometric version of the OpenRouter brand symbol.
|
||||
function OpenRouterIcon({ size, color, label = "OpenRouter" }: { size: number; color: string; label?: string }) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
data-testid="openrouter-icon"
|
||||
aria-label={label}
|
||||
>
|
||||
<path
|
||||
d="M12 2.5a9.5 9.5 0 1 0 9.5 9.5h-2.7a6.8 6.8 0 1 1-2-4.8l-3.1 3.1H22V2.5l-3.2 3.2A9.45 9.45 0 0 0 12 2.5"
|
||||
fill={color}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// GitHub logo (Octocat mark) from SimpleIcons.
|
||||
function GitHubIcon({ size, color, label = "GitHub" }: { size: number; color: string; label?: string }) {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
data-testid="github-icon"
|
||||
aria-label={label}
|
||||
>
|
||||
<path
|
||||
d="M12 0a12 12 0 0 0-3.79 23.39c.6.11.82-.26.82-.58v-2.23c-3.34.73-4.04-1.41-4.04-1.41-.55-1.39-1.34-1.76-1.34-1.76-1.09-.75.08-.74.08-.74 1.2.08 1.84 1.24 1.84 1.24 1.08 1.84 2.82 1.31 3.5 1 .1-.78.42-1.31.77-1.62-2.67-.3-5.47-1.34-5.47-5.95 0-1.31.47-2.38 1.24-3.22-.12-.31-.54-1.56.12-3.24 0 0 1.01-.32 3.3 1.23a11.3 11.3 0 0 1 6 0c2.29-1.55 3.3-1.23 3.3-1.23.66 1.68.24 2.93.12 3.24.77.84 1.24 1.91 1.24 3.22 0 4.62-2.8 5.64-5.48 5.94.43.37.82 1.1.82 2.22v3.29c0 .32.22.69.83.58A12 12 0 0 0 12 0"
|
||||
fill={color}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// Anthropic "A" mark composited with a small terminal "> _" badge in the
|
||||
// bottom-right, visually signalling "Anthropic, but via the local CLI".
|
||||
function ClaudeCliIcon({ size, color, label = "Anthropic — via Claude CLI" }: { size: number; color: string; label?: string }) {
|
||||
@@ -179,7 +219,7 @@ function ClaudeCliIcon({ size, color, label = "Anthropic — via Claude CLI" }:
|
||||
<rect x="13" y="13" width="10" height="9" rx="1.5" fill={color} />
|
||||
<path
|
||||
d="M15.2 16.2l1.6 1.4-1.6 1.4M18.6 19.6h2.4"
|
||||
stroke="var(--bg-primary, #111)"
|
||||
stroke="var(--provider-icon-contrast)"
|
||||
strokeWidth="1.2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
@@ -193,18 +233,30 @@ const providerConfig: Record<
|
||||
string,
|
||||
{ component: typeof AnthropicIcon; color: string; label?: string }
|
||||
> = {
|
||||
anthropic: { component: AnthropicIcon, color: "#d4a27f" }, // warm tan
|
||||
"claude-cli": { component: ClaudeCliIcon, color: "#d4a27f", label: "Anthropic — via Claude CLI" },
|
||||
openai: { component: OpenAIIcon, color: "#10a37f" }, // green
|
||||
"openai-codex": { component: OpenAIIcon, color: "#10a37f", label: "OpenAI Codex" }, // green (same as openai)
|
||||
google: { component: GeminiIcon, color: "#4285f4" }, // blue
|
||||
gemini: { component: GeminiIcon, color: "#4285f4" }, // blue (same as google)
|
||||
ollama: { component: OllamaIcon, color: "#fff" }, // white
|
||||
minimax: { component: MiniMaxIcon, color: "#E73562" }, // pink/red
|
||||
zai: { component: ZaiIcon, color: "#1A6DFF" }, // blue
|
||||
kimi: { component: KimiIcon, color: "#6C5CE7" }, // purple
|
||||
moonshot: { component: KimiIcon, color: "#6C5CE7" }, // purple (same as kimi)
|
||||
"kimi-coding": { component: KimiIcon, color: "#6C5CE7", label: "Kimi" }, // purple (Kimi alias)
|
||||
// Branded provider colors are tokenized in app/styles.css for theme-system consistency.
|
||||
anthropic: { component: AnthropicIcon, color: "var(--provider-anthropic)" },
|
||||
"claude-cli": { component: ClaudeCliIcon, color: "var(--provider-anthropic)", label: "Anthropic — via Claude CLI" },
|
||||
|
||||
openai: { component: OpenAIIcon, color: "var(--provider-openai)" },
|
||||
"openai-codex": { component: OpenAIIcon, color: "var(--provider-openai)", label: "OpenAI Codex" }, // OpenAI alias
|
||||
|
||||
google: { component: GeminiIcon, color: "var(--provider-gemini)" },
|
||||
gemini: { component: GeminiIcon, color: "var(--provider-gemini)" }, // Gemini alias family
|
||||
|
||||
// Monochrome marks use theme-aware text color for dark/light safety.
|
||||
ollama: { component: OllamaIcon, color: "var(--text)" },
|
||||
github: { component: GitHubIcon, color: "var(--text)" },
|
||||
"github-copilot": { component: GitHubIcon, color: "var(--text)", label: "GitHub Copilot" },
|
||||
|
||||
// OpenRouter appears in onboarding auth cards and should not fall back to CPU.
|
||||
openrouter: { component: OpenRouterIcon, color: "var(--provider-openrouter)" },
|
||||
|
||||
minimax: { component: MiniMaxIcon, color: "var(--provider-minimax)" },
|
||||
zai: { component: ZaiIcon, color: "var(--provider-zai)" },
|
||||
|
||||
kimi: { component: KimiIcon, color: "var(--provider-kimi)" },
|
||||
moonshot: { component: KimiIcon, color: "var(--provider-kimi)" }, // Moonshot alias
|
||||
"kimi-coding": { component: KimiIcon, color: "var(--provider-kimi)", label: "Kimi" }, // Kimi alias
|
||||
};
|
||||
|
||||
export function ProviderIcon({ provider, size = "sm" }: ProviderIconProps) {
|
||||
|
||||
@@ -523,6 +523,17 @@
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
}
|
||||
.auth-provider-icon-slot {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
inline-size: calc(var(--space-md) + var(--space-xs) * 2);
|
||||
block-size: calc(var(--space-md) + var(--space-xs) * 2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.auth-provider-icon-slot .provider-icon {
|
||||
display: inline-flex;
|
||||
}
|
||||
.auth-hint {
|
||||
display: block;
|
||||
padding: 12px 4px 0;
|
||||
|
||||
@@ -17,6 +17,7 @@ const PiExtensionsManager = lazy(() => import("./PiExtensionsManager").then((m)
|
||||
import { ClaudeCliProviderCard } from "./ClaudeCliProviderCard";
|
||||
import { PluginSlot } from "./PluginSlot";
|
||||
import { AgentPromptsManager } from "./AgentPromptsManager";
|
||||
import { ProviderIcon } from "./ProviderIcon";
|
||||
import { applyPresetToSelection, generateUniquePresetId } from "../utils/modelPresets";
|
||||
import { appendTokenQuery } from "../auth";
|
||||
import "./SettingsModal.css";
|
||||
@@ -3441,6 +3442,14 @@ export function SettingsModal({
|
||||
<div key={provider.id} className="auth-provider-card auth-provider-card--authenticated">
|
||||
<div className="auth-provider-header">
|
||||
<div className="auth-provider-info">
|
||||
{/* Stable icon wrapper contract for auth card tests: auth-provider-icon-<providerId> */}
|
||||
<span
|
||||
className="auth-provider-icon-slot"
|
||||
data-testid={`auth-provider-icon-${provider.id}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<ProviderIcon provider={provider.id} size="md" />
|
||||
</span>
|
||||
<strong>{provider.name}</strong>
|
||||
<span
|
||||
data-testid={`auth-status-${provider.id}`}
|
||||
@@ -3516,6 +3525,14 @@ export function SettingsModal({
|
||||
<div key={provider.id} className="auth-provider-card">
|
||||
<div className="auth-provider-header">
|
||||
<div className="auth-provider-info">
|
||||
{/* Stable icon wrapper contract for auth card tests: auth-provider-icon-<providerId> */}
|
||||
<span
|
||||
className="auth-provider-icon-slot"
|
||||
data-testid={`auth-provider-icon-${provider.id}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<ProviderIcon provider={provider.id} size="md" />
|
||||
</span>
|
||||
<strong>{provider.name}</strong>
|
||||
<span
|
||||
data-testid={`auth-status-${provider.id}`}
|
||||
|
||||
@@ -34,10 +34,10 @@ describe("CustomModelDropdown ProviderIcon Integration", () => {
|
||||
expect(openaiIcon).toBeInTheDocument();
|
||||
expect(openaiIcon).toHaveAttribute("aria-label", "OpenAI");
|
||||
|
||||
// Verify the SVG has the correct fill color (#10a37f - OpenAI green)
|
||||
// Verify the SVG has the correct fill color (var(--provider-openai) - OpenAI green)
|
||||
const paths = openaiIcon.querySelectorAll("path");
|
||||
expect(paths.length).toBeGreaterThan(0);
|
||||
expect(paths[0]).toHaveAttribute("fill", "#10a37f");
|
||||
expect(paths[0]).toHaveAttribute("fill", "var(--provider-openai)");
|
||||
});
|
||||
|
||||
it("renders actual OpenAI SVG icon in dropdown group header", async () => {
|
||||
@@ -58,7 +58,7 @@ describe("CustomModelDropdown ProviderIcon Integration", () => {
|
||||
// Verify the icon has the correct color
|
||||
const paths = openaiIcon.querySelectorAll("path");
|
||||
expect(paths.length).toBeGreaterThan(0);
|
||||
expect(paths[0]).toHaveAttribute("fill", "#10a37f");
|
||||
expect(paths[0]).toHaveAttribute("fill", "var(--provider-openai)");
|
||||
});
|
||||
|
||||
it("renders actual provider icons for all providers in dropdown", async () => {
|
||||
@@ -71,17 +71,18 @@ describe("CustomModelDropdown ProviderIcon Integration", () => {
|
||||
const anthropicIcon = screen.getByTestId("anthropic-icon");
|
||||
expect(anthropicIcon).toBeInTheDocument();
|
||||
expect(anthropicIcon).toHaveAttribute("aria-label", "Anthropic");
|
||||
expect(anthropicIcon.querySelector("path")).toHaveAttribute("fill", "#d4a27f");
|
||||
expect(anthropicIcon.querySelector("path")).toHaveAttribute("fill", "var(--provider-anthropic)");
|
||||
|
||||
const openaiIcon = screen.getByTestId("openai-icon");
|
||||
expect(openaiIcon).toBeInTheDocument();
|
||||
expect(openaiIcon).toHaveAttribute("aria-label", "OpenAI");
|
||||
expect(openaiIcon.querySelector("path")).toHaveAttribute("fill", "#10a37f");
|
||||
expect(openaiIcon.querySelector("path")).toHaveAttribute("fill", "var(--provider-openai)");
|
||||
|
||||
const ollamaIcon = screen.getByTestId("ollama-icon");
|
||||
expect(ollamaIcon).toBeInTheDocument();
|
||||
expect(ollamaIcon).toHaveAttribute("aria-label", "Ollama");
|
||||
expect(ollamaIcon.querySelector("path")).toHaveAttribute("fill", "#fff");
|
||||
// Ollama icon is intentionally theme-aware (token-based) rather than hardcoded white.
|
||||
expect(ollamaIcon.querySelector("path")).toHaveAttribute("fill", "var(--text)");
|
||||
});
|
||||
|
||||
it("renders both trigger icon and group header icons when dropdown is open with OpenAI selected", async () => {
|
||||
@@ -101,7 +102,7 @@ describe("CustomModelDropdown ProviderIcon Integration", () => {
|
||||
// Both should have correct attributes
|
||||
openaiIcons.forEach((icon) => {
|
||||
expect(icon).toHaveAttribute("aria-label", "OpenAI");
|
||||
expect(icon.querySelector("path")).toHaveAttribute("fill", "#10a37f");
|
||||
expect(icon.querySelector("path")).toHaveAttribute("fill", "var(--provider-openai)");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -135,7 +136,7 @@ describe("CustomModelDropdown ProviderIcon Integration", () => {
|
||||
// Verify the first one has correct attributes
|
||||
const kimiIcon = kimiIcons[0];
|
||||
expect(kimiIcon).toHaveAttribute("aria-label", "Kimi");
|
||||
expect(kimiIcon.querySelector("path")).toHaveAttribute("fill", "#6C5CE7");
|
||||
expect(kimiIcon.querySelector("path")).toHaveAttribute("fill", "var(--provider-kimi)");
|
||||
});
|
||||
|
||||
it("uses explicit icon/text layout hooks for favorited model rows", async () => {
|
||||
@@ -178,7 +179,7 @@ describe("CustomModelDropdown ProviderIcon Integration", () => {
|
||||
const kimiIcon = screen.getByTestId("kimi-icon");
|
||||
expect(kimiIcon).toBeInTheDocument();
|
||||
expect(kimiIcon).toHaveAttribute("aria-label", "Kimi");
|
||||
expect(kimiIcon.querySelector("path")).toHaveAttribute("fill", "#6C5CE7");
|
||||
expect(kimiIcon.querySelector("path")).toHaveAttribute("fill", "var(--provider-kimi)");
|
||||
});
|
||||
|
||||
it("renders Kimi icon in trigger when moonshot model is selected (alias)", () => {
|
||||
@@ -187,6 +188,6 @@ describe("CustomModelDropdown ProviderIcon Integration", () => {
|
||||
const kimiIcon = screen.getByTestId("kimi-icon");
|
||||
expect(kimiIcon).toBeInTheDocument();
|
||||
expect(kimiIcon).toHaveAttribute("aria-label", "Kimi");
|
||||
expect(kimiIcon.querySelector("path")).toHaveAttribute("fill", "#6C5CE7");
|
||||
expect(kimiIcon.querySelector("path")).toHaveAttribute("fill", "var(--provider-kimi)");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -499,6 +499,20 @@ describe("ModelOnboardingModal", () => {
|
||||
expect(screen.queryByTestId("onboarding-advanced-provider-settings")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders onboarding provider icon wrapper for connected advanced providers", async () => {
|
||||
mockFetchAuthStatus.mockResolvedValueOnce({
|
||||
providers: [
|
||||
{ id: "anthropic", name: "Anthropic", authenticated: false, type: "oauth" },
|
||||
{ id: "openrouter", name: "OpenRouter", authenticated: true, type: "api_key" },
|
||||
],
|
||||
});
|
||||
|
||||
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} projectId="proj_123" />);
|
||||
|
||||
const openRouterWrapper = await screen.findByTestId("onboarding-provider-icon-openrouter");
|
||||
expect(within(openRouterWrapper).getByTestId("provider-icon")).toHaveAttribute("data-provider", "openrouter");
|
||||
});
|
||||
|
||||
it("shows model dropdown in AI Setup step", async () => {
|
||||
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} projectId="proj_123" />);
|
||||
|
||||
@@ -754,7 +768,7 @@ describe("ModelOnboardingModal", () => {
|
||||
expect(description.closest(".onboarding-provider-card")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("renders ProviderIcon for each provider card", async () => {
|
||||
it("renders stable onboarding-provider-icon wrappers for provider cards", async () => {
|
||||
render(<ModelOnboardingModal onComplete={vi.fn()} addToast={vi.fn()} projectId="proj_123" />);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -762,15 +776,11 @@ describe("ModelOnboardingModal", () => {
|
||||
expect(screen.getByText("OpenAI")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Verify provider icons are rendered for both providers
|
||||
const icons = screen.getAllByTestId("provider-icon");
|
||||
expect(icons.length).toBe(2);
|
||||
const anthropicIconWrapper = screen.getByTestId("onboarding-provider-icon-anthropic");
|
||||
const openaiIconWrapper = screen.getByTestId("onboarding-provider-icon-openai");
|
||||
|
||||
// Verify the data-provider attributes match expected IDs
|
||||
const anthropicIcon = icons.find((icon) => icon.getAttribute("data-provider") === "anthropic");
|
||||
const openaiIcon = icons.find((icon) => icon.getAttribute("data-provider") === "openai");
|
||||
expect(anthropicIcon).toBeTruthy();
|
||||
expect(openaiIcon).toBeTruthy();
|
||||
expect(within(anthropicIconWrapper).getByTestId("provider-icon")).toHaveAttribute("data-provider", "anthropic");
|
||||
expect(within(openaiIconWrapper).getByTestId("provider-icon")).toHaveAttribute("data-provider", "openai");
|
||||
});
|
||||
|
||||
it("applies connected modifier class to authenticated provider cards", async () => {
|
||||
|
||||
@@ -12,7 +12,7 @@ describe("ProviderIcon", () => {
|
||||
it("applies provider-specific color for openai-codex", () => {
|
||||
render(<ProviderIcon provider="openai-codex" />);
|
||||
const icon = screen.getByTestId("openai-icon").parentElement;
|
||||
expect(icon).toHaveStyle({ color: "#10a37f" });
|
||||
expect(icon).toHaveStyle({ color: "var(--provider-openai)" });
|
||||
});
|
||||
|
||||
it("passes correct color to SVG fill for openai-codex", () => {
|
||||
@@ -20,7 +20,7 @@ describe("ProviderIcon", () => {
|
||||
const svg = screen.getByTestId("openai-icon");
|
||||
const paths = svg.querySelectorAll("path");
|
||||
expect(paths.length).toBeGreaterThan(0);
|
||||
expect(paths[0]).toHaveAttribute("fill", "#10a37f");
|
||||
expect(paths[0]).toHaveAttribute("fill", "var(--provider-openai)");
|
||||
});
|
||||
|
||||
it("renders Anthropic brand icon for anthropic provider", () => {
|
||||
@@ -29,6 +29,15 @@ describe("ProviderIcon", () => {
|
||||
expect(screen.getByLabelText("Anthropic")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders claude-cli icon with tokenized contrast stroke", () => {
|
||||
render(<ProviderIcon provider="claude-cli" />);
|
||||
const svg = screen.getByTestId("claude-cli-icon");
|
||||
expect(svg).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("Anthropic — via Claude CLI")).toBeInTheDocument();
|
||||
const badgeGlyph = svg.querySelector('path[stroke]');
|
||||
expect(badgeGlyph).toHaveAttribute("stroke", "var(--provider-icon-contrast)");
|
||||
});
|
||||
|
||||
it("renders OpenAI brand icon for openai provider", () => {
|
||||
render(<ProviderIcon provider="openai" />);
|
||||
expect(screen.getByTestId("openai-icon")).toBeInTheDocument();
|
||||
@@ -82,25 +91,25 @@ describe("ProviderIcon", () => {
|
||||
it("applies provider-specific color for anthropic", () => {
|
||||
render(<ProviderIcon provider="anthropic" />);
|
||||
const icon = screen.getByTestId("anthropic-icon").parentElement;
|
||||
expect(icon).toHaveStyle({ color: "#d4a27f" });
|
||||
expect(icon).toHaveStyle({ color: "var(--provider-anthropic)" });
|
||||
});
|
||||
|
||||
it("applies provider-specific color for openai", () => {
|
||||
render(<ProviderIcon provider="openai" />);
|
||||
const icon = screen.getByTestId("openai-icon").parentElement;
|
||||
expect(icon).toHaveStyle({ color: "#10a37f" });
|
||||
expect(icon).toHaveStyle({ color: "var(--provider-openai)" });
|
||||
});
|
||||
|
||||
it("applies provider-specific color for google", () => {
|
||||
render(<ProviderIcon provider="google" />);
|
||||
const icon = screen.getByTestId("gemini-icon").parentElement;
|
||||
expect(icon).toHaveStyle({ color: "#4285f4" });
|
||||
expect(icon).toHaveStyle({ color: "var(--provider-gemini)" });
|
||||
});
|
||||
|
||||
it("applies provider-specific color for ollama", () => {
|
||||
it("applies theme-safe color for ollama", () => {
|
||||
render(<ProviderIcon provider="ollama" />);
|
||||
const icon = screen.getByTestId("ollama-icon").parentElement;
|
||||
expect(icon).toHaveStyle({ color: "#fff" });
|
||||
expect(icon).toHaveStyle({ color: "var(--text)" });
|
||||
});
|
||||
|
||||
it("applies default color for unknown providers", () => {
|
||||
@@ -156,7 +165,7 @@ describe("ProviderIcon", () => {
|
||||
const paths = svg.querySelectorAll("path");
|
||||
expect(paths.length).toBeGreaterThan(0);
|
||||
// First path should have the provider color
|
||||
expect(paths[0]).toHaveAttribute("fill", "#d4a27f");
|
||||
expect(paths[0]).toHaveAttribute("fill", "var(--provider-anthropic)");
|
||||
});
|
||||
|
||||
it("passes correct color to SVG fill for openai", () => {
|
||||
@@ -164,7 +173,7 @@ describe("ProviderIcon", () => {
|
||||
const svg = screen.getByTestId("openai-icon");
|
||||
const paths = svg.querySelectorAll("path");
|
||||
expect(paths.length).toBeGreaterThan(0);
|
||||
expect(paths[0]).toHaveAttribute("fill", "#10a37f");
|
||||
expect(paths[0]).toHaveAttribute("fill", "var(--provider-openai)");
|
||||
});
|
||||
|
||||
it("passes correct color to SVG fill for gemini", () => {
|
||||
@@ -172,15 +181,15 @@ describe("ProviderIcon", () => {
|
||||
const svg = screen.getByTestId("gemini-icon");
|
||||
const paths = svg.querySelectorAll("path");
|
||||
expect(paths.length).toBeGreaterThan(0);
|
||||
expect(paths[0]).toHaveAttribute("fill", "#4285f4");
|
||||
expect(paths[0]).toHaveAttribute("fill", "var(--provider-gemini)");
|
||||
});
|
||||
|
||||
it("passes correct color to SVG fill for ollama", () => {
|
||||
it("passes theme-safe color to SVG fill for ollama", () => {
|
||||
render(<ProviderIcon provider="ollama" />);
|
||||
const svg = screen.getByTestId("ollama-icon");
|
||||
const paths = svg.querySelectorAll("path");
|
||||
expect(paths.length).toBeGreaterThan(0);
|
||||
expect(paths[0]).toHaveAttribute("fill", "#fff");
|
||||
expect(paths[0]).toHaveAttribute("fill", "var(--text)");
|
||||
});
|
||||
|
||||
it("renders MiniMax brand icon for minimax provider", () => {
|
||||
@@ -192,7 +201,7 @@ describe("ProviderIcon", () => {
|
||||
it("applies provider-specific color for minimax", () => {
|
||||
render(<ProviderIcon provider="minimax" />);
|
||||
const icon = screen.getByTestId("minimax-icon").parentElement;
|
||||
expect(icon).toHaveStyle({ color: "#E73562" });
|
||||
expect(icon).toHaveStyle({ color: "var(--provider-minimax)" });
|
||||
});
|
||||
|
||||
it("passes correct color to SVG fill for minimax", () => {
|
||||
@@ -200,7 +209,7 @@ describe("ProviderIcon", () => {
|
||||
const svg = screen.getByTestId("minimax-icon");
|
||||
const paths = svg.querySelectorAll("path");
|
||||
expect(paths.length).toBeGreaterThan(0);
|
||||
expect(paths[0]).toHaveAttribute("fill", "#E73562");
|
||||
expect(paths[0]).toHaveAttribute("fill", "var(--provider-minimax)");
|
||||
});
|
||||
|
||||
it("normalizes Minimax (capitalized) to minimax", () => {
|
||||
@@ -219,7 +228,7 @@ describe("ProviderIcon", () => {
|
||||
it("applies provider-specific color for zai", () => {
|
||||
render(<ProviderIcon provider="zai" />);
|
||||
const icon = screen.getByTestId("zai-icon").parentElement;
|
||||
expect(icon).toHaveStyle({ color: "#1A6DFF" });
|
||||
expect(icon).toHaveStyle({ color: "var(--provider-zai)" });
|
||||
});
|
||||
|
||||
it("passes correct color to SVG fill for zai", () => {
|
||||
@@ -227,7 +236,7 @@ describe("ProviderIcon", () => {
|
||||
const svg = screen.getByTestId("zai-icon");
|
||||
const paths = svg.querySelectorAll("path");
|
||||
expect(paths.length).toBeGreaterThan(0);
|
||||
expect(paths[0]).toHaveAttribute("fill", "#1A6DFF");
|
||||
expect(paths[0]).toHaveAttribute("fill", "var(--provider-zai)");
|
||||
});
|
||||
|
||||
it("normalizes Zai (capitalized) to zai", () => {
|
||||
@@ -246,7 +255,7 @@ describe("ProviderIcon", () => {
|
||||
it("applies provider-specific color for kimi", () => {
|
||||
render(<ProviderIcon provider="kimi" />);
|
||||
const icon = screen.getByTestId("kimi-icon").parentElement;
|
||||
expect(icon).toHaveStyle({ color: "#6C5CE7" });
|
||||
expect(icon).toHaveStyle({ color: "var(--provider-kimi)" });
|
||||
});
|
||||
|
||||
it("passes correct color to SVG fill for kimi", () => {
|
||||
@@ -254,7 +263,7 @@ describe("ProviderIcon", () => {
|
||||
const svg = screen.getByTestId("kimi-icon");
|
||||
const paths = svg.querySelectorAll("path");
|
||||
expect(paths.length).toBeGreaterThan(0);
|
||||
expect(paths[0]).toHaveAttribute("fill", "#6C5CE7");
|
||||
expect(paths[0]).toHaveAttribute("fill", "var(--provider-kimi)");
|
||||
});
|
||||
|
||||
it("normalizes Kimi (capitalized) to kimi", () => {
|
||||
@@ -273,7 +282,7 @@ describe("ProviderIcon", () => {
|
||||
it("applies provider-specific color for moonshot (alias)", () => {
|
||||
render(<ProviderIcon provider="moonshot" />);
|
||||
const icon = screen.getByTestId("kimi-icon").parentElement;
|
||||
expect(icon).toHaveStyle({ color: "#6C5CE7" });
|
||||
expect(icon).toHaveStyle({ color: "var(--provider-kimi)" });
|
||||
});
|
||||
|
||||
it("passes correct color to SVG fill for moonshot (alias)", () => {
|
||||
@@ -281,7 +290,7 @@ describe("ProviderIcon", () => {
|
||||
const svg = screen.getByTestId("kimi-icon");
|
||||
const paths = svg.querySelectorAll("path");
|
||||
expect(paths.length).toBeGreaterThan(0);
|
||||
expect(paths[0]).toHaveAttribute("fill", "#6C5CE7");
|
||||
expect(paths[0]).toHaveAttribute("fill", "var(--provider-kimi)");
|
||||
});
|
||||
|
||||
it("normalizes Moonshot (capitalized) to moonshot", () => {
|
||||
@@ -291,6 +300,24 @@ describe("ProviderIcon", () => {
|
||||
expect(wrapper).toHaveAttribute("data-provider", "moonshot");
|
||||
});
|
||||
|
||||
it("renders OpenRouter brand icon for openrouter provider", () => {
|
||||
render(<ProviderIcon provider="openrouter" />);
|
||||
expect(screen.getByTestId("openrouter-icon")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("OpenRouter")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders GitHub brand icon for github provider", () => {
|
||||
render(<ProviderIcon provider="github" />);
|
||||
expect(screen.getByTestId("github-icon")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("GitHub")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("reuses GitHub icon for github-copilot alias", () => {
|
||||
render(<ProviderIcon provider="github-copilot" />);
|
||||
expect(screen.getByTestId("github-icon")).toBeInTheDocument();
|
||||
expect(screen.getByLabelText("GitHub Copilot")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders Kimi brand icon for kimi-coding provider (alias)", () => {
|
||||
render(<ProviderIcon provider="kimi-coding" />);
|
||||
expect(screen.getByTestId("kimi-icon")).toBeInTheDocument();
|
||||
@@ -300,7 +327,7 @@ describe("ProviderIcon", () => {
|
||||
it("applies provider-specific color for kimi-coding (alias)", () => {
|
||||
render(<ProviderIcon provider="kimi-coding" />);
|
||||
const icon = screen.getByTestId("kimi-icon").parentElement;
|
||||
expect(icon).toHaveStyle({ color: "#6C5CE7" });
|
||||
expect(icon).toHaveStyle({ color: "var(--provider-kimi)" });
|
||||
});
|
||||
|
||||
it("passes correct color to SVG fill for kimi-coding (alias)", () => {
|
||||
@@ -308,7 +335,7 @@ describe("ProviderIcon", () => {
|
||||
const svg = screen.getByTestId("kimi-icon");
|
||||
const paths = svg.querySelectorAll("path");
|
||||
expect(paths.length).toBeGreaterThan(0);
|
||||
expect(paths[0]).toHaveAttribute("fill", "#6C5CE7");
|
||||
expect(paths[0]).toHaveAttribute("fill", "var(--provider-kimi)");
|
||||
});
|
||||
|
||||
// Regression test: verify the Kimi icon is the crescent moon shape, not the old "K" placeholder
|
||||
|
||||
@@ -385,6 +385,45 @@ describe("SettingsModal", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Authentication provider icon wrappers", () => {
|
||||
it("renders stable auth-provider-icon wrappers with branded and fallback SVG behavior", async () => {
|
||||
mockFetchAuthStatus.mockResolvedValueOnce({
|
||||
providers: [
|
||||
{ id: "openrouter", name: "OpenRouter", authenticated: true, type: "api_key" },
|
||||
{ id: "unknown-provider", name: "Unknown Provider", authenticated: false, type: "api_key" },
|
||||
],
|
||||
});
|
||||
|
||||
renderModal();
|
||||
await waitForSettingsModalReady();
|
||||
|
||||
const openRouterIconWrapper = await screen.findByTestId("auth-provider-icon-openrouter");
|
||||
expect(within(openRouterIconWrapper).getByTestId("openrouter-icon")).toBeInTheDocument();
|
||||
|
||||
const unknownIconWrapper = screen.getByTestId("auth-provider-icon-unknown-provider");
|
||||
const fallbackSvg = unknownIconWrapper.querySelector("svg");
|
||||
expect(fallbackSvg).toBeInTheDocument();
|
||||
expect(fallbackSvg).not.toHaveAttribute("data-testid");
|
||||
});
|
||||
|
||||
it("renders icon wrappers for both authenticated and available provider rows", async () => {
|
||||
mockFetchAuthStatus.mockResolvedValueOnce({
|
||||
providers: [
|
||||
{ id: "github", name: "GitHub", authenticated: true, type: "oauth" },
|
||||
{ id: "openai", name: "OpenAI", authenticated: false, type: "api_key" },
|
||||
],
|
||||
});
|
||||
|
||||
renderModal();
|
||||
await waitForSettingsModalReady();
|
||||
|
||||
expect(screen.getByTestId("auth-provider-icon-github")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("auth-provider-icon-openai")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("auth-status-github")).toHaveTextContent("✓ Active");
|
||||
expect(screen.getByTestId("auth-status-openai")).toHaveTextContent("✗ Not connected");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Plugins section navigation", () => {
|
||||
it("does not render a standalone Pi Extensions sidebar item", async () => {
|
||||
renderModal();
|
||||
|
||||
@@ -161,6 +161,16 @@
|
||||
/* Logo & branding tokens */
|
||||
--logo-accent: var(--todo);
|
||||
|
||||
/* Provider brand tokens used by ProviderIcon in settings/onboarding auth flows. */
|
||||
--provider-anthropic: #d4a27f;
|
||||
--provider-openai: #10a37f;
|
||||
--provider-gemini: #4285f4;
|
||||
--provider-openrouter: #6f4cff;
|
||||
--provider-minimax: #e73562;
|
||||
--provider-zai: #1a6dff;
|
||||
--provider-kimi: #6c5ce7;
|
||||
--provider-icon-contrast: var(--bg);
|
||||
|
||||
/* Task-creation CTA tokens */
|
||||
--cta-bg: #238636;
|
||||
--cta-border: #2ea043;
|
||||
|
||||
Reference in New Issue
Block a user