add electron e2e harness and regression coverage (#697)
This commit is contained in:
committed by
GitHub
Unverified
parent
514a6c4112
commit
2668082809
@@ -8,14 +8,14 @@ import { TitleBar } from './TitleBar';
|
||||
|
||||
export function MainLayout() {
|
||||
return (
|
||||
<div className="flex h-screen flex-col overflow-hidden bg-background">
|
||||
<div data-testid="main-layout" className="flex h-screen flex-col overflow-hidden bg-background">
|
||||
{/* Title bar: drag region on macOS, icon + controls on Windows */}
|
||||
<TitleBar />
|
||||
|
||||
{/* Below the title bar: sidebar + content */}
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
<Sidebar />
|
||||
<main className="flex-1 overflow-auto p-6">
|
||||
<main data-testid="main-content" className="flex-1 overflow-auto p-6">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -46,13 +46,15 @@ interface NavItemProps {
|
||||
badge?: string;
|
||||
collapsed?: boolean;
|
||||
onClick?: () => void;
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
function NavItem({ to, icon, label, badge, collapsed, onClick }: NavItemProps) {
|
||||
function NavItem({ to, icon, label, badge, collapsed, onClick, testId }: NavItemProps) {
|
||||
return (
|
||||
<NavLink
|
||||
to={to}
|
||||
onClick={onClick}
|
||||
data-testid={testId}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
'flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-[14px] font-medium transition-colors',
|
||||
@@ -206,15 +208,16 @@ export function Sidebar() {
|
||||
}
|
||||
|
||||
const navItems = [
|
||||
{ to: '/models', icon: <Cpu className="h-[18px] w-[18px]" strokeWidth={2} />, label: t('sidebar.models') },
|
||||
{ to: '/agents', icon: <Bot className="h-[18px] w-[18px]" strokeWidth={2} />, label: t('sidebar.agents') },
|
||||
{ to: '/channels', icon: <Network className="h-[18px] w-[18px]" strokeWidth={2} />, label: t('sidebar.channels') },
|
||||
{ to: '/skills', icon: <Puzzle className="h-[18px] w-[18px]" strokeWidth={2} />, label: t('sidebar.skills') },
|
||||
{ to: '/cron', icon: <Clock className="h-[18px] w-[18px]" strokeWidth={2} />, label: t('sidebar.cronTasks') },
|
||||
{ to: '/models', icon: <Cpu className="h-[18px] w-[18px]" strokeWidth={2} />, label: t('sidebar.models'), testId: 'sidebar-nav-models' },
|
||||
{ to: '/agents', icon: <Bot className="h-[18px] w-[18px]" strokeWidth={2} />, label: t('sidebar.agents'), testId: 'sidebar-nav-agents' },
|
||||
{ to: '/channels', icon: <Network className="h-[18px] w-[18px]" strokeWidth={2} />, label: t('sidebar.channels'), testId: 'sidebar-nav-channels' },
|
||||
{ to: '/skills', icon: <Puzzle className="h-[18px] w-[18px]" strokeWidth={2} />, label: t('sidebar.skills'), testId: 'sidebar-nav-skills' },
|
||||
{ to: '/cron', icon: <Clock className="h-[18px] w-[18px]" strokeWidth={2} />, label: t('sidebar.cronTasks'), testId: 'sidebar-nav-cron' },
|
||||
];
|
||||
|
||||
return (
|
||||
<aside
|
||||
data-testid="sidebar"
|
||||
className={cn(
|
||||
'flex shrink-0 flex-col border-r bg-[#eae8e1]/60 dark:bg-background transition-all duration-300',
|
||||
sidebarCollapsed ? 'w-16' : 'w-64'
|
||||
@@ -247,6 +250,7 @@ export function Sidebar() {
|
||||
{/* Navigation */}
|
||||
<nav className="flex flex-col px-2 gap-0.5">
|
||||
<button
|
||||
data-testid="sidebar-new-chat"
|
||||
onClick={() => {
|
||||
const { messages } = useChatStore.getState();
|
||||
if (messages.length > 0) newSession();
|
||||
@@ -334,6 +338,7 @@ export function Sidebar() {
|
||||
<div className="p-2 mt-auto">
|
||||
<NavLink
|
||||
to="/settings"
|
||||
data-testid="sidebar-nav-settings"
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
'flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-[14px] font-medium transition-colors',
|
||||
@@ -354,6 +359,7 @@ export function Sidebar() {
|
||||
</NavLink>
|
||||
|
||||
<Button
|
||||
data-testid="sidebar-open-dev-console"
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'flex items-center gap-2.5 rounded-lg px-2.5 py-2 h-auto text-[14px] font-medium transition-colors w-full mt-1',
|
||||
@@ -391,4 +397,4 @@ export function Sidebar() {
|
||||
/>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,12 +240,12 @@ export function ProvidersSettings() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div data-testid="providers-settings" className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-3xl font-serif text-foreground font-normal tracking-tight" style={{ fontFamily: 'Georgia, Cambria, "Times New Roman", Times, serif' }}>
|
||||
<h2 data-testid="providers-settings-title" className="text-3xl font-serif text-foreground font-normal tracking-tight" style={{ fontFamily: 'Georgia, Cambria, "Times New Roman", Times, serif' }}>
|
||||
{t('aiProviders.title', 'AI Providers')}
|
||||
</h2>
|
||||
<Button onClick={() => setShowAddDialog(true)} className="rounded-full px-5 h-9 shadow-none font-medium text-[13px]">
|
||||
<Button data-testid="providers-add-button" onClick={() => setShowAddDialog(true)} className="rounded-full px-5 h-9 shadow-none font-medium text-[13px]">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
{t('aiProviders.add')}
|
||||
</Button>
|
||||
@@ -256,7 +256,7 @@ export function ProvidersSettings() {
|
||||
<Loader2 className="h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
) : displayProviders.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-muted-foreground bg-black/5 dark:bg-white/5 rounded-3xl border border-transparent border-dashed">
|
||||
<div data-testid="providers-empty-state" className="flex flex-col items-center justify-center py-20 text-muted-foreground bg-black/5 dark:bg-white/5 rounded-3xl border border-transparent border-dashed">
|
||||
<Key className="h-12 w-12 mb-4 opacity-50" />
|
||||
<h3 className="text-[15px] font-medium mb-1 text-foreground">{t('aiProviders.empty.title')}</h3>
|
||||
<p className="text-[13px] text-center mb-6 max-w-sm">
|
||||
@@ -505,6 +505,7 @@ function ProviderCard({
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid={`provider-card-${account.id}`}
|
||||
className={cn(
|
||||
"group flex flex-col p-4 rounded-2xl transition-all relative overflow-hidden hover:bg-black/5 dark:hover:bg-white/5",
|
||||
isDefault
|
||||
@@ -569,10 +570,11 @@ function ProviderCard({
|
||||
{!isEditing && (
|
||||
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{!isDefault && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 rounded-full text-muted-foreground hover:text-blue-600 hover:bg-white dark:hover:bg-card shadow-sm"
|
||||
<Button
|
||||
data-testid={`provider-set-default-${account.id}`}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 rounded-full text-muted-foreground hover:text-blue-600 hover:bg-white dark:hover:bg-card shadow-sm"
|
||||
onClick={onSetDefault}
|
||||
title={t('aiProviders.card.setDefault')}
|
||||
>
|
||||
@@ -580,6 +582,7 @@ function ProviderCard({
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
data-testid={`provider-edit-${account.id}`}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 rounded-full text-muted-foreground hover:text-foreground hover:bg-white dark:hover:bg-card shadow-sm"
|
||||
@@ -589,6 +592,7 @@ function ProviderCard({
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
data-testid={`provider-delete-${account.id}`}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 rounded-full text-muted-foreground hover:text-destructive hover:bg-white dark:hover:bg-card shadow-sm"
|
||||
@@ -1205,7 +1209,7 @@ function AddProviderDialog({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 bg-black/50 flex items-center justify-center p-4">
|
||||
<div data-testid="add-provider-dialog" className="fixed inset-0 z-50 bg-black/50 flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-2xl max-h-[90vh] flex flex-col rounded-3xl border-0 shadow-2xl bg-[#f3f1e9] dark:bg-card overflow-hidden">
|
||||
<CardHeader className="relative pb-2 shrink-0">
|
||||
<CardTitle className="text-2xl font-serif font-normal">{t('aiProviders.dialog.title')}</CardTitle>
|
||||
@@ -1213,6 +1217,7 @@ function AddProviderDialog({
|
||||
{t('aiProviders.dialog.desc')}
|
||||
</CardDescription>
|
||||
<Button
|
||||
data-testid="add-provider-close-button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-4 top-4 rounded-full h-8 w-8 -mr-2 -mt-2 text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5"
|
||||
@@ -1226,6 +1231,7 @@ function AddProviderDialog({
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
{availableTypes.map((type) => (
|
||||
<button
|
||||
data-testid={`add-provider-type-${type.id}`}
|
||||
key={type.id}
|
||||
onClick={() => {
|
||||
setSelectedType(type.id);
|
||||
@@ -1296,6 +1302,7 @@ function AddProviderDialog({
|
||||
<div className="space-y-2.5">
|
||||
<Label htmlFor="name" className={labelClasses}>{t('aiProviders.dialog.displayName')}</Label>
|
||||
<Input
|
||||
data-testid="add-provider-name-input"
|
||||
id="name"
|
||||
placeholder={typeInfo?.id === 'custom' ? t('aiProviders.custom') : typeInfo?.name}
|
||||
value={name}
|
||||
@@ -1347,6 +1354,7 @@ function AddProviderDialog({
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Input
|
||||
data-testid="add-provider-api-key-input"
|
||||
id="apiKey"
|
||||
type={showKey ? 'text' : 'password'}
|
||||
placeholder={typeInfo?.id === 'ollama' ? t('aiProviders.notRequired') : typeInfo?.placeholder}
|
||||
@@ -1378,6 +1386,7 @@ function AddProviderDialog({
|
||||
<div className="space-y-2.5">
|
||||
<Label htmlFor="baseUrl" className={labelClasses}>{t('aiProviders.dialog.baseUrl')}</Label>
|
||||
<Input
|
||||
data-testid="add-provider-base-url-input"
|
||||
id="baseUrl"
|
||||
placeholder={getProtocolBaseUrlPlaceholder(apiProtocol)}
|
||||
value={baseUrl}
|
||||
@@ -1391,6 +1400,7 @@ function AddProviderDialog({
|
||||
<div className="space-y-2.5">
|
||||
<Label htmlFor="modelId" className={labelClasses}>{t('aiProviders.dialog.modelId')}</Label>
|
||||
<Input
|
||||
data-testid="add-provider-model-id-input"
|
||||
id="modelId"
|
||||
placeholder={typeInfo?.modelIdPlaceholder || 'provider/model-id'}
|
||||
value={modelId}
|
||||
|
||||
@@ -170,7 +170,7 @@ export const PROVIDER_TYPE_INFO: ProviderTypeInfo[] = [
|
||||
{ id: 'openrouter', name: 'OpenRouter', icon: '🌐', placeholder: 'sk-or-v1-...', model: 'Multi-Model', requiresApiKey: true, showModelId: true, modelIdPlaceholder: 'openai/gpt-5.4', defaultModelId: 'openai/gpt-5.4', docsUrl: 'https://openrouter.ai/models' },
|
||||
{ id: 'minimax-portal-cn', name: 'MiniMax (CN)', icon: '☁️', placeholder: 'sk-...', model: 'MiniMax', requiresApiKey: false, isOAuth: true, supportsApiKey: true, defaultModelId: 'MiniMax-M2.7', showModelId: true, showModelIdInDevModeOnly: true, modelIdPlaceholder: 'MiniMax-M2.7', apiKeyUrl: 'https://platform.minimaxi.com/' },
|
||||
{ id: 'moonshot', name: 'Moonshot (CN)', icon: '🌙', placeholder: 'sk-...', model: 'Kimi', requiresApiKey: true, defaultBaseUrl: 'https://api.moonshot.cn/v1', defaultModelId: 'kimi-k2.5', docsUrl: 'https://platform.moonshot.cn/' },
|
||||
{ id: 'siliconflow', name: 'SiliconFlow (CN)', icon: '🌊', placeholder: 'sk-...', model: 'Multi-Model', requiresApiKey: true, defaultBaseUrl: 'https://api.siliconflow.cn/v1', showModelId: true, modelIdPlaceholder: 'deepseek-ai/DeepSeek-V3', defaultModelId: 'deepseek-ai/DeepSeek-V3', docsUrl: 'https://docs.siliconflow.cn/cn/userguide/introduction' },
|
||||
{ id: 'siliconflow', name: 'SiliconFlow (CN)', icon: '🌊', placeholder: 'sk-...', model: 'Multi-Model', requiresApiKey: true, defaultBaseUrl: 'https://api.siliconflow.cn/v1', showModelId: true, showModelIdInDevModeOnly: true, modelIdPlaceholder: 'deepseek-ai/DeepSeek-V3', defaultModelId: 'deepseek-ai/DeepSeek-V3', docsUrl: 'https://docs.siliconflow.cn/cn/userguide/introduction' },
|
||||
{ id: 'minimax-portal', name: 'MiniMax (Global)', icon: '☁️', placeholder: 'sk-...', model: 'MiniMax', requiresApiKey: false, isOAuth: true, supportsApiKey: true, defaultModelId: 'MiniMax-M2.7', showModelId: true, showModelIdInDevModeOnly: true, modelIdPlaceholder: 'MiniMax-M2.7', apiKeyUrl: 'https://platform.minimax.io' },
|
||||
{ id: 'qwen-portal', name: 'Qwen (Global)', icon: '☁️', placeholder: 'sk-...', model: 'Qwen', requiresApiKey: false, isOAuth: true, defaultModelId: 'coder-model', showModelId: true, showModelIdInDevModeOnly: true, modelIdPlaceholder: 'coder-model' },
|
||||
{ id: 'ark', name: 'ByteDance Ark', icon: 'A', placeholder: 'your-ark-api-key', model: 'Doubao', requiresApiKey: true, defaultBaseUrl: 'https://ark.cn-beijing.volces.com/api/v3', showBaseUrl: true, showModelId: true, modelIdPlaceholder: 'ep-20260228000000-xxxxx', docsUrl: 'https://www.volcengine.com/', codePlanPresetBaseUrl: 'https://ark.cn-beijing.volces.com/api/coding/v3', codePlanPresetModelId: 'ark-code-latest', codePlanDocsUrl: 'https://www.volcengine.com/docs/82379/1928261?lang=zh' },
|
||||
|
||||
@@ -192,13 +192,13 @@ export function Models() {
|
||||
const usageLoading = isGatewayRunning && fetchState.status === 'loading';
|
||||
|
||||
return (
|
||||
<div className="flex flex-col -m-6 dark:bg-background h-[calc(100vh-2.5rem)] overflow-hidden">
|
||||
<div data-testid="models-page" className="flex flex-col -m-6 dark:bg-background h-[calc(100vh-2.5rem)] overflow-hidden">
|
||||
<div className="w-full max-w-5xl mx-auto flex flex-col h-full p-10 pt-16">
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row md:items-start justify-between mb-12 shrink-0 gap-4">
|
||||
<div>
|
||||
<h1 className="text-5xl md:text-6xl font-serif text-foreground mb-3 font-normal tracking-tight" style={{ fontFamily: 'Georgia, Cambria, "Times New Roman", Times, serif' }}>
|
||||
<h1 data-testid="models-page-title" className="text-5xl md:text-6xl font-serif text-foreground mb-3 font-normal tracking-tight" style={{ fontFamily: 'Georgia, Cambria, "Times New Roman", Times, serif' }}>
|
||||
{t('dashboard:models.title')}
|
||||
</h1>
|
||||
<p className="text-[17px] text-foreground/70 font-medium">
|
||||
|
||||
@@ -448,7 +448,7 @@ export function Settings() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col -m-6 dark:bg-background h-[calc(100vh-2.5rem)] overflow-hidden">
|
||||
<div data-testid="settings-page" className="flex flex-col -m-6 dark:bg-background h-[calc(100vh-2.5rem)] overflow-hidden">
|
||||
<div className="w-full max-w-5xl mx-auto flex flex-col h-full p-10 pt-16">
|
||||
|
||||
{/* Header */}
|
||||
@@ -612,6 +612,7 @@ export function Settings() {
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
data-testid="settings-dev-mode-switch"
|
||||
checked={devModeUnlocked}
|
||||
onCheckedChange={setDevModeUnlocked}
|
||||
/>
|
||||
@@ -638,8 +639,8 @@ export function Settings() {
|
||||
{devModeUnlocked && (
|
||||
<>
|
||||
<Separator className="bg-black/5 dark:bg-white/5" />
|
||||
<div>
|
||||
<h2 className="text-3xl font-serif text-foreground mb-6 font-normal tracking-tight" style={{ fontFamily: 'Georgia, Cambria, "Times New Roman", Times, serif' }}>
|
||||
<div data-testid="settings-developer-section">
|
||||
<h2 data-testid="settings-developer-title" className="text-3xl font-serif text-foreground mb-6 font-normal tracking-tight" style={{ fontFamily: 'Georgia, Cambria, "Times New Roman", Times, serif' }}>
|
||||
{t('developer.title')}
|
||||
</h2>
|
||||
<div className="space-y-8">
|
||||
@@ -756,6 +757,7 @@ export function Settings() {
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Input
|
||||
data-testid="settings-developer-gateway-token"
|
||||
readOnly
|
||||
value={controlUiInfo?.token || ''}
|
||||
placeholder={t('developer.tokenUnavailable')}
|
||||
|
||||
@@ -200,7 +200,7 @@ export function Setup() {
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col overflow-hidden bg-background text-foreground">
|
||||
<div data-testid="setup-page" className="flex h-screen flex-col overflow-hidden bg-background text-foreground">
|
||||
<TitleBar />
|
||||
<div className="flex-1 overflow-auto">
|
||||
{/* Progress Indicator */}
|
||||
@@ -293,11 +293,11 @@ export function Setup() {
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{!isLastStep && safeStepIndex !== STEP.RUNTIME && (
|
||||
<Button variant="ghost" onClick={handleSkip}>
|
||||
<Button data-testid="setup-skip-button" variant="ghost" onClick={handleSkip}>
|
||||
{t('nav.skipSetup')}
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={handleNext} disabled={!canProceed}>
|
||||
<Button data-testid="setup-next-button" onClick={handleNext} disabled={!canProceed}>
|
||||
{isLastStep ? (
|
||||
t('nav.getStarted')
|
||||
) : (
|
||||
@@ -324,7 +324,7 @@ function WelcomeContent() {
|
||||
const { language, setLanguage } = useSettingsStore();
|
||||
|
||||
return (
|
||||
<div className="text-center space-y-4">
|
||||
<div data-testid="setup-welcome-step" className="text-center space-y-4">
|
||||
<div className="mb-4 flex justify-center">
|
||||
<img src={clawxIcon} alt="ClawX" className="h-16 w-16" />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user