fix(web): 导演台检查 - 隔离开发复现入口的外观初始化请求
This commit is contained in:
@@ -69,6 +69,12 @@ PR #543 本次未合入:其已发布迁移校验和改写、管理员显式图
|
||||
- [ ] 检查浅色、深色与窄屏下的费用区域、模型表格横向滚动及长金额换行。
|
||||
- [ ] 新建或编辑模型,在“模型展示名(一级目录)”选择其他渠道已有分组;输入新分组保存后再次编辑,确认下拉更新且创作端归组正确。
|
||||
|
||||
## CI 检查修复与镜像构建提速
|
||||
|
||||
- [x] 修复导演台 DEV 复现入口在 React 初始化前请求 `/api/public/appearance` 导致的 E2E 502:入口和 Provider 共用精确路径隔离,普通页面与生产环境仍执行外观初始化,未放宽浏览器错误断言。
|
||||
- [x] 外观启动与导演台专项测试 54 项通过,包含执行真实 `main.tsx` 分支的 9 项新增回归;生产构建(含 TypeScript 检查)通过,仍有 chunk 大小告警。
|
||||
- [ ] 在 GitHub 重跑 Director P0 Chrome E2E,确认六个场景不再出现外观接口 502;本轮未启动本地开发服务器或执行 Chrome E2E,不以入口回归测试替代浏览器验收。
|
||||
|
||||
## 画布素材链路与工作条改造(2026-09-19)
|
||||
|
||||
已实现:
|
||||
|
||||
@@ -9,6 +9,7 @@ import { FullScreenLoader } from "@/components/ui/aceternity/full-screen-loader"
|
||||
import { getAntThemeConfig } from "@/lib/app-theme";
|
||||
import { applySkinTheme } from "@/lib/skin-themes";
|
||||
import { appQueryClient } from "@/lib/query-client";
|
||||
import { isIsolatedDirectorRepro } from "@/lib/dev-repro";
|
||||
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
|
||||
import { applyAppearanceMetadata, useAppearanceStore } from "@/stores/use-appearance-store";
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
@@ -35,8 +36,8 @@ export function AppProviders({ children }: { children: ReactNode }) {
|
||||
|
||||
// DEV 复现台必须是同源本地确定性场景:AuthSessionHydrator 会打 /api/auth/session,
|
||||
// ClientRootInit 会打 /api/model-catalog,没有后端时产生真实 502,与导演台无关却会污染判据。
|
||||
// 只精确匹配该路径;生产构建中 import.meta.env.DEV 为 false,本分支被摇树删除。
|
||||
const isolateDevRepro = import.meta.env.DEV && typeof window !== "undefined" && window.location.pathname === "/dev/director-repro";
|
||||
// 与启动入口共用精确路径边界;生产构建中 import.meta.env.DEV 为 false,始终不启用隔离。
|
||||
const isolateDevRepro = typeof window !== "undefined" && isIsolatedDirectorRepro(import.meta.env.DEV, window.location.pathname);
|
||||
|
||||
return (
|
||||
<ConfigProvider locale={zhCN} theme={getAntThemeConfig(dark, appearance.activeSkin)} wave={{ disabled: true }}>
|
||||
|
||||
3
web/src/lib/dev-repro.ts
Normal file
3
web/src/lib/dev-repro.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function isIsolatedDirectorRepro(dev: boolean, pathname: string): boolean {
|
||||
return dev && pathname === "/dev/director-repro";
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
import "@fontsource-variable/inter";
|
||||
import "@fontsource-variable/jetbrains-mono";
|
||||
import { bootstrapAppearance } from "@/services/appearance-bootstrap";
|
||||
import { isIsolatedDirectorRepro } from "@/lib/dev-repro";
|
||||
|
||||
// The public film entry checks its availability independently of workspace bootstrap.
|
||||
if (/^\/welcome\/?$/.test(window.location.pathname)) void import("./welcome-application");
|
||||
else void bootstrapAppearance().finally(() => import("./application"));
|
||||
else {
|
||||
// The backend-free DEV lab must not make requests before AppProviders isolates it.
|
||||
const appearanceReady = isIsolatedDirectorRepro(import.meta.env.DEV, window.location.pathname) ? Promise.resolve() : bootstrapAppearance();
|
||||
void appearanceReady.finally(() => import("./application"));
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ test("initial HTML stays brand neutral until the public appearance is resolved",
|
||||
|
||||
expect(html).not.toContain("影策");
|
||||
expect(html).toContain("<title>正在加载</title>");
|
||||
expect(mainSource).toContain("bootstrapAppearance()");
|
||||
expect(mainSource).toContain('import("./application")');
|
||||
expect(mainSource.indexOf("bootstrapAppearance()")).toBeLessThan(mainSource.indexOf('import("./application")'));
|
||||
});
|
||||
|
||||
|
||||
76
web/test/application-bootstrap.test.mjs
Normal file
76
web/test/application-bootstrap.test.mjs
Normal file
@@ -0,0 +1,76 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { runInNewContext } from "node:vm";
|
||||
|
||||
import { isIsolatedDirectorRepro } from "../src/lib/dev-repro";
|
||||
|
||||
// Execute the real entry point; replace only its font, network and UI side effects.
|
||||
async function prepareEntry(dev, pathname) {
|
||||
const build = await Bun.build({
|
||||
entrypoints: [new URL("../src/main.tsx", import.meta.url).pathname],
|
||||
target: "browser",
|
||||
format: "iife",
|
||||
define: { "import.meta.env.DEV": JSON.stringify(dev) },
|
||||
plugins: [
|
||||
{
|
||||
name: "entry-side-effects",
|
||||
setup(builder) {
|
||||
builder.onResolve({ filter: /^(@fontsource-variable\/|@\/services\/appearance-bootstrap$|\.\/(welcome-)?application$)/ }, ({ path }) => ({ path, namespace: "entry-test" }));
|
||||
builder.onLoad({ filter: /.*/, namespace: "entry-test" }, ({ path }) => {
|
||||
if (path.startsWith("@fontsource-variable/")) return { contents: "", loader: "js" };
|
||||
if (path === "@/services/appearance-bootstrap") {
|
||||
return { contents: 'export function bootstrapAppearance() { events.push("appearance"); return appearanceReady; }', loader: "js" };
|
||||
}
|
||||
return { contents: `events.push(${JSON.stringify(path)}); entryLoaded();`, loader: "js" };
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(build.success).toBe(true);
|
||||
|
||||
const events = [];
|
||||
let resolveAppearance;
|
||||
let entryLoaded;
|
||||
const appearanceReady = new Promise((resolve) => (resolveAppearance = resolve));
|
||||
const loaded = new Promise((resolve) => (entryLoaded = resolve));
|
||||
runInNewContext(await build.outputs[0].text(), { window: { location: { pathname } }, events, appearanceReady, entryLoaded });
|
||||
return { events, resolveAppearance, loaded };
|
||||
}
|
||||
|
||||
test("DEV director lab loads without calling the appearance backend", async () => {
|
||||
const entry = await prepareEntry(true, "/dev/director-repro");
|
||||
await entry.loaded;
|
||||
expect(entry.events).toEqual(["./application"]);
|
||||
});
|
||||
|
||||
for (const [dev, pathname] of [
|
||||
[false, "/dev/director-repro"],
|
||||
[true, "/login"],
|
||||
[false, "/login"],
|
||||
[true, "/dev/director-repro/"],
|
||||
[true, "/dev/director-repro-other"],
|
||||
]) {
|
||||
test(`appearance still blocks normal startup: dev=${dev} path=${pathname}`, async () => {
|
||||
const entry = await prepareEntry(dev, pathname);
|
||||
expect(entry.events).toEqual(["appearance"]);
|
||||
entry.resolveAppearance();
|
||||
await entry.loaded;
|
||||
expect(entry.events).toEqual(["appearance", "./application"]);
|
||||
});
|
||||
}
|
||||
|
||||
for (const pathname of ["/welcome", "/welcome/"]) {
|
||||
test(`public film entry remains independent: ${pathname}`, async () => {
|
||||
const entry = await prepareEntry(false, pathname);
|
||||
await entry.loaded;
|
||||
expect(entry.events).toEqual(["./welcome-application"]);
|
||||
});
|
||||
}
|
||||
|
||||
test("provider isolation shares the exact DEV-only route boundary", () => {
|
||||
expect(isIsolatedDirectorRepro(true, "/dev/director-repro")).toBe(true);
|
||||
expect(isIsolatedDirectorRepro(false, "/dev/director-repro")).toBe(false);
|
||||
for (const path of ["/", "/login", "/dev/director-repro/", "/dev/director-repro-other"]) {
|
||||
expect(isIsolatedDirectorRepro(true, path)).toBe(false);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user