From 000e3172fb3b310a85d6b6cffa7a59211853d7ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Tue, 6 Oct 2026 14:31:40 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=8E=89=E5=AF=BC=E5=87=BA=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF=E5=8D=A1=E5=9C=A8=E3=80=8C=E6=AD=A3=E5=9C=A8=E8=AF=BB?= =?UTF-8?q?=E5=8F=96=E5=AF=BC=E5=87=BA=E4=BF=A1=E6=81=AF=E2=80=A6=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `usePanel` 的注册表开关改成「进过小红书 tab」或「当前目标就是小红书」两个条件取或:从 tab 条切过来的路径原本既不开开关、也不发生一次文件访问,面板于是永远等不到第一次读取; - 两条入口(点目标大卡、点 tab 条)合并到同一个 `enterTarget`,不再各写一份; - `visited` 单独留着,是为了离开 tab 之后仍保有开关,表单自动保存的 600ms 窗口不被切 tab 掐掉; - 新增 `tests/xhsMinitoolPanelTargets.test.tsx`,三条入口各钉一条。 --- .../export/state/usePanel.ts | 33 +++++-- .../tests/xhsMinitoolPanelTargets.test.tsx | 86 +++++++++++++++++++ 2 files changed, 110 insertions(+), 9 deletions(-) create mode 100644 apps/ai-game-creator-shell/tests/xhsMinitoolPanelTargets.test.tsx diff --git a/apps/ai-game-creator-shell/src/view/project-development/export/state/usePanel.ts b/apps/ai-game-creator-shell/src/view/project-development/export/state/usePanel.ts index 718a31bef..d9642eb8a 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/export/state/usePanel.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/export/state/usePanel.ts @@ -39,21 +39,36 @@ export function usePanel({ */ const [workspaceEntered, setWorkspaceEntered] = useState(false); /** - * 导出注册表的开关比订阅更窄:只有真的点进过小红书 tab 才打开。进陶泥儿不该在用户项目里 + * 导出注册表的开关比订阅更窄:只有真的用过小红书 tab 才打开。进陶泥儿不该在用户项目里 * 补出一份 `.export/xhs-minitool.json`。 + * + * 判据是**两个条件取或**,缺一不可: + * - `visited` 让「离开小红书 tab 之后」继续保有开关(表单自动保存的 600ms 窗口不该因为切 + * tab 被掐掉); + * - `activeTarget` 让「从别的 tab 切过来」当场打开开关。只认 `visited` 是不够的——`visited` + * 只在点目标大卡(`openTarget`)时置真,而从 tab 条切过来走的是 `selectTarget`,于是面板 + * 会永远停在「正在读取导出信息…」:一次文件访问都没发生。 */ - const [xiaohongshuEntered, setXiaohongshuEntered] = useState(false); + const [xiaohongshuVisited, setXiaohongshuVisited] = useState(false); + const xiaohongshuEntered = + xiaohongshuVisited || activeTarget === Target.Xiaohongshu; - const openTarget = useCallback((target: Target) => { + /** 切到某个目标时的共同动作:两条入口(点卡片、点 tab 条)都必须经过它。 */ + const enterTarget = useCallback((target: Target) => { setActiveTarget(target); - setStage(Stage.Workspace); - setWorkspaceEntered(true); - if (target === Target.Xiaohongshu) setXiaohongshuEntered(true); + if (target === Target.Xiaohongshu) setXiaohongshuVisited(true); }, []); - const selectTarget = useCallback((target: Target) => { - setActiveTarget(target); - }, []); + const openTarget = useCallback( + (target: Target) => { + enterTarget(target); + setStage(Stage.Workspace); + setWorkspaceEntered(true); + }, + [enterTarget], + ); + + const selectTarget = enterTarget; const taonier: TaonierTabState = useTaonierTab({ enabled: workspaceEntered && Boolean(projectPath), diff --git a/apps/ai-game-creator-shell/tests/xhsMinitoolPanelTargets.test.tsx b/apps/ai-game-creator-shell/tests/xhsMinitoolPanelTargets.test.tsx new file mode 100644 index 000000000..9a45ec6f2 --- /dev/null +++ b/apps/ai-game-creator-shell/tests/xhsMinitoolPanelTargets.test.tsx @@ -0,0 +1,86 @@ +// @vitest-environment jsdom +/** + * 面板目标切换与「导出注册表开关」的接线。 + * + * 回归的是一次真实故障:点目标大卡进来会打开注册表开关,从顶部 tab 条切过来却不会——那时 + * `useXhsMinitoolExport` 一次文件访问都没发生,`form` 永远是 `null`,左半面板于是永久停在 + * 「正在读取导出信息…」上,看起来像卡死。这个开关只该由「当前目标 / 曾经到过」决定, + * 不该由「从哪条入口进来」决定。 + */ +import { act, renderHook } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; + +import { Target } from '../src/view/project-development/export/model'; + +const { xiaohongshuInputs } = vi.hoisted(() => ({ + xiaohongshuInputs: [] as { enabled: boolean; minitoolEnabled: boolean }[], +})); + +vi.mock( + '../src/view/project-development/export/state/useXiaohongshuTab', + () => ({ + useXiaohongshuTab: (input: { + enabled: boolean; + minitoolEnabled: boolean; + }) => { + xiaohongshuInputs.push(input); + return {} as never; + }, + }), +); +vi.mock('../src/view/project-development/export/state/useTaonierTab', () => ({ + useTaonierTab: () => ({}), +})); + +const { usePanel } = await import( + '../src/view/project-development/export/state/usePanel' +); + +function renderPanel() { + return renderHook(() => + usePanel({ + projectPath: '/tmp/project', + ensureConversationReadAllowed: async () => true, + ensureConversationWriteAllowed: async () => true, + onClose: () => {}, + }), + ); +} + +function lastMinitoolEnabled(): boolean | undefined { + return xiaohongshuInputs.at(-1)?.minitoolEnabled; +} + +describe('导出面板的目标切换', () => { + it('从目标大卡直接进小红书:注册表开关打开', () => { + xiaohongshuInputs.length = 0; + const panel = renderPanel(); + + act(() => panel.result.current.openTarget(Target.Xiaohongshu)); + + expect(lastMinitoolEnabled()).toBe(true); + }); + + it('先进陶泥儿再从 tab 条切到小红书:注册表开关一样要打开', () => { + xiaohongshuInputs.length = 0; + const panel = renderPanel(); + + act(() => panel.result.current.openTarget(Target.Taonier)); + // 进陶泥儿不该在用户项目里补出一份 `.export/xhs-minitool.json`。 + expect(lastMinitoolEnabled()).toBe(false); + + act(() => panel.result.current.selectTarget(Target.Xiaohongshu)); + + expect(lastMinitoolEnabled()).toBe(true); + }); + + it('到过小红书之后再切走:开关保持打开,表单自动保存的窗口不被掐掉', () => { + xiaohongshuInputs.length = 0; + const panel = renderPanel(); + + act(() => panel.result.current.openTarget(Target.Xiaohongshu)); + act(() => panel.result.current.selectTarget(Target.Taonier)); + + expect(lastMinitoolEnabled()).toBe(true); + }); +});