@allbluecn/web-app 0.10.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +26 -11
- package/public/favicon.ico +0 -0
- package/public/logo/allblue-logo-app-icon.svg +7 -0
- package/public/logo/allblue-logo-dark-square.svg +4 -0
- package/public/logo/allblue-logo-dark.svg +4 -0
- package/public/logo/allblue-logo-light-square.svg +4 -0
- package/public/logo/allblue-logo-light.svg +4 -0
- package/src/__tests__/bench/baselines/format-timestamp.json +44 -0
- package/src/__tests__/bench/core-hot-path.bench.ts +71 -0
- package/src/__tests__/integration/db-crud.integration.test.ts +79 -0
- package/src/__tests__/rejects-tothrow-regression.test.ts +31 -0
- package/src/__tests__/security/ai/prompt-injection.test.ts +105 -0
- package/src/__tests__/security/ssrf-url-whitelist.test.ts +54 -0
- package/src/__tests__/security/xss-sanitize.test.ts +99 -0
- package/src/__tests__/setup.ts +9 -0
- package/src/__tests__/visual/__screenshots__/theme-surface.vrt.test.tsx/theme-surface-dark-baseline-1-chromium-darwin.png +0 -0
- package/src/__tests__/visual/__screenshots__/theme-surface.vrt.test.tsx/theme-surface-light-baseline-1-chromium-darwin.png +0 -0
- package/src/__tests__/visual/__screenshots__/user-avatar.vrt.test.tsx/user-avatar-initial-fallback-baseline-1-chromium-darwin.png +0 -0
- package/src/__tests__/visual/theme-surface.vrt.test.tsx +54 -0
- package/src/__tests__/visual/user-avatar.vrt.test.tsx +32 -0
- package/src/components/error/global-error-boundary.tsx +2 -2
- package/src/components/knowledge/general-groups.ts +15 -0
- package/src/components/knowledge/kb-filter-popover.tsx +143 -0
- package/src/components/knowledge-public/legal-compliance-editor.tsx +4 -2
- package/src/components/layout/sidebar-03/__tests__/nav-workspace.test.tsx +9 -6
- package/src/components/layout/sidebar-03/nav-knowledge.tsx +19 -17
- package/src/components/layout/sidebar-03/nav-secondary.tsx +57 -47
- package/src/components/layout/sidebar-03/nav-workspace.tsx +20 -22
- package/src/components/mdx.tsx +9 -0
- package/src/components/shared-kit/avatars/notion-avatar.tsx +2 -1
- package/src/components/shared-kit/icons/__tests__/lucide-curated-icons.test.ts +2 -2
- package/src/components/shared-kit/icons/__tests__/lucide-icon-picker.test.tsx +3 -2
- package/src/components/shared-kit/icons/lucide-icon-picker/curated-icons.ts +5 -10
- package/src/components/shared-kit/icons/lucide-icon-picker/lucide-icon-by-name.tsx +3 -3
- package/src/components/shared-kit/theme/theme-surface.tsx +52 -0
- package/src/components/story/relations/general-groups.ts +1 -15
- package/src/components/story/relations/knowledge-select-dialog.tsx +13 -87
- package/src/components/story/story-list-view.tsx +7 -1
- package/src/components/tiptap/node/table-node/ui/table-move-row-column-button/use-table-move-row-column.ts +0 -2
- package/src/components/workspace/__tests__/activity/workspace-activity-card.test.tsx +36 -0
- package/src/components/workspace/__tests__/contribution/workspace-contribution-card.test.tsx +17 -0
- package/src/components/workspace/__tests__/dialogs/create-workspace-dialog.test.tsx +63 -0
- package/src/components/workspace/__tests__/dialogs/delete-workspace-options.test.tsx +89 -0
- package/src/components/workspace/__tests__/dialogs/edit-workspace-dialog.test.tsx +68 -0
- package/src/components/workspace/__tests__/header/workspace-header.test.tsx +40 -0
- package/src/components/workspace/__tests__/kb/workspace-kb-card.test.tsx +154 -0
- package/src/components/workspace/__tests__/members/workspace-member-item.test.tsx +24 -0
- package/src/components/workspace/__tests__/members/workspace-members-manager.test.tsx +51 -0
- package/src/components/workspace/__tests__/projects/workspace-project-list.test.tsx +46 -0
- package/src/components/workspace/{create-workspace-dialog.tsx → dialogs/create-workspace-dialog.tsx} +1 -1
- package/src/components/workspace/{edit-workspace-dialog.tsx → dialogs/edit-workspace-dialog.tsx} +1 -1
- package/src/components/workspace/kb/workspace-kb-card.tsx +594 -0
- package/src/components/workspace/kb/workspace-kb-manage-dialog.tsx +174 -0
- package/src/components/workspace/kb/workspace-quick-doc-dialog.tsx +431 -0
- package/src/components/workspace/{workspace-members-manager.tsx → members/workspace-members-manager.tsx} +78 -40
- package/src/lib/__tests__/msw-sample.test.ts +68 -0
- package/src/lib/__tests__/pgvector.test.ts +2 -2
- package/src/lib/changelog/sdk-versions.ts +21 -21
- package/src/lib/collections/knowledge/knowledge-bases-collection.ts +3 -1
- package/src/lib/collections/reference/workspaces-collection.ts +3 -1
- package/src/lib/fetch-json.ts +23 -0
- package/src/lib/fumadocs-i18n.ts +7 -0
- package/src/{components/workspace/workspace-kb-card.tsx → lib/i18n/resolver.ts} +6 -12
- package/src/lib/layout.shared.tsx +40 -0
- package/src/lib/sanitize.ts +14 -1
- package/src/lib/session.ts +9 -1
- package/src/lib/source.ts +15 -0
- package/src/lib/vector-store/__tests__/factory.test.ts +1 -1
- package/src/lib/vector-store/__tests__/sqlite-vec-store.test.ts +2 -2
- package/src/plugins/core/tags/__tests__/tags.test.ts +10 -10
- package/src/routeTree.gen.ts +65 -0
- package/src/routes/$lang/docs/$/route.lazy.tsx +62 -0
- package/src/routes/$lang/docs/$/route.tsx +52 -0
- package/src/routes/-tests/docs-redirect.test.ts +45 -0
- package/src/routes/__root.tsx +13 -7
- package/src/routes/__tests__/-edition-routing.test.tsx +4 -4
- package/src/routes/__tests__/route-tree-structure.test.ts +65 -0
- package/src/routes/_nav/knowledge/compliance/index.lazy.tsx +5 -2
- package/src/routes/_nav/knowledge/index.lazy.tsx +1 -1
- package/src/routes/_nav/knowledge/laws/index.lazy.tsx +5 -2
- package/src/routes/_nav/route.tsx +21 -0
- package/src/routes/_nav/workspaces/$workspaceId/route.lazy.tsx +13 -11
- package/src/routes/_nav/workspaces/$workspaceId/route.tsx +4 -2
- package/src/routes/_nav/workspaces/$workspaceId/settings/route.lazy.tsx +1 -1
- package/src/routes/_nav/workspaces/index.lazy.tsx +3 -3
- package/src/routes/api/search.ts +13 -0
- package/src/routes/docs.tsx +9 -0
- package/src/server/__tests__/agent-crud.test.ts +4 -4
- package/src/server/__tests__/chat-conversations.test.ts +1 -3
- package/src/server/__tests__/fetch-guard.test.ts +99 -0
- package/src/server/__tests__/permissions.test.ts +1 -3
- package/src/server/__tests__/resources.team-seats.test.ts +2 -0
- package/src/server/__tests__/skill-crud.test.ts +3 -3
- package/src/server/ai-gateway/__tests__/ai-output-guard.test.ts +95 -0
- package/src/server/ai-gateway/__tests__/kb-context-guard.test.ts +136 -0
- package/src/server/ai-gateway/__tests__/knowledge-search-fence.test.ts +240 -0
- package/src/server/ai-gateway/__tests__/llm-fence.test.ts +125 -0
- package/src/server/ai-gateway/__tests__/run-store.test.ts +52 -0
- package/src/server/ai-gateway/ai-output-guard.ts +77 -0
- package/src/server/ai-gateway/index.ts +5 -0
- package/src/server/ai-gateway/kb-context-guard.ts +46 -0
- package/src/server/ai-gateway/llm-fence.ts +82 -0
- package/src/server/ai-gateway/persistence/run-store.ts +19 -0
- package/src/server/ai-gateway/story-prompt.ts +4 -2
- package/src/server/ai-gateway/tools/definitions.ts +12 -6
- package/src/server/ai-gateway/tools/server.ts +97 -54
- package/src/server/builtin-project.ts +21 -0
- package/src/server/email/sender.ts +8 -2
- package/src/server/fetch-guard.ts +170 -0
- package/src/server/middlewares/security-headers.ts +113 -0
- package/src/server/serverFns/__tests__/billing.test.ts +263 -0
- package/src/server/serverFns/__tests__/link-preview.test.ts +239 -0
- package/src/server/serverFns/__tests__/project-kb.test.ts +3 -3
- package/src/server/serverFns/__tests__/project-team-assign.test.ts +1 -1
- package/src/server/serverFns/__tests__/story-comments.test.ts +265 -0
- package/src/server/serverFns/__tests__/story-crud-archive-detail.test.ts +266 -0
- package/src/server/serverFns/__tests__/story-crud-labels-users.test.ts +227 -0
- package/src/server/serverFns/__tests__/story-crud.test.ts +526 -0
- package/src/server/serverFns/__tests__/story-links.test.ts +262 -0
- package/src/server/serverFns/__tests__/story-list-query.test.ts +259 -0
- package/src/server/serverFns/__tests__/story-reactions.test.ts +230 -0
- package/src/server/serverFns/__tests__/story-tree.test.ts +317 -0
- package/src/server/serverFns/__tests__/story-types.test.ts +294 -0
- package/src/server/serverFns/__tests__/team-accept.test.ts +6 -6
- package/src/server/serverFns/__tests__/team-invitation.test.ts +3 -3
- package/src/server/serverFns/__tests__/team-join-request.test.ts +8 -8
- package/src/server/serverFns/__tests__/update-seats.test.ts +4 -4
- package/src/server/serverFns/__tests__/workspace-kb-idor.test.ts +160 -0
- package/src/server/serverFns/__tests__/workspace-kb-utils.test.ts +54 -0
- package/src/server/serverFns/__tests__/workspace-kb.test.ts +348 -0
- package/src/server/serverFns/__tests__/workspace.test.ts +28 -8
- package/src/server/serverFns/iteration/__tests__/iteration-crud.test.ts +3 -5
- package/src/server/serverFns/iteration/__tests__/iteration-stories.test.ts +1 -1
- package/src/server/serverFns/iteration/__tests__/iteration-transfer.test.ts +2 -2
- package/src/server/serverFns/kb-doc-search.ts +111 -0
- package/src/server/serverFns/knowledge-public/compliance/create.ts +2 -1
- package/src/server/serverFns/knowledge-public/legal/create.ts +2 -1
- package/src/server/serverFns/story/__tests__/bulk.test.ts +1 -1
- package/src/server/serverFns/story/__tests__/label-suggest-fn.test.ts +2 -6
- package/src/server/serverFns/story/__tests__/story-assoc.test.ts +2 -4
- package/src/server/serverFns/story/__tests__/story-dependencies.test.ts +2 -2
- package/src/server/serverFns/story/__tests__/story-groups.test.ts +2 -2
- package/src/server/serverFns/story/kb-general-consts.ts +39 -0
- package/src/server/serverFns/story/kb-general.ts +30 -17
- package/src/server/serverFns/story/story-assoc.ts +13 -103
- package/src/server/serverFns/story/story-crud.ts +2 -5
- package/src/server/serverFns/story/story-groups.ts +4 -13
- package/src/server/serverFns/team.ts +1 -1
- package/src/server/serverFns/workspace-kb-utils.ts +138 -0
- package/src/server/serverFns/workspace-kb.ts +403 -0
- package/src/server/serverFns/workspace.ts +4 -1
- package/src/shared/ai-cli/__tests__/adapter.test.ts +67 -0
- package/src/shared/ai-cli/__tests__/cli-detector.test.ts +99 -0
- package/src/shared/ai-cli/adapter.ts +2 -2
- package/src/shared/ai-cli/cli-detector.ts +18 -9
- package/src/start.ts +2 -1
- package/src/styles/globals.css +116 -0
- package/src/types/jest-dom-vitest.d.ts +8 -7
- package/vite.shared.ts +18 -1
- package/public/file.svg +0 -1
- package/public/globe.svg +0 -1
- package/public/window.svg +0 -1
- package/src/lib/__tests__/cli-detector.test.ts +0 -80
- /package/src/components/workspace/{workspace-activity-card.tsx → activity/workspace-activity-card.tsx} +0 -0
- /package/src/components/workspace/{workspace-contribution-card.tsx → contribution/workspace-contribution-card.tsx} +0 -0
- /package/src/components/workspace/{delete-workspace-options.tsx → dialogs/delete-workspace-options.tsx} +0 -0
- /package/src/components/workspace/{workspace-header.tsx → header/workspace-header.tsx} +0 -0
- /package/src/components/workspace/{workspace-member-item.tsx → members/workspace-member-item.tsx} +0 -0
- /package/src/components/workspace/{workspace-project-list.tsx → projects/workspace-project-list.tsx} +0 -0
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import * as React from "react";
|
|
19
19
|
import { useServerFn } from "@tanstack/react-start";
|
|
20
20
|
import { toast } from "sonner";
|
|
21
|
-
import {
|
|
21
|
+
import { BookOpenText, BookMarked, Bookmark, Check, Scale, ShieldCheck, Sprout } from "lucide-react";
|
|
22
22
|
import { Button } from "@/components/ui/button";
|
|
23
23
|
import { Input } from "@/components/ui/input";
|
|
24
24
|
import { Badge } from "@/components/ui/badge";
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
type SearchDocRow,
|
|
41
41
|
} from "@/server/serverFns/story";
|
|
42
42
|
import { generalGroupLabel } from "./general-groups";
|
|
43
|
+
import { KbFilterPopover, type KbFilter } from "@/components/knowledge/kb-filter-popover";
|
|
43
44
|
import type { DocBrief } from "../types";
|
|
44
45
|
import { m } from "@/paraglide/messages.js";
|
|
45
46
|
|
|
@@ -50,24 +51,6 @@ interface KbBrief {
|
|
|
50
51
|
visibility: string;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
type KbFilter = { type: "kb"; id: string } | { type: "general"; group: string } | null;
|
|
54
|
-
|
|
55
|
-
const GENERAL_GROUP_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
|
|
56
|
-
legal: Scale,
|
|
57
|
-
compliance: ShieldCheck,
|
|
58
|
-
dictionary: BookMarked,
|
|
59
|
-
bookmark: Bookmark,
|
|
60
|
-
summary: Sprout,
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const GENERAL_GROUP_META = [
|
|
64
|
-
{ key: "legal" },
|
|
65
|
-
{ key: "compliance" },
|
|
66
|
-
{ key: "dictionary" },
|
|
67
|
-
{ key: "bookmark" },
|
|
68
|
-
{ key: "summary" },
|
|
69
|
-
] as const;
|
|
70
|
-
|
|
71
54
|
export interface KnowledgeSelectDialogProps {
|
|
72
55
|
open: boolean;
|
|
73
56
|
onOpenChange: (open: boolean) => void;
|
|
@@ -98,7 +81,6 @@ export function KnowledgeSelectDialog({
|
|
|
98
81
|
const [selected, setSelected] = React.useState<Set<string>>(new Set());
|
|
99
82
|
const [loading, setLoading] = React.useState(false);
|
|
100
83
|
const [submitting, setSubmitting] = React.useState(false);
|
|
101
|
-
const [kbPopoverOpen, setKbPopoverOpen] = React.useState(false);
|
|
102
84
|
|
|
103
85
|
const linkedIds = React.useMemo(() => new Set(linkedDocs.map((d) => d.id)), [linkedDocs]);
|
|
104
86
|
|
|
@@ -156,7 +138,6 @@ export function KnowledgeSelectDialog({
|
|
|
156
138
|
|
|
157
139
|
const handleKbSelect = (filter: KbFilter) => {
|
|
158
140
|
setKbFilter(filter);
|
|
159
|
-
setKbPopoverOpen(false);
|
|
160
141
|
void loadDocs(filter, search);
|
|
161
142
|
};
|
|
162
143
|
|
|
@@ -281,7 +262,6 @@ export function KnowledgeSelectDialog({
|
|
|
281
262
|
}
|
|
282
263
|
};
|
|
283
264
|
|
|
284
|
-
const activeKb = kbFilter?.type === "kb" ? kbs.find((k) => k.id === kbFilter.id) : undefined;
|
|
285
265
|
|
|
286
266
|
return (
|
|
287
267
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
@@ -290,71 +270,17 @@ export function KnowledgeSelectDialog({
|
|
|
290
270
|
<DialogTitle>{m.story_knowledgeDialog_title()}</DialogTitle>
|
|
291
271
|
</DialogHeader>
|
|
292
272
|
|
|
293
|
-
<
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
{activeKb
|
|
305
|
-
? activeKb.name
|
|
306
|
-
: kbFilter?.type === "general"
|
|
307
|
-
? generalGroupLabel(kbFilter.group)
|
|
308
|
-
: m.story_knowledgeDialog_allKbs()}
|
|
309
|
-
<ChevronsUpDown className="size-3.5 opacity-50" />
|
|
310
|
-
</Button>
|
|
311
|
-
</PopoverTrigger>
|
|
312
|
-
<PopoverContent align="end" className="w-60 p-1">
|
|
313
|
-
<button
|
|
314
|
-
type="button"
|
|
315
|
-
className="flex w-full items-center justify-between rounded-sm px-2 py-1.5 mt-1 text-sm hover:bg-muted"
|
|
316
|
-
onClick={() => handleKbSelect(null)}
|
|
317
|
-
>
|
|
318
|
-
{m.story_knowledgeDialog_allKbs()}
|
|
319
|
-
{kbFilter === null && <Check className="size-3.5" />}
|
|
320
|
-
</button>
|
|
321
|
-
<div className="border-t px-2 pt-3 text-xs font-medium text-muted-foreground">
|
|
322
|
-
{m.story_knowledgeDialog_userKbs()}
|
|
323
|
-
</div>
|
|
324
|
-
{kbs.map((kb) => (
|
|
325
|
-
<button
|
|
326
|
-
key={kb.id}
|
|
327
|
-
type="button"
|
|
328
|
-
className="flex w-full items-center justify-between rounded-sm px-2 py-1.5 text-sm hover:bg-muted"
|
|
329
|
-
onClick={() => handleKbSelect({ type: "kb", id: kb.id })}
|
|
330
|
-
>
|
|
331
|
-
<span className="truncate">{kb.name}</span>
|
|
332
|
-
{kbFilter?.type === "kb" && kbFilter.id === kb.id && <Check className="size-3.5" />}
|
|
333
|
-
</button>
|
|
334
|
-
))}
|
|
335
|
-
<div className=" border-t px-2 pt-3 text-xs font-medium text-muted-foreground">
|
|
336
|
-
{m.story_knowledgeDialog_generalKbs()}
|
|
337
|
-
</div>
|
|
338
|
-
{GENERAL_GROUP_META.map((group) => {
|
|
339
|
-
const Icon = GENERAL_GROUP_ICONS[group.key];
|
|
340
|
-
return (
|
|
341
|
-
<button
|
|
342
|
-
key={group.key}
|
|
343
|
-
type="button"
|
|
344
|
-
className="flex w-full items-center justify-between rounded-sm px-2 py-1.5 text-sm hover:bg-muted"
|
|
345
|
-
onClick={() => handleKbSelect({ type: "general", group: group.key })}
|
|
346
|
-
>
|
|
347
|
-
<span className="flex min-w-0 items-center gap-2">
|
|
348
|
-
<Icon className="size-3.5 shrink-0 text-muted-foreground" />
|
|
349
|
-
<span className="truncate">{generalGroupLabel(group.key)}</span>
|
|
350
|
-
</span>
|
|
351
|
-
{kbFilter?.type === "general" && kbFilter.group === group.key && <Check className="size-3.5" />}
|
|
352
|
-
</button>
|
|
353
|
-
);
|
|
354
|
-
})}
|
|
355
|
-
</PopoverContent>
|
|
356
|
-
</Popover>
|
|
357
|
-
</div>
|
|
273
|
+
<KbFilterPopover
|
|
274
|
+
search={search}
|
|
275
|
+
onSearchChange={handleSearch}
|
|
276
|
+
filter={kbFilter}
|
|
277
|
+
onFilterChange={(f) => { setKbFilter(f); void loadDocs(f, search); }}
|
|
278
|
+
userKbs={kbs}
|
|
279
|
+
searchPlaceholder={m.story_knowledgeDialog_searchPlaceholder()}
|
|
280
|
+
allLabel={m.story_knowledgeDialog_allKbs()}
|
|
281
|
+
userKbsLabel={m.story_knowledgeDialog_userKbs()}
|
|
282
|
+
generalKbsLabel={m.story_knowledgeDialog_generalKbs()}
|
|
283
|
+
/>
|
|
358
284
|
|
|
359
285
|
<div className="h-80 overflow-y-auto rounded-md border p-1">
|
|
360
286
|
{loading ? (
|
|
@@ -79,7 +79,13 @@ export function StoryListView({ stories: initialStories, users: initialUsers, pr
|
|
|
79
79
|
const [users, setUsers] = useState<UserBrief[]>(initialUsers);
|
|
80
80
|
const [projectId, setProjectId] = useState<string | undefined>(() => {
|
|
81
81
|
try {
|
|
82
|
-
|
|
82
|
+
const stored = localStorage.getItem(STORY_PROJECT_FILTER_KEY) ?? undefined;
|
|
83
|
+
// 清理过期残留(项目已删除或 localStorage 跨账号残留),避免列表查询空集
|
|
84
|
+
if (stored && stored !== "__archived__" && !initialProjects.some((p) => p.id === stored)) {
|
|
85
|
+
localStorage.removeItem(STORY_PROJECT_FILTER_KEY);
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
return stored;
|
|
83
89
|
} catch {
|
|
84
90
|
return undefined;
|
|
85
91
|
}
|
|
@@ -315,8 +315,6 @@ function tableMoveRowColumn({
|
|
|
315
315
|
const moveOperation =
|
|
316
316
|
finalOrientation === "row" ? moveTableRow : moveTableColumn
|
|
317
317
|
|
|
318
|
-
console.log({ from, to, finalOrientation, direction })
|
|
319
|
-
|
|
320
318
|
const dispatch = (tr: Transaction) => editor.view.dispatch(tr)
|
|
321
319
|
|
|
322
320
|
if (editor.state.selection instanceof CellSelection) {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { WorkspaceActivityCard } from "../../activity/workspace-activity-card";
|
|
4
|
+
|
|
5
|
+
vi.mock("@/components/project/project-activity-list", () => ({
|
|
6
|
+
ProjectActivityList: ({ activities }: { activities: any[] }) => (
|
|
7
|
+
<ul data-testid="activity-list">
|
|
8
|
+
{activities.map((a: any) => (
|
|
9
|
+
<li key={a.id}>{a.action}</li>
|
|
10
|
+
))}
|
|
11
|
+
</ul>
|
|
12
|
+
),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock("@tanstack/react-router", () => ({
|
|
16
|
+
Link: ({ children }: any) => <a>{children}</a>,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
describe("WorkspaceActivityCard", () => {
|
|
20
|
+
it("渲染活动列表", () => {
|
|
21
|
+
const ACTIVITIES = [
|
|
22
|
+
{
|
|
23
|
+
id: "a1",
|
|
24
|
+
action: "创建项目",
|
|
25
|
+
projectId: "p1",
|
|
26
|
+
userId: "u1",
|
|
27
|
+
targetType: null,
|
|
28
|
+
targetId: null,
|
|
29
|
+
metadata: {},
|
|
30
|
+
createdAt: new Date(),
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
render(<WorkspaceActivityCard activities={ACTIVITIES} workspaceId="ws1" />);
|
|
34
|
+
expect(screen.getByTestId("activity-list")).toBeInTheDocument();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { WorkspaceContributionCard } from "../../contribution/workspace-contribution-card";
|
|
4
|
+
|
|
5
|
+
vi.mock("@/components/shared-kit/data-viz/contribution-graph-tooltip", () => ({
|
|
6
|
+
ContributionGraphWithTooltip: ({ title }: { title: string }) => (
|
|
7
|
+
<div data-testid="contribution-graph">{title}</div>
|
|
8
|
+
),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
describe("WorkspaceContributionCard", () => {
|
|
12
|
+
it("渲染贡献图卡片", () => {
|
|
13
|
+
const data = [{ date: "2024-01-01", count: 5, level: 3 }];
|
|
14
|
+
render(<WorkspaceContributionCard data={data} />);
|
|
15
|
+
expect(screen.getByTestId("contribution-graph")).toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { CreateWorkspaceDialog } from "../../dialogs/create-workspace-dialog";
|
|
4
|
+
|
|
5
|
+
vi.mock("@/components/ui/button", () => ({
|
|
6
|
+
Button: ({ children, ...props }: any) => <button {...props}>{children}</button>,
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
vi.mock("@/components/ui/input", () => ({
|
|
10
|
+
Input: (props: any) => <input {...props} />,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock("@/components/ui/dialog", () => ({
|
|
14
|
+
Dialog: ({ children }: any) => <div>{children}</div>,
|
|
15
|
+
DialogContent: ({ children }: any) => <div>{children}</div>,
|
|
16
|
+
DialogFooter: ({ children }: any) => <div>{children}</div>,
|
|
17
|
+
DialogHeader: ({ children }: any) => <div>{children}</div>,
|
|
18
|
+
DialogTitle: ({ children }: any) => <h2>{children}</h2>,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock("@/components/shared-kit/icons/lucide-icon-picker", () => ({
|
|
22
|
+
LucideIconPicker: () => <div data-testid="icon-picker" />,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock("@tanstack/react-router", () => ({
|
|
26
|
+
useRouter: () => ({ navigate: vi.fn() }),
|
|
27
|
+
Link: ({ children }: any) => <a>{children}</a>,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
vi.mock("@tanstack/react-form", () => {
|
|
31
|
+
const makeField = (value: string) => ({
|
|
32
|
+
value,
|
|
33
|
+
handleChange: vi.fn(),
|
|
34
|
+
handleBlur: vi.fn(),
|
|
35
|
+
meta: { errors: [] },
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
useForm: () => {
|
|
39
|
+
const fields = {
|
|
40
|
+
name: makeField(""),
|
|
41
|
+
icon: makeField("folder"),
|
|
42
|
+
description: makeField(""),
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
handleSubmit: (e: any) => e.preventDefault(),
|
|
46
|
+
setFieldValue: () => {},
|
|
47
|
+
state: { values: { name: "", icon: "folder", description: "" } },
|
|
48
|
+
fields,
|
|
49
|
+
Field: ({ name, children }: { name: "name" | "icon" | "description"; children: unknown }) =>
|
|
50
|
+
typeof children === "function"
|
|
51
|
+
? (children as (field: unknown) => unknown)(fields[name])
|
|
52
|
+
: null,
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("CreateWorkspaceDialog", () => {
|
|
59
|
+
it("打开时渲染标题", () => {
|
|
60
|
+
render(<CreateWorkspaceDialog open={true} onOpenChange={() => {}} />);
|
|
61
|
+
expect(screen.getByRole("heading")).toBeInTheDocument();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { render } from "@testing-library/react";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { DeleteWorkspaceOptions } from "../../dialogs/delete-workspace-options";
|
|
7
|
+
|
|
8
|
+
vi.mock("@/components/ui/field", () => ({
|
|
9
|
+
Field: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
|
10
|
+
FieldContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
|
11
|
+
FieldDescription: ({ children }: { children: ReactNode }) => (
|
|
12
|
+
<p>{children}</p>
|
|
13
|
+
),
|
|
14
|
+
FieldLabel: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
|
15
|
+
FieldTitle: ({ children }: { children: ReactNode }) => <span>{children}</span>,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock("@/components/ui/radio-group", () => ({
|
|
19
|
+
RadioGroup: ({ children }: { children: ReactNode }) => (
|
|
20
|
+
<div data-radix-radio-group>{children}</div>
|
|
21
|
+
),
|
|
22
|
+
RadioGroupItem: () => <input type="radio" />,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock("@/components/ui/select", () => ({
|
|
26
|
+
Select: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
|
27
|
+
SelectContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
|
28
|
+
SelectItem: ({ children }: { children: ReactNode }) => <div>{children}</div>,
|
|
29
|
+
SelectTrigger: ({ children }: { children: ReactNode }) => (
|
|
30
|
+
<div data-radix-select-trigger>{children}</div>
|
|
31
|
+
),
|
|
32
|
+
SelectValue: () => <span />,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
vi.mock("@/components/ui/separator", () => ({
|
|
36
|
+
Separator: () => <hr />,
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
describe("DeleteWorkspaceOptions", () => {
|
|
40
|
+
const baseProps = {
|
|
41
|
+
projectCount: 2,
|
|
42
|
+
deleteMode: "transfer" as const,
|
|
43
|
+
onDeleteModeChange: () => {},
|
|
44
|
+
storyMode: "transfer" as const,
|
|
45
|
+
onStoryModeChange: () => {},
|
|
46
|
+
knowledgeBaseMode: "delete" as const,
|
|
47
|
+
onKnowledgeBaseModeChange: () => {},
|
|
48
|
+
targetWorkspaceId: "ws-2",
|
|
49
|
+
onTargetWorkspaceIdChange: () => {},
|
|
50
|
+
workspaceOptions: [
|
|
51
|
+
{ id: "ws-2", name: "其他工作区" },
|
|
52
|
+
{ id: "ws-3", name: "另一个工作区" },
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
it("转移模式下渲染两组处理选项并显示目标工作区下拉", () => {
|
|
57
|
+
const { container } = render(<DeleteWorkspaceOptions {...baseProps} />);
|
|
58
|
+
|
|
59
|
+
expect(
|
|
60
|
+
container.querySelectorAll("[data-radix-radio-group]"),
|
|
61
|
+
).toHaveLength(2);
|
|
62
|
+
expect(container.querySelector("[data-radix-select-trigger]")).not.toBeNull();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("清空模式下渲染三组处理选项且无目标工作区下拉", () => {
|
|
66
|
+
const { container } = render(
|
|
67
|
+
<DeleteWorkspaceOptions
|
|
68
|
+
{...baseProps}
|
|
69
|
+
deleteMode="clear"
|
|
70
|
+
storyMode="clear"
|
|
71
|
+
/>,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
expect(
|
|
75
|
+
container.querySelectorAll("[data-radix-radio-group]"),
|
|
76
|
+
).toHaveLength(3);
|
|
77
|
+
expect(container.querySelector("[data-radix-select-trigger]")).toBeNull();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("无项目时仅渲染知识库处理选项", () => {
|
|
81
|
+
const { container } = render(
|
|
82
|
+
<DeleteWorkspaceOptions {...baseProps} projectCount={0} />,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
expect(
|
|
86
|
+
container.querySelectorAll("[data-radix-radio-group]"),
|
|
87
|
+
).toHaveLength(1);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { EditWorkspaceDialog } from "../../dialogs/edit-workspace-dialog";
|
|
4
|
+
|
|
5
|
+
vi.mock("@/components/ui/button", () => ({
|
|
6
|
+
Button: ({ children, ...props }: any) => <button {...props}>{children}</button>,
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
vi.mock("@/components/ui/input", () => ({
|
|
10
|
+
Input: (props: any) => <input {...props} />,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock("@/components/ui/dialog", () => ({
|
|
14
|
+
Dialog: ({ children }: any) => <div>{children}</div>,
|
|
15
|
+
DialogContent: ({ children }: any) => <div>{children}</div>,
|
|
16
|
+
DialogFooter: ({ children }: any) => <div>{children}</div>,
|
|
17
|
+
DialogHeader: ({ children }: any) => <div>{children}</div>,
|
|
18
|
+
DialogTitle: ({ children }: any) => <h2>{children}</h2>,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock("@/components/shared-kit/icons/lucide-icon-picker", () => ({
|
|
22
|
+
LucideIconPicker: () => <div data-testid="icon-picker" />,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock("@tanstack/react-router", () => ({
|
|
26
|
+
useRouter: () => ({ navigate: vi.fn() }),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
vi.mock("@tanstack/react-form", () => {
|
|
30
|
+
const makeField = (value: string) => ({
|
|
31
|
+
value,
|
|
32
|
+
handleChange: vi.fn(),
|
|
33
|
+
handleBlur: vi.fn(),
|
|
34
|
+
meta: { errors: [] },
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
useForm: () => {
|
|
38
|
+
const fields = {
|
|
39
|
+
name: makeField("测试"),
|
|
40
|
+
icon: makeField("folder"),
|
|
41
|
+
description: makeField(""),
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
handleSubmit: (e: any) => e.preventDefault(),
|
|
45
|
+
setFieldValue: () => {},
|
|
46
|
+
state: { values: { name: "测试", icon: "folder", description: "" } },
|
|
47
|
+
fields,
|
|
48
|
+
Field: ({ name, children }: { name: "name" | "icon" | "description"; children: unknown }) =>
|
|
49
|
+
typeof children === "function"
|
|
50
|
+
? (children as (field: unknown) => unknown)(fields[name])
|
|
51
|
+
: null,
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("EditWorkspaceDialog", () => {
|
|
58
|
+
it("打开时渲染标题", () => {
|
|
59
|
+
render(
|
|
60
|
+
<EditWorkspaceDialog
|
|
61
|
+
open={true}
|
|
62
|
+
onOpenChange={() => {}}
|
|
63
|
+
workspace={{ id: "ws1", name: "测试", icon: "folder", description: null }}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
expect(screen.getByRole("heading")).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { WorkspaceHeader } from "../../header/workspace-header";
|
|
4
|
+
|
|
5
|
+
vi.mock("@/components/ui/button", () => ({
|
|
6
|
+
Button: ({ children, ...props }: any) => <button {...props}>{children}</button>,
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
vi.mock("@/components/ui/dropdown-menu", () => ({
|
|
10
|
+
DropdownMenu: ({ children }: any) => <div>{children}</div>,
|
|
11
|
+
DropdownMenuTrigger: ({ children }: any) => <div>{children}</div>,
|
|
12
|
+
DropdownMenuContent: ({ children }: any) => <div>{children}</div>,
|
|
13
|
+
DropdownMenuItem: ({ children }: any) => <div>{children}</div>,
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
vi.mock("@/components/ui/separator", () => ({
|
|
17
|
+
Separator: () => <div data-testid="separator" />,
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock("@/components/shared-kit/icons/lucide-icon-picker/lucide-icon-by-name", () => ({
|
|
21
|
+
LucideIconByName: () => <div data-testid="icon" />,
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
vi.mock("@tanstack/react-router", () => ({
|
|
25
|
+
Link: ({ children }: any) => <a>{children}</a>,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
describe("WorkspaceHeader", () => {
|
|
29
|
+
it("渲染工作空间名称", () => {
|
|
30
|
+
const WORKSPACE = {
|
|
31
|
+
id: "ws1",
|
|
32
|
+
name: "测试工作空间",
|
|
33
|
+
description: null,
|
|
34
|
+
icon: "folder",
|
|
35
|
+
_count: { projects: 0 },
|
|
36
|
+
};
|
|
37
|
+
render(<WorkspaceHeader workspace={WORKSPACE} onEdit={() => {}} onDelete={() => {}} />);
|
|
38
|
+
expect(screen.getByText("测试工作空间")).toBeInTheDocument();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
|
3
|
+
|
|
4
|
+
const mocks = vi.hoisted(() => ({
|
|
5
|
+
invalidate: vi.fn(),
|
|
6
|
+
setKbs: vi.fn(),
|
|
7
|
+
renameDoc: vi.fn(),
|
|
8
|
+
removeDoc: vi.fn(),
|
|
9
|
+
reorderDocs: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock("@tanstack/react-router", () => ({
|
|
13
|
+
useRouter: () => ({ invalidate: mocks.invalidate }),
|
|
14
|
+
Link: ({ to, children, ...rest }: any) => (
|
|
15
|
+
<a href={typeof to === "string" ? to : "#"} {...rest}>{children}</a>
|
|
16
|
+
),
|
|
17
|
+
}));
|
|
18
|
+
vi.mock("@tanstack/react-start", () => ({
|
|
19
|
+
useServerFn: (fn: any) => async (...args: any[]) => fn(...args),
|
|
20
|
+
}));
|
|
21
|
+
vi.mock("@/server/serverFns/workspace-kb", () => ({
|
|
22
|
+
getWorkspaceKbCardFn: vi.fn().mockResolvedValue({ kbs: [], quickDocs: [] }),
|
|
23
|
+
listWorkspaceAvailableKbsFn: vi.fn(),
|
|
24
|
+
setWorkspaceKbsFn: mocks.setKbs,
|
|
25
|
+
renameQuickDocFn: mocks.renameDoc,
|
|
26
|
+
removeQuickDocFn: mocks.removeDoc,
|
|
27
|
+
reorderQuickDocsFn: mocks.reorderDocs,
|
|
28
|
+
searchQuickDocCandidatesFn: vi.fn(),
|
|
29
|
+
addQuickDocsFn: vi.fn(),
|
|
30
|
+
}));
|
|
31
|
+
vi.mock("@dnd-kit/react", () => ({
|
|
32
|
+
DragDropProvider: ({ children }: any) => <div>{children}</div>,
|
|
33
|
+
useDroppable: () => ({ ref: null, isDropTarget: false }),
|
|
34
|
+
}));
|
|
35
|
+
vi.mock("@dnd-kit/react/sortable", () => ({
|
|
36
|
+
useSortable: () => ({ ref: undefined, handleRef: undefined, isDragging: false }),
|
|
37
|
+
}));
|
|
38
|
+
vi.mock("sonner", () => ({
|
|
39
|
+
toast: { error: vi.fn(), success: vi.fn(), warning: vi.fn() },
|
|
40
|
+
}));
|
|
41
|
+
vi.mock("../workspace-kb-manage-dialog", () => ({
|
|
42
|
+
WorkspaceKbManageDialog: () => null,
|
|
43
|
+
}));
|
|
44
|
+
vi.mock("../workspace-quick-doc-dialog", () => ({
|
|
45
|
+
WorkspaceQuickDocDialog: () => null,
|
|
46
|
+
}));
|
|
47
|
+
vi.mock("@/components/ui/tooltip", () => ({
|
|
48
|
+
Tooltip: ({ children }: any) => <span data-testid="tooltip-wrapper">{children}</span>,
|
|
49
|
+
TooltipContent: ({ children }: any) => <span>{children}</span>,
|
|
50
|
+
TooltipTrigger: ({ children }: any) => <span data-testid="tooltip-trigger">{children}</span>,
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
import { WorkspaceKbCard } from "../../kb/workspace-kb-card";
|
|
54
|
+
|
|
55
|
+
const KB = { id: "kb1", name: "团队库", icon: "notebook", visibility: "team" };
|
|
56
|
+
const DOC_ROW = {
|
|
57
|
+
id: "q1", displayTitle: "接入指南", columnIndex: 0, sortOrder: 0, label: null, icon: null, source: "doc" as const,
|
|
58
|
+
docId: "d1", kbId: "kb1", kbName: "团队库", docIcon: null,
|
|
59
|
+
group: null, externalId: null, faviconUrl: null, sourceTitle: "接入指南", url: null, stale: false,
|
|
60
|
+
};
|
|
61
|
+
const STALE_ROW = {
|
|
62
|
+
...DOC_ROW, id: "q2", displayTitle: "旧法条", source: "general" as const,
|
|
63
|
+
docId: null, kbId: null, kbName: null, group: "legal" as const, externalId: "gone",
|
|
64
|
+
faviconUrl: null, sourceTitle: null, url: null, stale: true,
|
|
65
|
+
};
|
|
66
|
+
const EXTERNAL_URL_ROW = {
|
|
67
|
+
...DOC_ROW, id: "q3", displayTitle: "外部工具", source: "general" as const,
|
|
68
|
+
docId: null, kbId: null, kbName: null, group: "bookmark" as const, externalId: "ext123",
|
|
69
|
+
faviconUrl: null, sourceTitle: "PNG to ICO", url: "https://tool.xuecan.net/png-to-ico/", stale: false,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
vi.clearAllMocks();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("WorkspaceKbCard 渲染", () => {
|
|
77
|
+
it("空数据显示空状态且「前往知识库」禁用", () => {
|
|
78
|
+
render(<WorkspaceKbCard workspaceId="ws1" data={{ kbs: [], quickDocs: [] }} />);
|
|
79
|
+
expect(screen.getByText(/快捷知识入口/)).toBeInTheDocument();
|
|
80
|
+
// 前往知识库图标按钮 disabled,无 aria-label
|
|
81
|
+
const buttons = screen.getAllByRole("button");
|
|
82
|
+
const disabledBtn = buttons.find((b) => b.getAttribute("disabled") !== null);
|
|
83
|
+
expect(disabledBtn).toBeTruthy();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("渲染快捷文档行(5列布局)", () => {
|
|
87
|
+
render(<WorkspaceKbCard workspaceId="ws1" data={{ kbs: [], quickDocs: [DOC_ROW] }} />);
|
|
88
|
+
expect(screen.getByText("接入指南")).toBeInTheDocument();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("失效条目显示「源已失效」", () => {
|
|
92
|
+
render(<WorkspaceKbCard workspaceId="ws1" data={{ kbs: [], quickDocs: [STALE_ROW] }} />);
|
|
93
|
+
expect(screen.getByText(/源已失效/)).toBeInTheDocument();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("已关联知识库时不显示知识库名称(chip 已移除),前往按钮 tooltip 显示库名", () => {
|
|
97
|
+
render(<WorkspaceKbCard workspaceId="ws1" data={{ kbs: [KB], quickDocs: [] }} />);
|
|
98
|
+
// chip 行已移除,但 goToKb tooltip 仍显示库名
|
|
99
|
+
expect(screen.getByText("团队库")).toBeInTheDocument();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("WorkspaceKbCard 乐观更新", () => {
|
|
104
|
+
it("删除确认后乐观移除并调 removeQuickDocFn", async () => {
|
|
105
|
+
render(<WorkspaceKbCard workspaceId="ws1" data={{ kbs: [], quickDocs: [DOC_ROW] }} />);
|
|
106
|
+
fireEvent.click(screen.getByRole("button", { name: "删除" }));
|
|
107
|
+
fireEvent.click(screen.getByRole("button", { name: "确认" }));
|
|
108
|
+
await waitFor(() => {
|
|
109
|
+
expect(mocks.removeDoc).toHaveBeenCalledWith({ data: { quickDocId: "q1" } });
|
|
110
|
+
});
|
|
111
|
+
expect(screen.queryByText("接入指南")).not.toBeInTheDocument();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("重命名提交调用 renameQuickDocFn 且 UI 更新", async () => {
|
|
115
|
+
render(<WorkspaceKbCard workspaceId="ws1" data={{ kbs: [], quickDocs: [DOC_ROW] }} />);
|
|
116
|
+
fireEvent.click(screen.getByRole("button", { name: "重命名" }));
|
|
117
|
+
const input = screen.getByRole("textbox");
|
|
118
|
+
fireEvent.change(input, { target: { value: "新标题" } });
|
|
119
|
+
fireEvent.keyDown(input, { key: "Enter" });
|
|
120
|
+
await waitFor(() => {
|
|
121
|
+
expect(mocks.renameDoc).toHaveBeenCalledWith({
|
|
122
|
+
data: { quickDocId: "q1", displayTitle: "新标题" },
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
expect(screen.getByText("新标题")).toBeInTheDocument();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("外部 URL 卡片使用 <a href> 而非 Link", () => {
|
|
129
|
+
render(<WorkspaceKbCard workspaceId="ws1" data={{ kbs: [], quickDocs: [EXTERNAL_URL_ROW] }} />);
|
|
130
|
+
// 外部 URL 应渲染为普通 <a> 标签,而非 TanStack Router Link(mock 为 <a>)
|
|
131
|
+
const link = screen.getByText("外部工具").closest("a");
|
|
132
|
+
expect(link).toBeInTheDocument();
|
|
133
|
+
expect((link as HTMLAnchorElement).href).toBe("https://tool.xuecan.net/png-to-ico/");
|
|
134
|
+
expect((link as HTMLAnchorElement).target).toBe("_blank");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("编辑状态下点击行不触发导航", async () => {
|
|
138
|
+
render(<WorkspaceKbCard workspaceId="ws1" data={{ kbs: [], quickDocs: [DOC_ROW] }} />);
|
|
139
|
+
// 进入编辑模式
|
|
140
|
+
fireEvent.click(screen.getByRole("button", { name: "重命名" }));
|
|
141
|
+
// 编辑态:快捷文档行的主链接不应存在(渲染为 span,无导航能力)
|
|
142
|
+
const row = document.querySelector("[data-quick-doc-row]");
|
|
143
|
+
expect(row).toBeInTheDocument();
|
|
144
|
+
// 主链接区域(包含 title 的 <a>)应不存在
|
|
145
|
+
const mainLink = row!.querySelector<HTMLAnchorElement>('.flex-1.truncate > a');
|
|
146
|
+
expect(mainLink).not.toBeInTheDocument();
|
|
147
|
+
// Input 应存在且可编辑
|
|
148
|
+
expect(screen.getByRole("textbox")).toBeInTheDocument();
|
|
149
|
+
// 点击行区域不应有导航(因为没有 Link)
|
|
150
|
+
fireEvent.click(row!);
|
|
151
|
+
// 编辑状态应保持(点击行内不会意外退出)
|
|
152
|
+
expect(screen.getByRole("textbox")).toBeInTheDocument();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { WorkspaceMemberItem } from "../../members/workspace-member-item";
|
|
4
|
+
|
|
5
|
+
vi.mock("@/components/shared-kit", () => ({
|
|
6
|
+
UserAvatar: ({ name }: { name?: string }) => <div data-testid="user-avatar">{name?.[0] ?? "?"}</div>,
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
const MEMBER = {
|
|
10
|
+
id: "m1",
|
|
11
|
+
userId: "u1",
|
|
12
|
+
role: "member" as const,
|
|
13
|
+
status: "active" as const,
|
|
14
|
+
joinedAt: new Date(),
|
|
15
|
+
user: { id: "u1", username: "张三", email: "zhangsan@example.com" },
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
describe("WorkspaceMemberItem", () => {
|
|
19
|
+
it("渲染成员用户名和邮箱", () => {
|
|
20
|
+
render(<WorkspaceMemberItem member={MEMBER} workspaceOwnerId="owner1" onRemove={() => {}} />);
|
|
21
|
+
expect(screen.getByText("张三")).toBeInTheDocument();
|
|
22
|
+
expect(screen.getByText("zhangsan@example.com")).toBeInTheDocument();
|
|
23
|
+
});
|
|
24
|
+
});
|