fix(workspace): 创作工作台 - 修复线上样式层级并优化 Agent、菜单与素材密度
This commit is contained in:
49
web/test/agent-panel-presentation.test.tsx
Normal file
49
web/test/agent-panel-presentation.test.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { AgentWelcome } from "@/components/canvas/canvas-agent-welcome";
|
||||
import { AgentChatComposer } from "@/components/canvas/canvas-cloud-agent-chat-ui";
|
||||
import { agentPermissionVisual } from "@/components/canvas/canvas-cloud-agent-settings";
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
test("welcome uses the configured brand and escapes it as text", () => {
|
||||
const html = renderToStaticMarkup(<AgentWelcome brandName="我的<工作室>" nodeCount={2} onChooseSkill={noop} onDraftPrompt={noop} />);
|
||||
expect(html).toContain("在这里,和我的<工作室>让灵感,慢慢成形");
|
||||
expect(html).toContain("2 个节点");
|
||||
expect(html.match(/type="button"/g)).toHaveLength(3);
|
||||
expect(html).not.toContain('disabled=""');
|
||||
});
|
||||
|
||||
test("empty canvas disables only the canvas-analysis shortcut", () => {
|
||||
const html = renderToStaticMarkup(<AgentWelcome brandName="影策" nodeCount={0} onChooseSkill={noop} onDraftPrompt={noop} />);
|
||||
expect(html.match(/disabled=""/g)).toHaveLength(1);
|
||||
expect(html).toContain("添加节点后,一起梳理创作思路");
|
||||
expect(html).not.toContain("生成前由你确认");
|
||||
});
|
||||
|
||||
test("permission modes have distinct icons, not just distinct colors", () => {
|
||||
const icons = (["read_only", "request_approval", "auto"] as const).map((mode) => {
|
||||
const Icon = agentPermissionVisual(mode).icon;
|
||||
return renderToStaticMarkup(<Icon />);
|
||||
});
|
||||
expect(new Set(icons).size).toBe(3);
|
||||
expect(icons[0]).toContain("lucide-lock-keyhole");
|
||||
expect(icons[1]).toContain("lucide-shield-check");
|
||||
expect(icons[2]).toContain("lucide-sparkles");
|
||||
});
|
||||
|
||||
test("compact send button preserves empty-draft and sending guards", () => {
|
||||
for (const [prompt, sending, disabled] of [
|
||||
["", false, true],
|
||||
["故事灵感", false, false],
|
||||
["故事灵感", true, true],
|
||||
] as const) {
|
||||
const html = renderToStaticMarkup(<AgentChatComposer prompt={prompt} sending={sending} placeholder="聊聊想法" theme={canvasThemes.dark} onPromptChange={noop} onSubmit={noop} />);
|
||||
const button = html.match(/<button\b[^>]*class="agent-composer-send [^"]*"[^>]*>/)?.[0];
|
||||
expect(button).toBeDefined();
|
||||
expect(button).toContain("size-7");
|
||||
expect(button?.includes('disabled=""')).toBe(disabled);
|
||||
expect(button).not.toContain("box-shadow:");
|
||||
}
|
||||
});
|
||||
29
web/test/css-layer-order.test.ts
Normal file
29
web/test/css-layer-order.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { expect, test } from "bun:test";
|
||||
|
||||
const html = await Bun.file(new URL("../index.html", import.meta.url)).text();
|
||||
const layerOrder = "@layer theme, base, components, utilities;";
|
||||
|
||||
test("HTML 在任何外部样式或脚本之前固定 CSS 层顺序", () => {
|
||||
const firstStyle = html.match(/<style\b[^>]*>([\s\S]*?)<\/style>/i);
|
||||
expect(firstStyle).not.toBeNull();
|
||||
const css = firstStyle![1].replace(/\/\*[\s\S]*?\*\//g, "").trim();
|
||||
expect(css.startsWith(layerOrder)).toBe(true);
|
||||
|
||||
const firstLoad = html.search(/<script\b|<link\b[^>]*\brel=["'](?:stylesheet|modulepreload)["']/i);
|
||||
expect(firstLoad).toBeGreaterThan(html.indexOf(layerOrder));
|
||||
});
|
||||
|
||||
test("入口层顺序与 Tailwind 一致,不依赖插件和全局 CSS 的加载先后", async () => {
|
||||
const tailwind = await Bun.file(new URL("../node_modules/tailwindcss/index.css", import.meta.url)).text();
|
||||
const editor = await Bun.file(new URL("../src/lib/plugins/builtin/editor/editor-shell.css", import.meta.url)).text();
|
||||
const firstStyle = html.match(/<style\b[^>]*>([\s\S]*?)<\/style>/i)![1];
|
||||
expect(tailwind.trimStart().startsWith(layerOrder)).toBe(true);
|
||||
expect(editor).toContain("@layer utilities {");
|
||||
|
||||
// CSS layers retain their first declared order, even when a later chunk repeats them.
|
||||
const layersIn = (css: string) => [...new Set([...css.matchAll(/@layer\s+([^;{]+)[;{]/g)].flatMap((match) => match[1].split(",").map((name) => name.trim())))];
|
||||
expect(layersIn(editor + tailwind).indexOf("utilities")).toBeLessThan(layersIn(editor + tailwind).indexOf("base"));
|
||||
for (const chunks of [editor + tailwind, tailwind + editor]) {
|
||||
expect(layersIn(firstStyle + chunks)).toEqual(["theme", "base", "components", "utilities"]);
|
||||
}
|
||||
});
|
||||
92
web/test/workspace-menu-density.test.tsx
Normal file
92
web/test/workspace-menu-density.test.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { ConfigProvider, Select } from "antd";
|
||||
import { useContext } from "react";
|
||||
import { CollectionToolbar } from "@/components/layout/collection-toolbar";
|
||||
import { getWorkspaceAntThemeConfig } from "@/lib/app-theme";
|
||||
import { assetGridCardMinWidth, assetGridDensityOptions, parseAssetGridDensity } from "@/pages/assets/asset-grid-density";
|
||||
|
||||
test("density restores all saved choices and defaults invalid preferences to standard", () => {
|
||||
for (const value of [6, 8, 10] as const) {
|
||||
expect(parseAssetGridDensity(String(value))).toBe(value);
|
||||
expect(parseAssetGridDensity(value)).toBe(value);
|
||||
}
|
||||
for (const value of [null, undefined, "", "broken", "12"]) {
|
||||
expect(parseAssetGridDensity(value)).toBe(8);
|
||||
}
|
||||
});
|
||||
|
||||
test("each density has a distinct usable card size at normal library widths", () => {
|
||||
expect(assetGridDensityOptions.map(({ value }) => value)).toEqual([6, 8, 10]);
|
||||
const widths = [6, 8, 10].map((density) => assetGridCardMinWidth[parseAssetGridDensity(density)]);
|
||||
expect(widths).toEqual([272, 208, 160]);
|
||||
// CSS auto-fill: n columns fit when n * minimum + (n - 1) * gap <= width.
|
||||
for (const available of [720, 900, 1100]) {
|
||||
const columns = widths.map((minimum) => Math.floor((available + 16) / (minimum + 16)));
|
||||
expect(columns[0]).toBeLessThan(columns[1]);
|
||||
expect(columns[1]).toBeLessThan(columns[2]);
|
||||
}
|
||||
});
|
||||
|
||||
test("real collection toolbar supplies filled selects without changing their labels or value", () => {
|
||||
function PopupConfigProbe() {
|
||||
const { select } = useContext(ConfigProvider.ConfigContext);
|
||||
return <span>{JSON.stringify(select?.classNames)}</span>;
|
||||
}
|
||||
const html = renderToStaticMarkup(
|
||||
<CollectionToolbar>
|
||||
<Select aria-label="素材显示密度" value={10} options={assetGridDensityOptions} />
|
||||
<PopupConfigProbe />
|
||||
</CollectionToolbar>,
|
||||
);
|
||||
expect(html).toContain("ant-select-filled");
|
||||
expect(html).toContain("素材显示密度");
|
||||
expect(html).toContain("紧凑");
|
||||
expect(html).toContain('role="combobox"');
|
||||
expect(html).toContain("workspace-quiet-popup");
|
||||
expect(html).toContain('data-input-modality="keyboard"');
|
||||
});
|
||||
|
||||
test("collection select focus uses a thin keyboard ring, not a persistent pointer outline", () => {
|
||||
const css = readFileSync(new URL("../src/styles/workspace-product.css", import.meta.url), "utf8");
|
||||
const toolbar = readFileSync(new URL("../src/components/layout/collection-toolbar.tsx", import.meta.url), "utf8");
|
||||
expect(css).toContain(".collection-toolbar .ant-select {\n outline: none !important;");
|
||||
expect(css).toContain('[data-input-modality="keyboard"] .ant-select:not(.ant-select-disabled):focus-within');
|
||||
expect(css).toContain("outline: 1px solid var(--control-focus-ring) !important;");
|
||||
expect(toolbar).toContain('onPointerDownCapture={() => setInputModality("pointer")}');
|
||||
expect(toolbar).toContain("onKeyDownCapture=");
|
||||
expect(toolbar).toContain("event.currentTarget.contains(event.relatedTarget)");
|
||||
expect(toolbar).not.toContain(".blur()");
|
||||
});
|
||||
|
||||
test("workspace menu tokens are shadow-free and use theme-aware surfaces", () => {
|
||||
const { components } = getWorkspaceAntThemeConfig();
|
||||
for (const name of ["Select", "Dropdown", "Popover"] as const) {
|
||||
expect(components?.[name]?.boxShadowSecondary).toBe("none");
|
||||
expect(components?.[name]?.colorBgElevated).toBe("var(--user-surface-raised)");
|
||||
}
|
||||
expect(components?.Select?.optionSelectedBg).toBe("var(--user-surface-hover)");
|
||||
});
|
||||
|
||||
test("real grid consumes density rather than the old fixed column calculation", () => {
|
||||
const page = readFileSync(new URL("../src/pages/assets/index.tsx", import.meta.url), "utf8");
|
||||
const css = readFileSync(new URL("../src/styles/workspace-product.css", import.meta.url), "utf8");
|
||||
const globals = readFileSync(new URL("../src/styles/globals.css", import.meta.url), "utf8");
|
||||
expect(page).toContain('"--collection-grid-min-width": `${assetGridCardMinWidth[gridDensity]}px`');
|
||||
expect(css).toContain("minmax(min(100%, var(--collection-grid-min-width, 224px)), 1fr)");
|
||||
expect(globals).not.toContain("--assets-grid-columns");
|
||||
expect(css).not.toContain(".assets-library-page .assets-library-grid { grid-template-columns: repeat(2");
|
||||
});
|
||||
|
||||
test("menu surfaces are explicitly scoped and old account inner overrides are removed", () => {
|
||||
const css = readFileSync(new URL("../src/styles/workspace-menus.css", import.meta.url), "utf8");
|
||||
const globals = readFileSync(new URL("../src/styles/globals.css", import.meta.url), "utf8");
|
||||
expect(css).toContain("@layer utilities");
|
||||
expect(css).toContain(".workspace-account-popover .ant-popover-container");
|
||||
expect(css).toContain("border: 0 !important");
|
||||
expect(css).toContain("box-shadow: none !important");
|
||||
expect(css).not.toContain(".ant-modal");
|
||||
expect(globals).not.toContain(".workspace-account-popover .ant-popover-inner");
|
||||
expect(globals).toContain(".ant-select:has(input:focus-visible)");
|
||||
});
|
||||
Reference in New Issue
Block a user