271 lines
17 KiB
TypeScript
271 lines
17 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("api.post(`/admin/channels/${encodeURIComponent(channelId)}/models/fetch`)");
|
|
expect(apiSource).toContain("api.post(`/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("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("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, 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/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(featureSource).toContain('className="admin-feature-runtime-note"');
|
|
|
|
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");
|
|
});
|