Files
ttstd-infinite-canvas/web/test/admin-ui-regressions.test.ts
Rou 054dab28cc fix(workbench): 模型目录与助手形象 - 修正 Live2D 首帧闪现和模型默认分组 (#773)
* fix(workbench): 模型目录与助手形象 - 修正 Live2D 首帧闪现和模型默认分组

进入画布时形象配置与 Live2D 首帧未就绪不再显示默认球,改为加载提示,失败才回退。

新导入模型一级目录默认未分类、二级渠道展示名默认跟随上游 ID;历史非空分组(含与模型 ID 同名)保留。

拉取模型弹窗默认只勾选已存在模型并移除逐项已存在字样;一级分组下拉点击即展开全部选项。

* style(web): 画布与模型导入 - 按 Prettier 规范格式化改动文件

修复 PR 中 Web checks 的格式化检查失败:Live2D 启动器 JSX、模型导入选择工具函数与 Live2D 加载测试按项目 Prettier 配置重新排版,不改变行为。

* test(web): Live2D 生命周期 - 修复 bun 1.3.9 下 text 导入与模块导入冲突

agent-live2d-lifecycle 曾用 with { type: text } 导入 live2d-avatar.tsx,bun 1.3.9 会按路径复用该缓存,导致新增的模块导入拿不到 Live2DAvatar 具名导出并报 SyntaxError。改为 readFileSync 读取源码文本,本地以 bun 1.3.9 复现并验证通过。
2026-10-14 09:36:01 +08:00

398 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { expect, test } from "bun:test";
import { moduleGroupSource } from "./helpers/module-group-source";
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([
Promise.resolve(moduleGroupSource("pages/admin/components/admin-announcements-panel.tsx")),
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 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); 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(selectExistingImportModels(result.models,");
expect(component).toContain("已选择 {selectedFetchModels.length} / {fetchPreviewModels.length} 个模型");
expect(component).toContain("disabled={importing || !visibleFetchModels.length || allVisibleFetchModelsSelected}");
expect(component).toContain("onClick={() => setSelectedFetchModels((current) => selectVisibleModels(current, visibleFetchModels))}");
expect(component).toContain("disabled={importing || selectedVisibleFetchCount === 0}");
expect(component).toContain("onClick={() => setSelectedFetchModels((current) => deselectVisibleModels(current, visibleFetchModels))}");
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 range presets and uses order finances without a separate pricing editor", 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).not.toContain("模型价格配置");
expect(source).not.toContain("listAdminModelPricings");
expect(source).toContain("finance.revenueMicrocredits");
expect(source).toContain("finance.profitMicrocredits");
expect(source).toContain("finance.costedOrders < finance.settledOrders");
expect(source).toContain("...analyticsFinanceColumns");
expect(source).toContain("后端未返回完整财务统计");
});
test("storage settings keep generic S3 controls and connection validation", async () => {
const source = await Promise.resolve(moduleGroupSource("pages/admin/settings/storage-settings-page.tsx"));
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([
Promise.resolve(moduleGroupSource("pages/admin/settings/storage-settings-page.tsx")),
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(),
Promise.resolve(moduleGroupSource("pages/admin/settings/appearance-settings-page.tsx")),
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="用户名 / 昵称 / 邮箱 / 用户 ID"');
expect(storageSource).toContain("user: debouncedUserQuery || undefined");
expect(storageSource).toContain("userId: debouncedUserQuery || undefined");
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('label="请求阶段" value={requestKindText(log.requestKind)}');
expect(detailSource).toContain('label="计费" value={billingText(log)}');
expect(detailSource).toContain('label="请求 ID" value={<CopyValue value={log.id} />}');
expect(detailSource).toContain('label="供应商任务 ID" value={log.providerRequestId ? <CopyValue value={log.providerRequestId} /> : "未记录"}');
expect(apiSource).toContain("billingAmountMicrocredits: number");
expect(apiSource).toContain("billingAvailable: boolean");
});
test("banner announcement editor keeps title styles through edit, save and status toggle", async () => {
const [panelSource, editorSource, sliderSource, apiSource, contentSource, emojiPickerSource, noticeSource] = await Promise.all([
Bun.file(new URL("../src/pages/admin/components/admin-banner-announcements-panel.tsx", import.meta.url)).text(),
Bun.file(new URL("../src/pages/admin/components/banner-title-editor.tsx", import.meta.url)).text(),
Bun.file(new URL("../src/components/layout/banner-announcements-slider.tsx", import.meta.url)).text(),
Bun.file(new URL("../src/services/api/announcements.ts", import.meta.url)).text(),
Bun.file(new URL("../src/components/layout/banner-announcement-content.tsx", import.meta.url)).text(),
Bun.file(new URL("../src/pages/admin/components/banner-notice-emoji-picker.tsx", import.meta.url)).text(),
Bun.file(new URL("../src/lib/announcements/banner-notice.ts", import.meta.url)).text(),
]);
const panel = compactSource(panelSource);
const slider = compactSource(sliderSource);
// 状态开关与保存都必须带上样式分段与通知类型:后端更新走显式字段表,漏传会清空它们。
expect(panel).toContain("titleRuns: banner.titleRuns");
expect(panel).toContain("titleRuns: runs");
expect(panel).toContain("noticeType: selectedNoticeType");
expect(panel).toContain("noticeType: normalizeBannerNoticeType(banner.noticeType)");
// 编辑入口不能被新建信号重置:父级 createOpen 消费后立即复位,编辑路径不再写回该标志。
expect(panel).toContain("onCreateOpenChange(false)");
expect(panel).toContain('setDialog({ mode: "edit", banner })');
expect(panel).toContain("banner.titleRuns?.length");
// AntD 6 用 destroyOnHidden,destroyOnClose 已废弃。
expect(panel).toContain("destroyOnHidden");
expect(panel).not.toContain("destroyOnClose");
// Modal 渲染在 body portal 里,必须挂 admin-modal-root 才能拿到弹窗作用域的强边框 / 分层 token,
// 否则编辑器等自绘控件的边框回落到 :root 的 8% 透明度,肉眼不可见。
expect(panel).toContain('rootClassName="admin-modal-root"');
// 非表单控件不能放进 Form.Item(会被注入 value/onChange/ref)。
expect(panel).toContain("<BannerNoticePreview runs={titleRuns} hasLink={Boolean(linkValue?.trim())} noticeType={noticeType} />");
// 通知类型在后台要有独立入口;emoji 图标不设独立字段,经编辑器「图标」按钮插入。
expect(panel).toContain("<BannerNoticeTypeSelector");
expect(panel).not.toContain("BannerNoticeIconPicker");
expect(panel).not.toContain("normalizeBannerNoticeIcon");
expect(editorSource).toContain("applyBannerTitleStyle");
expect(editorSource).toContain("clearBannerTitleStyle");
expect(editorSource).toContain("lowContrastBannerTitleColors");
// 标题是单行语义,Enter 不产生新段落。
expect(editorSource).toContain('event.key === "Enter"');
// emoji 素材插到光标处、作为普通文本保存(无独立字段、无默认图标)。
expect(editorSource).toContain("<BannerNoticeEmojiPopover");
expect(editorSource).toContain("insertContent(emoji)");
expect(editorSource).not.toContain("BannerAnnouncementIcon");
// 预览必须走前台同一份展示单元,否则「预览即前台」不成立。
expect(editorSource).toContain("bannerAnnouncementBarStyle(noticeType)");
// 前台通知条必须按分段渲染,不能退回纯文本;底色来自当前通知的类型,标题前不再有固定图标。
expect(sliderSource).toContain("BannerAnnouncementTitle");
expect(sliderSource).toContain("currentBanner.titleRuns");
expect(slider).toContain("bannerAnnouncementBarStyle(currentBanner.noticeType)");
expect(slider).not.toContain("BannerAnnouncementIcon");
expect(slider).not.toContain("notice-banner-surface");
// 展示单元是唯一来源:底色、富文本标题、详情入口都在这里定义,前台与预览共用;
// 图标素材是标题文本的一部分,展示单元不再单独处理图标。
expect(contentSource).toContain("export function bannerAnnouncementBarStyle");
expect(contentSource).toContain("export function BannerAnnouncementLinkHint");
expect(contentSource).toContain("export function BannerAnnouncementTitle");
expect(contentSource).not.toContain("BannerAnnouncementIcon");
// emoji 面板:默认收起(Popover 点击触发),插入后不自动关闭,方便连续插入。
expect(emojiPickerSource).toContain('trigger="click"');
expect(emojiPickerSource).toContain("onPick(item.char)");
expect(noticeSource).toContain("BANNER_NOTICE_EMOJI_GROUPS");
expect(noticeSource).not.toContain("DEFAULT_ICON");
expect(apiSource).toContain("titleRuns?: BannerTitleRun[]");
expect(apiSource).toContain("noticeType?: BannerNoticeType");
expect(apiSource).not.toContain("icon?:");
expect(apiSource).toContain("export type { BannerTitleRun }");
});
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: #f7f8fa;");
expect(tokens).toContain("--admin-canvas: #111317;");
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");
}
});