Files
opsxapi/src/features/testing/TestingView.tsx
T
logan 2bb4ef7a95 桌面 GUI 外链修复 + 主题顺序调整(含结构扁平化进行中的工作区基线)
- 集成 tauri-plugin-opener:Cargo/lib.rs/capabilities/npm 四处接线,新增 src/openExternal.ts(桌面走 opener,浏览器回退 window.open,相对路径绝对化)
- 关于页 4 个链接、活动栏官网菜单、代理错误横幅 2 处健康检查链接全部改走 openExternal,桌面端可正常跳转
- 设置→外观 主题顺序调整为 深色/浅色/跟随系统
- release 二进制重编并验证(浏览器实测关于页链接可开文档页)
- 附带工作区中未提交的扁平化重构现状(另一会话进行中)
- homepage 落地页与文档终检通过(8787 / 与 /docs 双 200)
2026-10-16 03:03:36 +08:00

121 lines
6.1 KiB
TypeScript

import { useTestActions } from "./useTestActions";
import type { TestRun } from "../../core/index";
import { useEffect, useState } from "react";
import { useWorkspace } from "../../workspaceStore";
import { cx } from "../../components/common";
import { Icon } from "../../components/icons";
import { Button } from "../../components/primitives";
import { AiCaseDialog } from "../../components/dialogs/AiCaseDialog";
import { ScheduleDialog } from "../../components/dialogs/ScheduleDialog";
import { CaseDetail } from "./CaseDetail";
import { installHttp8Regression } from "./regression";
import { WorkspaceSidebar } from "../../components/WorkspaceSidebar";
export function TestingView() {
const testCases = useWorkspace((s) => s.testCases);
const testRuns = useWorkspace((s) => s.testRuns);
const schedules = useWorkspace((s) => s.schedules);
const addTestCase = useWorkspace((s) => s.addTestCase);
const [selected, setSelected] = useState<string | null>(testCases[0]?.id ?? null);
const [showAi, setShowAi] = useState(false);
const [showSchedule, setShowSchedule] = useState(false);
const { busy: runAllBusy, runAll } = useTestActions();
const tc = testCases.find((c) => c.id === selected) ?? null;
const lastRunOf = (caseId: string): TestRun | undefined => testRuns.find((r) => r.caseId === caseId);
useEffect(() => {
if (!testCases.some((c) => c.id === selected)) setSelected(testCases[0]?.id ?? null);
}, [testCases, selected]);
return (
<div className="flex min-h-0 min-w-0 flex-1">
{/* 左栏:用例列表 */}
<WorkspaceSidebar label="测试" className="test-case-sidebar">
<div className="flex items-center gap-1.5 p-2.5" style={{ borderBottom: "1px solid var(--border)" }}>
<Icon name="list-checks" size={15} className="ops-accent" />
<span className="text-[13px] font-semibold">场景用例</span>
<span className="ops-faint ml-auto text-[11px]">{testCases.length}</span>
</div>
<div className="p-2.5" style={{ borderBottom: "1px solid var(--border)" }}>
<div className="grid grid-cols-2 gap-1.5">
<button className="ops-btn ops-btn-secondary !px-2" onClick={() => setSelected(addTestCase())}>
<Icon name="plus" size={13} /> 新建
</button>
<button className="ops-btn ops-btn-secondary !px-2" onClick={() => setShowAi(true)}>
<Icon name="bot" size={13} /> AI 生成
</button>
</div>
<details className="mt-2 text-xs"><summary className="ops-faint cursor-pointer px-1 py-1">示例与学习</summary><button className="ops-btn ops-btn-ghost mt-1 w-full justify-start !px-2" title="装载本地 Gin /v1/echo 八组合回归用例" onClick={() => {
const state = useWorkspace.getState();
if ((state.collections.some((c) => c.name === "示例集合") || state.testCases.some((c) => c.id === "tc-opsx-http8-regression")) && !confirm("装载示例会刷新已有的示例集合和回归用例。确定继续?")) return;
setSelected(installHttp8Regression());
}}><Icon name="play" size={13} /> 装载回归示例</button></details>
</div>
<div className="ops-scroll min-h-0 flex-1 p-2">
{testCases.length === 0 && (
<div className="ops-faint px-2 py-8 text-center text-xs leading-5">
还没有用例
<br />
点「新建」手动编排,或用「AI 生成」
</div>
)}
{testCases.map((c) => {
const last = lastRunOf(c.id);
const sched = schedules.find((s) => s.caseId === c.id && s.enabled);
return (
<button
key={c.id}
className={cx("ops-tree-row w-full text-left", selected === c.id && "ops-tree-row-active")}
onClick={() => setSelected(c.id)}
>
<span className="truncate text-[12px] font-medium">{c.name}</span>
<span className="ops-faint ml-auto shrink-0 text-[11px]">{c.steps.length} 步</span>
{sched && <Icon name="calendar-clock" size={11} className="ops-accent" />}
{last && (
<Icon
name={last.passed ? "circle-check" : "circle-x"}
size={12}
className={last.passed ? "ops-success" : "ops-danger"}
/>
)}
</button>
);
})}
</div>
<div className="space-y-1.5 p-2.5" style={{ borderTop: "1px solid var(--border)" }}>
<button className="ops-btn ops-btn-secondary w-full" onClick={() => setShowSchedule(true)}>
<Icon name="calendar-clock" size={13} /> 定时任务
{schedules.filter((s) => s.enabled).length > 0 && (
<span className="ops-count">{schedules.filter((s) => s.enabled).length}</span>
)}
</button>
<Button variant="primary" className="w-full" busy={runAllBusy} disabled={testCases.length === 0} onClick={runAll}>
<Icon name="play" size={13} /> {runAllBusy ? "运行中…" : "运行全部"}
</Button>
</div>
</WorkspaceSidebar>
{/* 右侧:用例详情 */}
{tc ? (
<CaseDetail key={tc.id} tc={tc} onOpenSchedule={() => setShowSchedule(true)} />
) : (
<div className="grid min-w-0 flex-1 place-items-center">
<div className="text-center">
<div className="ops-dim mb-2 flex justify-center"><Icon name="list-checks" size={40} /></div>
<div className="ops-dim text-sm font-semibold">自动化测试</div>
<div className="ops-faint mt-1 text-xs leading-5">
手动编排 · AI 生成 · 定时调度,三种方式共用同一套场景用例
<br />
左侧新建用例;侧栏底部「自动化测试」随时进出本视图
</div>
</div>
</div>
)}
{showAi && <AiCaseDialog onClose={() => setShowAi(false)} onSaved={(id) => setSelected(id)} />}
{showSchedule && <ScheduleDialog focusCaseId={selected} onClose={() => setShowSchedule(false)} />}
</div>
);
}