Files
ttstd-infinite-canvas/web/test/admin-ui-regressions.test.ts
ddCat 9ce158aaa0 refactor(admin): 管理后台 UI - 独立设计系统并与用户端隔离
把后台 token、Ant 主题、壳层控件和抽屉弹窗从用户工作台拆开,去掉树状侧栏装饰线,色板仍用原来的中性黑白台阶。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-17 21:29:35 +08:00

335 lines
22 KiB
TypeScript

import { expect, test } from "bun:test";
function compactSource(source: string) {
return source.replace(/\s+/g, " ").trim();
}
function sourceSection(source: string, startMarker: string, endMarker: string) {
const start = source.indexOf(startMarker);
const end = source.indexOf(endMarker, start + startMarker.length);
expect(start).toBeGreaterThanOrEqual(0);
expect(end).toBeGreaterThan(start);
return source.slice(start, end);
}
test("announcement editor preserves image and pinned fields through edit and save", async () => {
const [panelSource, safetySource] = await Promise.all([
Bun.file(new URL("../src/pages/admin/components/admin-announcements-panel.tsx", import.meta.url)).text(),
Bun.file(new URL("../src/pages/admin/components/admin-announcement-safety.ts", import.meta.url)).text(),
]);
const panel = compactSource(panelSource);
expect(panel).toContain("uploadAdminAnnouncementImage");
expect(panel).toContain("discardAdminAnnouncementImage");
expect(panel).toContain('imageResourceId: announcement.imageResourceId || ""');
expect(panel).toContain("pinned: announcement.pinned");
expect(panel).toContain('imageResourceId: values.imageResourceId?.trim() || ""');
expect(panel).toContain("pinned: Boolean(values.pinned)");
expect(panel).toContain('(announcement?.imageResourceId || "") === (expectedContent.imageResourceId || "")');
expect(panel).toContain('rootClassName="admin-modal-root admin-announcement-editor-modal"');
expect(panel).toContain("centered");
expect(panel).not.toContain("<Drawer");
expect(safetySource).toContain("imageResourceId?: string");
expect(safetySource).toContain("pinned?: boolean");
});
test("plugin upload owns native drops and price availability text remains readable", async () => {
const [pluginSource, adminCss] = await Promise.all([Bun.file(new URL("../src/pages/plugins/plugin-documentation-modals.tsx", import.meta.url)).text(), Bun.file(new URL("../src/styles/admin-ui.css", import.meta.url)).text()]);
const toggleCss = sourceSection(adminCss, ".admin-price-tier-toggle span {", ".admin-model-editor-add-tier.ant-btn {");
expect(pluginSource).toContain("event.preventDefault()");
expect(pluginSource).toContain("onDragOver={(event)");
expect(pluginSource).toContain("onDrop={handlePluginDrop}");
expect(pluginSource).toContain("点击选择插件文件,也可拖拽到此处");
expect(pluginSource).toContain("释放文件以上传插件");
expect(pluginSource).toContain("isDraggingPlugin");
expect(compactSource(adminCss)).toContain(".admin-price-tier-controls { margin-left: auto;");
expect(toggleCss).toContain("overflow-wrap: anywhere;");
expect(toggleCss).toContain("white-space: normal;");
expect(toggleCss).not.toContain("text-overflow: ellipsis;");
});
test("model reference limits use compact rows only inside the admin editor", async () => {
const css = await Bun.file(new URL("../src/styles/admin-ui.css", import.meta.url)).text();
const numberField = sourceSection(css, ".admin-model-editor-references .admin-capability-number-field {", ".admin-model-editor-references .admin-capability-boolean-field {");
expect(numberField).toContain("grid-template-columns: minmax(0, 1fr) 80px;");
expect(numberField).toContain("min-height: 32px;");
expect(numberField).toContain("align-items: center;");
const switches = sourceSection(css, ".admin-model-editor-references .admin-capability-boolean-field label {", "@media (min-width: 601px)");
expect(switches).toContain("display: flex;");
expect(compactSource(css)).toContain(".admin-model-editor-references .admin-capability-reference-grid { align-items: start;");
expect(compactSource(css)).toContain(".admin-model-editor-modal .admin-capability-reference-grid { grid-template-columns: minmax(0, 1fr);");
});
test("model editor presents protocols in a searchable inline radio browser instead of a dropdown", async () => {
const [source, css] = await Promise.all([Bun.file(new URL("../src/pages/admin/components/channel-model-editor.tsx", import.meta.url)).text(), Bun.file(new URL("../src/styles/admin-ui.css", import.meta.url)).text()]);
const protocolSection = sourceSection(compactSource(source), '<Form.Item className="admin-model-protocol-field"', "{protocolError && (");
expect(protocolSection).toContain("<ModelProtocolBrowser");
expect(protocolSection).not.toContain("<Select");
expect(compactSource(css)).toContain(".admin-model-protocol-field { grid-column: 1 / -1;");
});
test("channel model fetch requires explicit selection before import", async () => {
const [componentSource, apiSource, adminCssSource] = await Promise.all([
Bun.file(new URL("../src/pages/admin/components/channel-model-manager.tsx", import.meta.url)).text(),
Bun.file(new URL("../src/services/api/wallet.ts", import.meta.url)).text(),
Bun.file(new URL("../src/styles/admin-ui.css", import.meta.url)).text(),
]);
const component = compactSource(componentSource);
expect(apiSource).toContain("http.post<{ models: string[] }>(`/admin/channels/${encodeURIComponent(channelId)}/models/fetch`)");
expect(apiSource).toContain("http.post<{ models: string[]; added: number }>(`/admin/channels/${encodeURIComponent(channelId)}/models/import`, { models })");
expect(component).toContain('title="选择要导入的模型"');
expect(component).toContain("默认已全选");
expect(component).toContain("setFetchPreviewOpen(true)");
expect(component).toContain("setSelectedFetchModels(result.models)");
expect(component).toContain("已选择 {selectedFetchModels.length} / {fetchPreviewModels.length} 个模型");
expect(component).toContain("disabled={importing || allFetchModelsSelected}");
expect(component).toContain("onClick={() => setSelectedFetchModels(fetchPreviewModels)}");
expect(component).toContain("disabled={importing || selectedFetchModels.length === 0}");
expect(component).toContain("onClick={() => setSelectedFetchModels([])}");
expect(component).toContain("取消全选");
expect(component).toContain("disabled={importing}");
expect(component).toContain("importAdminChannelModels(channel.id, selectedFetchModels)");
expect(component).toContain("disabled={!selectedFetchModels.length}");
expect(component).not.toContain("disabled: alreadyExists");
expect(component).not.toContain("const result = await fetchAdminChannelModels(channel.id); await reload();");
expect(adminCssSource).toContain(".admin-model-import-modal .channel-model-import-picker .ant-checkbox-checked");
expect(adminCssSource).toContain("border-color: var(--control-check-fg) !important");
});
test("channel model manager supports bounded atomic batch deletion", async () => {
const [componentSource, apiSource] = await Promise.all([Bun.file(new URL("../src/pages/admin/components/channel-model-manager.tsx", import.meta.url)).text(), Bun.file(new URL("../src/services/api/wallet.ts", import.meta.url)).text()]);
const component = compactSource(componentSource);
expect(apiSource).toContain("http.post<{ deleted: number }>(`/admin/channels/${encodeURIComponent(channelId)}/models/batch-delete`, { modelIds })");
expect(component).toContain("<AdminBatchBar count={selectedModelIds.length}");
expect(component).toContain("rowSelection:");
expect(component).toContain("selectedRowKeys: selectedModelIds");
expect(component).toContain("preserveSelectedRowKeys: true");
expect(component).toContain("setSelectedModelIds(next.slice(0, 100))");
expect(component).toContain("deleteAdminChannelModels(channel.id, selectedModelIds)");
expect(component).toContain("okButtonProps: { danger: true }");
expect(component).toContain("本次就不会删除任何模型");
expect(component).toContain("批量删除");
});
test("analytics keeps fixed range presets distinct and uses enabled channel models for pricing", async () => {
const source = compactSource(await Bun.file(new URL("../src/pages/admin/components/analytics-panel.tsx", import.meta.url)).text());
expect(source).toContain('type RangePreset = "7d" | "30d" | "60d"');
expect(source).toContain('["60d", "60 天"]');
expect(source).toContain('next.set("rangePreset", rangePreset)');
expect(source).toContain("setRangePreset(undefined)");
expect(source).toContain('placeholder={pricingModelOptions.length ? "选择已启用模型" : "暂无已启用模型"}');
expect(source).toContain("onValuesChange={handlePricingValuesChange}");
expect(source).toContain("onChange={handlePricingModelChange}");
expect(source).toContain('hasOwnProperty.call(changedValues, "model")');
expect(source).toContain('if (matchingChannels.length) form.setFieldValue("channelId", matchingChannels[0].id)');
expect(source).toContain("const sourceChannels = channels.filter(");
expect(source).toContain('Form.useWatch("channelId", form)');
expect(source).toContain("pricingChannelId");
expect(source).toContain("channel.id === pricingChannelId");
expect(source).toContain('inputMode="decimal"');
expect(source).toContain('className="admin-analytics-price-input"');
expect(source).toContain('className="admin-analytics-price-field"');
expect(source).toContain('rootClassName="admin-modal-root admin-analytics-pricing-modal"');
expect(source).toContain("zIndex={1200}");
expect(source).toContain("setPricingWorkspaceOpen(false)");
expect(source).toContain("validator: validatePriceInput");
expect(source).toContain("请输入非负价格,最多 6 位小数");
expect(source).toContain("function formatPriceInput(micros: number)");
expect(source).toContain("function toMicros(value?: string | number)");
expect(source).not.toContain("<InputNumber");
});
test("storage settings keep generic S3 controls and connection validation", async () => {
const source = await Bun.file(new URL("../src/pages/admin/settings/storage-settings-page.tsx", import.meta.url)).text();
const compacted = compactSource(source);
expect(compacted).toContain('{ mode: "s3", label: "S3 兼容存储"');
expect(compacted).toContain("testAdminOSSConnection(connectionInput(values))");
for (const field of ["s3Preset", "sessionToken", "pathStyle", "allowUserS3"]) {
expect(compacted).toContain(`name="${field}"`);
}
expect(compacted).toContain('["aliyun", "tencent", "qiniu", "s3"].includes(setting.provider || "")');
});
test("admin navigation keeps the storage resource page reachable", async () => {
const source = await Bun.file(new URL("../src/pages/admin/components/admin-shell.tsx", import.meta.url)).text();
expect(source).toContain('path: "/admin/resources"');
});
test("nested admin pages return to their own parent entry", async () => {
const source = await Bun.file(new URL("../src/pages/admin/components/admin-shell.tsx", import.meta.url)).text();
const compacted = compactSource(source);
expect(compacted).toContain("const currentItem = currentSection?.items.find");
expect(compacted).toContain("const sectionPath = back ? (currentItem?.path");
});
test("high impact feature shutdowns require confirmation", async () => {
const source = await Bun.file(new URL("../src/pages/admin/components/feature-availability-panel.tsx", import.meta.url)).text();
expect(source).toContain('title: "关闭用户积分功能?"');
expect(source).toContain('title: "关闭前台模型功能?"');
expect(source).toContain("onChange={requestFeatureChange}");
});
test("admin settings use full-width summaries without selected-card side stripes", async () => {
const [componentSource, cssSource] = await Promise.all([Bun.file(new URL("../src/pages/admin/components/admin-ui.tsx", import.meta.url)).text(), Bun.file(new URL("../src/styles/admin-ui.css", import.meta.url)).text()]);
expect(componentSource).not.toContain("lg:grid lg:grid-cols-4");
expect(cssSource).not.toContain('content: "配置摘要"');
expect(cssSource).not.toContain("grid-template-columns: minmax(0, 1fr) 344px");
expect(cssSource).not.toContain(".admin-storage-mode-choice::before");
const featureSelected = sourceSection(cssSource, ".admin-feature-board-row.is-selected {", ".admin-feature-board-row.is-dirty {");
const drawingSelected = sourceSection(cssSource, ".admin-drawing-engine-choice.is-selected {", ".admin-drawing-engine-choice.is-unavailable");
expect(featureSelected).not.toContain("inset 3px 0 0");
expect(drawingSelected).not.toContain("inset 3px 0 0");
});
test("task-first settings reveal dependent configuration only after the primary choice", async () => {
const [storageSource, emailSource, accessSource, featureSource, appearanceSource, welcomeSource, drawingSource, arkSource, interceptionSource, thirdPartySource, cssSource] = await Promise.all([
Bun.file(new URL("../src/pages/admin/settings/storage-settings-page.tsx", import.meta.url)).text(),
Bun.file(new URL("../src/pages/admin/components/email-settings-panel.tsx", import.meta.url)).text(),
Bun.file(new URL("../src/pages/admin/components/access-settings-panel.tsx", import.meta.url)).text(),
Bun.file(new URL("../src/pages/admin/components/feature-availability-panel.tsx", import.meta.url)).text(),
Bun.file(new URL("../src/pages/admin/settings/appearance-settings-page.tsx", import.meta.url)).text(),
Bun.file(new URL("../src/pages/admin/settings/components/welcome-setting.tsx", import.meta.url)).text(),
Bun.file(new URL("../src/pages/admin/settings/drawing-engine-settings-page.tsx", import.meta.url)).text(),
Bun.file(new URL("../src/pages/admin/settings/ark-private-assets-settings-page.tsx", import.meta.url)).text(),
Bun.file(new URL("../src/pages/admin/settings/response-interception-settings-page.tsx", import.meta.url)).text(),
Bun.file(new URL("../src/pages/admin/settings/libtv-settings-page.tsx", import.meta.url)).text(),
Bun.file(new URL("../src/styles/admin-ui.css", import.meta.url)).text(),
]);
expect(storageSource).toContain('title="1. 选择新资源存储位置"');
expect(storageSource).toContain("选择后继续完成第 2 步并保存");
expect(sourceSection(storageSource, "const requestModeChange", "const save")).not.toContain("save(values)");
expect(emailSource).toContain("{draftEnabled ? (");
expect(emailSource).toContain('id="admin-email-smtp"');
expect(emailSource).toContain('title="1. 是否发送账户安全邮件"');
expect(emailSource).toContain('title="2. 配置 SMTP 连接与发件身份"');
expect(accessSource).toContain("{draftLinuxDOEnabled ? (");
expect(accessSource).toContain('title="1. 是否允许创建新账号"');
expect(accessSource).toContain('title="2. 是否开放 Linux.do 登录"');
expect(featureSource).toContain('title="1. 用户工作台入口"');
expect(featureSource).toContain('title="2. 插件开放范围"');
expect(featureSource).toContain('title="3. 用户模型来源"');
expect(appearanceSource).toContain("<WelcomeSetting />");
expect(welcomeSource).toContain("<strong>启用欢迎页</strong>");
expect(welcomeSource).toContain("updateAdminFeatureAvailability({ welcomeEnabled: value })");
expect(drawingSource).toContain('title="1. 选择新建绘图默认编辑器"');
expect(drawingSource).toContain('title="2. 配置 tldraw 授权(按需)"');
expect(sourceSection(drawingSource, "const selectEngine", "async function save")).not.toContain("save(");
expect(arkSource).toContain('title="1. 配置方舟项目与 IAM 凭据"');
expect(arkSource).toContain('title="2. 是否启用可信素材同步"');
expect(arkSource).toContain("{prerequisitesReady || draftEnabled ? (");
expect(arkSource).toContain('aria-label="启用可信素材同步,保存修改后生效"');
expect(interceptionSource).toContain('title="1. 是否替换用户可见的上游错误"');
expect(interceptionSource).toContain('title="2. 配置替换规则与优先级"');
expect(interceptionSource).toContain('title="3. 本地预览用户最终文案"');
expect(interceptionSource).toContain("{enabled ? (");
expect(interceptionSource).not.toContain('className="admin-intercept-overview"');
expect(sourceSection(interceptionSource, "const changeEnabled", "if (loading")).not.toContain("save(");
expect(thirdPartySource).toContain('title="1. 配置 LibTV 服务端访问凭据"');
expect(thirdPartySource).toContain('title="2. 是否开放用户导入 LibTV 画布"');
expect(thirdPartySource).toContain('title="3. 验证已保存的 LibTV 凭据"');
expect(thirdPartySource).toContain("{draftHasToken ? (");
expect(thirdPartySource).toContain("{setting.hasToken && !clearTokenDraft ? (");
expect(thirdPartySource).not.toContain('className="admin-third-party-overview"');
expect(sourceSection(thirdPartySource, "const changeEnabled", "const markTokenForRemoval")).not.toContain("save(");
expect(compactSource(cssSource)).toContain(".admin-feature-board { width: 100%; max-width: none; grid-template-columns: minmax(0, 1fr);");
});
test("admin tables keep requested filters and actions in the intended positions", async () => {
const [storageSource, creditSource] = await Promise.all([
Bun.file(new URL("../src/pages/admin/components/storage-resources-panel.tsx", import.meta.url)).text(),
Bun.file(new URL("../src/pages/admin/components/credit-operations-panel.tsx", import.meta.url)).text(),
]);
const storageToolbar = sourceSection(storageSource, "toolbar={", "toolbarActiveFilters=");
expect(storageToolbar).toContain('className="admin-storage-resource-filters"');
expect(storageToolbar).toContain('placeholder="资源 ID 或对象路径"');
expect(storageToolbar).toContain('placeholder="用户"');
expect(storageToolbar).toContain('aria-label="筛选资源类型"');
expect(storageToolbar).toContain('aria-label="筛选资源状态"');
expect(storageToolbar).toContain('aria-label="筛选存储类型"');
const operationColumn = sourceSection(creditSource, 'title: "操作"', "const hasFilters");
expect(operationColumn).toContain('fixed: "right"');
});
test("request logs display user credit billing independently from upstream cost", async () => {
const [listSource, detailSource, apiSource] = await Promise.all([
Bun.file(new URL("../src/pages/admin/logs/logs-page.tsx", import.meta.url)).text(),
Bun.file(new URL("../src/pages/admin/components/api-log-detail-drawer.tsx", import.meta.url)).text(),
Bun.file(new URL("../src/services/api/auth.ts", import.meta.url)).text(),
]);
const billingSummary = sourceSection(listSource, "function BillingSummary", "function MediaResult");
expect(listSource).toContain('title: "积分计费"');
expect(listSource).toContain('title: "请求阶段 / 状态"');
expect(listSource).toContain('description="模型生成与结果下载记录;仅计费调用扣除积分"');
expect(billingSummary).toContain("billingAmountMicrocredits");
expect(billingSummary).toContain("billingAvailable");
expect(billingSummary).toContain("!log.billable");
expect(billingSummary).toContain("不计费");
expect(billingSummary).not.toContain("costAvailable");
expect(detailSource).toContain('["请求阶段", requestKindText(log.requestKind)]');
expect(detailSource).toContain('["计费属性", log.billable ? "计费调用" : "不计费"]');
expect(detailSource).toContain('["积分计费", billingText(log)]');
expect(detailSource).toContain('["上游成本", log.costAvailable');
expect(apiSource).toContain("billingAmountMicrocredits: number");
expect(apiSource).toContain("billingAvailable: boolean");
});
test("admin console tokens and shell stay isolated from the user workspace", async () => {
const [tokens, shell, chrome, globals] = await Promise.all([
Bun.file(new URL("../src/pages/admin/theme/admin-tokens.css", import.meta.url)).text(),
Bun.file(new URL("../src/pages/admin/components/admin-shell.tsx", import.meta.url)).text(),
Bun.file(new URL("../src/pages/admin/theme/admin-chrome.css", import.meta.url)).text(),
Bun.file(new URL("../src/styles/globals.css", import.meta.url)).text(),
]);
expect(tokens).toContain("--admin-canvas: #f5f5f5;");
expect(tokens).toContain("--admin-canvas: #0f0f0f;");
expect(tokens).not.toContain("--admin-layer-0: var(--workspace-");
expect(tokens).not.toContain("--admin-layer-0: var(--skin-admin-");
expect(shell).toContain("data-admin-root");
expect(shell).toContain("getIsolatedAdminAntTheme");
expect(shell).not.toContain("WorkspacePage");
expect(shell).not.toContain("getAdminAntThemeConfig");
expect(shell).not.toContain("app-workspace-nav-link");
expect(chrome).toContain("[data-admin-root] .admin-nav-link");
expect(chrome).toContain("border-left: 0 !important");
expect(chrome).not.toContain("left: -8px");
expect(chrome).toContain(".admin-drawer .ant-drawer-content");
expect(globals).not.toContain("/* 管理端专用视觉收口:不覆盖创作端 workspace 的导航、状态和图表样式。 */");
const [overlays, userDetail, prompts, payments, modelEditor] = await Promise.all([
Bun.file(new URL("../src/pages/admin/ui/overlays.tsx", import.meta.url)).text(),
Bun.file(new URL("../src/pages/admin/components/admin-user-detail-drawer.tsx", import.meta.url)).text(),
Bun.file(new URL("../src/pages/admin/storyboard-prompts/storyboard-prompts-page.tsx", import.meta.url)).text(),
Bun.file(new URL("../src/pages/admin/payments/payments-page.tsx", import.meta.url)).text(),
Bun.file(new URL("../src/pages/admin/components/channel-model-editor.tsx", import.meta.url)).text(),
]);
expect(overlays).toContain('rootClassName={cn("admin-drawer"');
expect(overlays).toContain('rootClassName={cn("admin-modal-root"');
expect(overlays).not.toContain("@/components/ui/product");
for (const source of [userDetail, prompts, payments, modelEditor]) {
expect(source).not.toContain("@/components/ui/product");
expect(source).not.toContain("AppDrawer");
expect(source).not.toContain("AppModal");
}
});