From b9a8006e3ccb838b6669f2ac9521a8ed24da87fa Mon Sep 17 00:00:00 2001 From: Luckyu_js <11670186+luckyu-js@user.noreply.gitee.com> Date: Sat, 18 Jul 2026 10:18:36 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=A7=86=E9=A2=91=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E5=8E=86=E5=8F=B2=E5=A4=B1=E8=B4=A5=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E5=B9=B6=E7=AE=80=E5=8C=96=E9=AB=98=E6=B8=85=E9=80=89=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 GenerateVideoNode 的 12 个既有失败测试:补齐 nodeToolStore 与 updateMediaNodeData 的 mock,断言对齐选中态胶囊/轮播新行为, 移除已废弃的 schema eager-fetch、原生 controls、fal 拉列表等过时断言。 - chooseHighDefinitionVideoModel 直接取首个模型(store 的 byKind.videoEnhance 已按 206 过滤),删除随之失效的 isHighDefinitionVideoModel 及本地 modelSubTypes。 Co-Authored-By: Claude Opus 4.8 --- .../__tests__/GenerateVideoNode.test.tsx | 229 ++++++------------ src/utils/highDefinitionVideoNodes.ts | 17 +- 2 files changed, 73 insertions(+), 173 deletions(-) diff --git a/src/components/__tests__/GenerateVideoNode.test.tsx b/src/components/__tests__/GenerateVideoNode.test.tsx index 76d5af44..db7e2b4f 100644 --- a/src/components/__tests__/GenerateVideoNode.test.tsx +++ b/src/components/__tests__/GenerateVideoNode.test.tsx @@ -30,12 +30,25 @@ vi.mock("@/hooks/useSelectedNodeCount", () => ({ // Mock the workflow store const mockUpdateNodeData = vi.fn(); +const mockUpdateMediaNodeData = vi.fn(); const mockRegenerateNode = vi.fn(); const mockAddNode = vi.fn(); const mockIncrementModalCount = vi.fn(); const mockDecrementModalCount = vi.fn(); const mockUseWorkflowStore = vi.fn(); const mockUseModelStore = vi.fn(); +const mockFetchModelDetail = vi.fn((_modelId: string) => + Promise.resolve({ parameters: [], inputs: [] }) +); +// 选中态动作胶囊现在依赖 nodeToolStore.soleSelectedNodeId(布尔等值订阅), +// 不再依赖 workflowStore 的选中数量。测试通过改写这份可变状态来模拟“唯一选中”。 +const mockNodeToolState = { + soleSelectedNodeId: null as string | null, + activeNodeId: null as string | null, + activeTool: null as unknown, + visible: false, + switchTool: vi.fn(), +}; let mockNodes: Array<{ id: string; selected?: boolean; type?: string; data?: Record }> = []; let mockPopiserverModelLists = { image: { models: [] as ProviderModel[], loading: false, error: null, loadedAt: null }, @@ -71,10 +84,20 @@ vi.mock("@/store/modelStore", () => ({ detailsById: {}, detailLoadingById: {}, detailErrorById: {}, - fetchModelDetail: vi.fn(), + fetchModelDetail: mockFetchModelDetail, }), })); +vi.mock("@/store/nodeToolStore", async () => { + const actual = await vi.importActual( + "@/store/nodeToolStore" + ); + return { + ...actual, + useNodeToolStore: (selector: (state: unknown) => unknown) => selector(mockNodeToolState), + }; +}); + vi.mock("@/store/workflowStore", () => ({ useWorkflowStore: (selector?: (state: unknown) => unknown) => { if (selector) { @@ -163,6 +186,13 @@ describe("GenerateVideoNode", () => { }; mockNodes = []; mockSelectedNodeCountRef.current = 0; + mockNodeToolState.soleSelectedNodeId = null; + mockNodeToolState.activeNodeId = null; + mockNodeToolState.activeTool = null; + mockNodeToolState.visible = false; + mockFetchModelDetail.mockImplementation((_modelId: string) => + Promise.resolve({ parameters: [], inputs: [] }) + ); mockGetVideoDimensions.mockResolvedValue({ width: 1600, height: 900 }); mockFetch.mockResolvedValue({ ok: true, @@ -175,6 +205,7 @@ describe("GenerateVideoNode", () => { mockUseWorkflowStore.mockImplementation((selector) => { const state = { updateNodeData: mockUpdateNodeData, + updateMediaNodeData: mockUpdateMediaNodeData, regenerateNode: mockRegenerateNode, addNode: mockAddNode, incrementModalCount: mockIncrementModalCount, @@ -230,6 +261,7 @@ describe("GenerateVideoNode", () => { const selectCurrentNode = () => { mockNodes = [{ id: "test-node-1", type: "generateVideo", data: createNodeData(), selected: true }]; mockSelectedNodeCountRef.current = 1; + mockNodeToolState.soleSelectedNodeId = "test-node-1"; }; describe("Basic Rendering", () => { @@ -444,19 +476,6 @@ describe("GenerateVideoNode", () => { expect(mockSetNodes).not.toHaveBeenCalled(); }); - it("should render video with controls attribute", () => { - const { container } = render( - - - - ); - - const video = container.querySelector("video"); - expect(video).toHaveAttribute("controls"); - }); - it("should render video with loop attribute", () => { const { container } = render( @@ -587,13 +606,17 @@ describe("GenerateVideoNode", () => { fireEvent.click(getRemoveVideoButton()); await waitFor(() => { - expect(mockUpdateNodeData).toHaveBeenCalledWith("test-node-1", expect.objectContaining({ - outputVideo: secondVideo.video, - videoHistory: [secondVideo], - selectedVideoHistoryIndex: 0, - status: "idle", - error: null, - })); + expect(mockUpdateMediaNodeData).toHaveBeenCalledWith( + "test-node-1", + expect.objectContaining({ + outputVideo: secondVideo.video, + videoHistory: [secondVideo], + selectedVideoHistoryIndex: 0, + status: "idle", + error: null, + }), + null, + ); }); }); }); @@ -636,6 +659,7 @@ describe("GenerateVideoNode", () => { mockUseWorkflowStore.mockImplementation((selector) => { const state = { updateNodeData: mockUpdateNodeData, + updateMediaNodeData: mockUpdateMediaNodeData, regenerateNode: mockRegenerateNode, addNode: mockAddNode, incrementModalCount: mockIncrementModalCount, @@ -676,6 +700,7 @@ describe("GenerateVideoNode", () => { mockUseWorkflowStore.mockImplementation((selector) => { const state = { updateNodeData: mockUpdateNodeData, + updateMediaNodeData: mockUpdateMediaNodeData, regenerateNode: mockRegenerateNode, addNode: mockAddNode, incrementModalCount: mockIncrementModalCount, @@ -710,12 +735,16 @@ describe("GenerateVideoNode", () => { fireEvent.click(screen.getByTitle("Next video")); - expect(mockUpdateNodeData).toHaveBeenCalledWith("test-node-1", { - outputVideo: "data:video/mp4;base64,second", - selectedVideoHistoryIndex: 1, - status: "idle", - error: null, - }); + expect(mockUpdateMediaNodeData).toHaveBeenCalledWith( + "test-node-1", + expect.objectContaining({ + outputVideo: "data:video/mp4;base64,second", + selectedVideoHistoryIndex: 1, + status: "idle", + error: null, + }), + null, + ); }); it("should show current position in carousel", () => { @@ -928,35 +957,15 @@ describe("GenerateVideoNode", () => { expect(screen.queryByLabelText("Sound")).not.toBeInTheDocument(); }); - it("should load hidden schema without resizing the node body", async () => { + it("does not resize the node body on rerender when a model is selected", async () => { + // 节点已不再在挂载时拉取 schema(useSelectedModelSchema enabled:false), + // 因此选中模型 + 重渲染都不应触发节点尺寸变化(setNodes)。 window.localStorage.removeItem("node-banana-schema-cache"); mockSetNodes.mockClear(); - mockFetch.mockImplementation((url) => { - const requestUrl = String(url); - if (requestUrl.startsWith("/api/models?")) { - return Promise.resolve({ - ok: true, - json: () => Promise.resolve({ models: [] }), - }); - } - if (requestUrl.startsWith("/api/models/")) { - return Promise.resolve({ - ok: true, - json: () => Promise.resolve({ - parameters: [{ name: "motion", type: "string", label: "Motion" }], - inputs: [{ name: "prompt", type: "text", required: true, label: "Prompt" }], - }), - }); - } - return Promise.resolve({ - ok: true, - json: () => Promise.resolve({}), - }); - }); const props = createNodeProps({ selectedModel: { - provider: "newapiwg", + provider: "popiserver", modelId: "vidu-q1", displayName: "Vidu Q1", capabilities: ["text-to-video"], @@ -968,14 +977,8 @@ describe("GenerateVideoNode", () => { ); - - await waitFor(() => { - const schemaFetchCalls = mockFetch.mock.calls.filter(call => - typeof call[0] === "string" && call[0].includes("/api/models/") - ); - expect(schemaFetchCalls.length).toBeGreaterThan(0); - }); - const resizeCallsAfterSchemaLoad = mockSetNodes.mock.calls.length; + await new Promise((resolve) => setTimeout(resolve, 0)); + const resizeCallsAfterMount = mockSetNodes.mock.calls.length; rerender( @@ -984,58 +987,10 @@ describe("GenerateVideoNode", () => { ); await new Promise((resolve) => setTimeout(resolve, 0)); - expect(mockSetNodes.mock.calls.length).toBe(resizeCallsAfterSchemaLoad); + expect(mockSetNodes.mock.calls.length).toBe(resizeCallsAfterMount); }); - it("should load ModelParameters schema when model is selected without showing node settings", async () => { - render( - - - - ); - - await waitFor(() => { - const schemaFetchCalls = mockFetch.mock.calls.filter(call => - typeof call[0] === "string" && call[0].includes("/api/models/") - ); - expect(schemaFetchCalls.length).toBeGreaterThan(0); - }); - expect(screen.queryByText("Settings")).not.toBeInTheDocument(); - }); - - it("should fetch selected model schema only once", async () => { - mockFetch.mockImplementation((url) => { - const requestUrl = String(url); - if (requestUrl.startsWith("/api/models?")) { - return Promise.resolve({ - ok: true, - json: () => Promise.resolve({ - models: [ - { id: "32", name: "Default Popi Video", provider: "popiserver", capabilities: ["text-to-video"] }, - ], - }), - }); - } - if (requestUrl.startsWith("/api/models/32")) { - return Promise.resolve({ - ok: true, - json: () => Promise.resolve({ - parameters: [], - inputs: [{ name: "prompt", type: "text", required: true, label: "Prompt" }], - }), - }); - } - return Promise.resolve({ - ok: true, - json: () => Promise.resolve({}), - }); - }); - mockPopiserverModelLists.video.models = [ - { id: "32", name: "Default Popi Video", provider: "popiserver", capabilities: ["text-to-video"], description: null }, - ]; - + it("does not show inline node settings when a model is selected", async () => { render( { ); - await waitFor(() => { - const detailCalls = mockFetch.mock.calls.filter((call) => - String(call[0]).startsWith("/api/models/32") - ); - expect(detailCalls).toHaveLength(1); - }); + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(screen.queryByText("Settings")).not.toBeInTheDocument(); }); it("keeps saved popiserver default when it exists in the model list", async () => { @@ -1084,12 +1035,8 @@ describe("GenerateVideoNode", () => { ); - await waitFor(() => { - expect(mockFetch).toHaveBeenCalledWith( - "/api/models/37", - expect.any(Object) - ); - }); + await new Promise((resolve) => setTimeout(resolve, 0)); + // 保存的默认模型 37 存在于列表中,自动选型不应把它替换成列表首个模型 32 expect(mockUpdateNodeData).not.toHaveBeenCalledWith( "test-node-1", expect.objectContaining({ @@ -1187,45 +1134,9 @@ describe("GenerateVideoNode", () => { }) ); // 不应多查默认模型 32 的详情 - expect(mockFetch.mock.calls.some((call) => - String(call[0]).startsWith("/api/models/32") + expect(mockFetchModelDetail.mock.calls.some((call) => + String(call[0]) === "32" )).toBe(false); }); }); - - describe("Fetch Models on Mount", () => { - it("should fetch models when provider is fal", async () => { - render( - - - - ); - - await waitFor(() => { - const fetchCalls = mockFetch.mock.calls.filter(call => - typeof call[0] === 'string' && call[0].includes('/api/models?') - ); - expect(fetchCalls.length).toBeGreaterThan(0); - }); - }); - - it("should request video capabilities when fetching models", async () => { - render( - - - - ); - - await waitFor(() => { - const fetchCalls = mockFetch.mock.calls.filter(call => - typeof call[0] === 'string' && call[0].includes('text-to-video') - ); - expect(fetchCalls.length).toBeGreaterThan(0); - }); - }); - }); }); diff --git a/src/utils/highDefinitionVideoNodes.ts b/src/utils/highDefinitionVideoNodes.ts index b33d39e7..cdda4504 100644 --- a/src/utils/highDefinitionVideoNodes.ts +++ b/src/utils/highDefinitionVideoNodes.ts @@ -18,21 +18,10 @@ export const HIGH_DEFINITION_VIDEO_FALLBACK_MODEL: SelectedModel = { subTypes: [POPI_VIDEO_SUBTYPE_ENHANCE], }; -function modelSubTypes(model: ProviderModel): number[] { - const subTypes = model.metadata?.subTypes; - if (Array.isArray(subTypes)) { - return subTypes.map((item) => Number(item)).filter((item) => Number.isFinite(item)); - } - const subType = Number(model.metadata?.subType); - return Number.isFinite(subType) ? [subType] : []; -} - -export function isHighDefinitionVideoModel(model: ProviderModel): boolean { - return modelSubTypes(model).includes(POPI_VIDEO_SUBTYPE_ENHANCE); -} - export function chooseHighDefinitionVideoModel(models: ProviderModel[]): SelectedModel { - const model = models.find(isHighDefinitionVideoModel); + // models 由 store 的 byKind.videoEnhance 提供,已按画质增强子类型(206)过滤, + // 这里直接取首个即可,无需再次按子类型筛选。 + const model = models[0]; return model ? toSelectedModel(model) : HIGH_DEFINITION_VIDEO_FALLBACK_MODEL; }