- 集成 tauri-plugin-opener:Cargo/lib.rs/capabilities/npm 四处接线,新增 src/openExternal.ts(桌面走 opener,浏览器回退 window.open,相对路径绝对化) - 关于页 4 个链接、活动栏官网菜单、代理错误横幅 2 处健康检查链接全部改走 openExternal,桌面端可正常跳转 - 设置→外观 主题顺序调整为 深色/浅色/跟随系统 - release 二进制重编并验证(浏览器实测关于页链接可开文档页) - 附带工作区中未提交的扁平化重构现状(另一会话进行中) - homepage 落地页与文档终检通过(8787 / 与 /docs 双 200)
121 lines
6.1 KiB
TypeScript
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>
|
|
);
|
|
}
|