重构画布动作上下文与刷新链路
新增编辑器级画布动作上下文与 Provider,统一承载画布动作 将 Agent 对话和任务侧栏的画布刷新迁移到上下文,移除逐层回调透传 统一生成任务完成后的项目刷新入口,避免重复加载项目快照 补齐上下文及刷新链路测试,并更新测试 Provider 装配
This commit is contained in:
+15
-5
@@ -3,11 +3,12 @@
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
render as testingLibraryRender,
|
||||
screen,
|
||||
waitFor,
|
||||
within,
|
||||
} from '@testing-library/react';
|
||||
import type { ReactElement, ReactNode } from 'react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type {
|
||||
@@ -17,6 +18,7 @@ import type {
|
||||
} from '@/packages/shared/src/contracts';
|
||||
import type { EditorAgentConversationClient } from '@/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts';
|
||||
import { EDITOR_AGENT_PATIENCE_NOTICE_DELAY_MS } from '@/src/components/image-editor/EditorAgentConversation/useEditorAgentConversation.ts';
|
||||
import { ImageCanvasActionsProvider } from '@/src/components/image-editor/ImageCanvasActionsProvider.tsx';
|
||||
import { useImageCanvasContextStore } from '@/src/components/image-editor/useImageCanvasContextStore.ts';
|
||||
|
||||
import { EditorAgentConversationPanelView } from './EditorAgentConversationPanelView.tsx';
|
||||
@@ -24,6 +26,17 @@ import { EditorAgentConversationPanelView } from './EditorAgentConversationPanel
|
||||
const createEditorProjectResourceMock = vi.hoisted(() => vi.fn());
|
||||
const uploadEditorMediaAssetFileMock = vi.hoisted(() => vi.fn());
|
||||
const probeImageFileDimensionsMock = vi.hoisted(() => vi.fn());
|
||||
const refreshCanvasMock = vi.fn();
|
||||
|
||||
function render(ui: ReactElement) {
|
||||
return testingLibraryRender(ui, {
|
||||
wrapper: ({ children }: { children: ReactNode }) => (
|
||||
<ImageCanvasActionsProvider refreshCanvas={refreshCanvasMock}>
|
||||
{children}
|
||||
</ImageCanvasActionsProvider>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
vi.mock('@/src/services/image-editor/editorProjectClient.ts', async () => {
|
||||
const actual = await vi.importActual<
|
||||
@@ -1801,14 +1814,11 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
resolveConfirmation = resolve;
|
||||
}),
|
||||
);
|
||||
const onCanvasRefreshRequested = vi.fn();
|
||||
|
||||
render(
|
||||
<EditorAgentConversationPanelView
|
||||
open
|
||||
onToggleOpen={vi.fn()}
|
||||
client={client}
|
||||
onCanvasRefreshRequested={onCanvasRefreshRequested}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -1850,7 +1860,7 @@ describe('EditorAgentConversationPanelView', () => {
|
||||
'internal completed tool output that must stay hidden',
|
||||
),
|
||||
).toBeNull();
|
||||
expect(onCanvasRefreshRequested).not.toHaveBeenCalled();
|
||||
expect(refreshCanvasMock).not.toHaveBeenCalled();
|
||||
expect(screen.queryByRole('button', { name: '确认' })).toBeNull();
|
||||
});
|
||||
|
||||
|
||||
-4
@@ -42,7 +42,6 @@ type EditorAgentConversationPanelViewProps = {
|
||||
onToggleOpen: () => void;
|
||||
layers?: CanvasLayer[];
|
||||
assets?: EditorAsset[];
|
||||
onCanvasRefreshRequested?: () => void;
|
||||
// TODO refactor: move the task list update seperate
|
||||
onConfirmSent?: () => void;
|
||||
client?: EditorAgentConversationClient;
|
||||
@@ -57,7 +56,6 @@ export function EditorAgentConversationPanelView({
|
||||
onToggleOpen,
|
||||
layers = [],
|
||||
assets = [],
|
||||
onCanvasRefreshRequested,
|
||||
onConfirmSent,
|
||||
client,
|
||||
}: EditorAgentConversationPanelViewProps) {
|
||||
@@ -93,7 +91,6 @@ export function EditorAgentConversationPanelView({
|
||||
} = useEditorAgentConversation({
|
||||
projectId: effectiveProjectId,
|
||||
client,
|
||||
onCanvasRefreshRequested,
|
||||
onConfirmSent,
|
||||
});
|
||||
const [draftText, setDraftText] = useState('');
|
||||
@@ -310,7 +307,6 @@ export function EditorAgentConversationPanelView({
|
||||
onReferenceImage={referenceContextAsset}
|
||||
onJobCompleted={() => {
|
||||
void refreshActiveConversation();
|
||||
onCanvasRefreshRequested?.();
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { act, render, screen, waitFor } from '@testing-library/react';
|
||||
import type { ReactElement, ReactNode } from 'react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { EditorAgentToolCall } from '@/packages/shared/src/contracts';
|
||||
import { ImageCanvasActionsProvider } from '@/src/components/image-editor/ImageCanvasActionsProvider.tsx';
|
||||
|
||||
import ToolCallView from './ToolCallView.tsx';
|
||||
|
||||
const getExternalGenerationJobStatusMock = vi.hoisted(() => vi.fn());
|
||||
const refreshCanvasMock = vi.fn();
|
||||
|
||||
vi.mock('@/src/services/external-generation', () => ({
|
||||
getExternalGenerationJobStatus: getExternalGenerationJobStatusMock,
|
||||
@@ -54,9 +57,20 @@ function createCompletedJobResponse(jobId: string) {
|
||||
};
|
||||
}
|
||||
|
||||
function renderWithCanvasActions(ui: ReactElement) {
|
||||
return render(ui, {
|
||||
wrapper: ({ children }: { children: ReactNode }) => (
|
||||
<ImageCanvasActionsProvider refreshCanvas={refreshCanvasMock}>
|
||||
{children}
|
||||
</ImageCanvasActionsProvider>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
describe('ToolCallView', () => {
|
||||
beforeEach(() => {
|
||||
getExternalGenerationJobStatusMock.mockReset();
|
||||
refreshCanvasMock.mockReset();
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -65,14 +79,13 @@ describe('ToolCallView', () => {
|
||||
] as const)(
|
||||
'keeps the server %s state when the pending response arrives late',
|
||||
async (status, statusLabel, error) => {
|
||||
const pendingResponse = createDeferred<
|
||||
ReturnType<typeof createCompletedJobResponse>
|
||||
>();
|
||||
const pendingResponse =
|
||||
createDeferred<ReturnType<typeof createCompletedJobResponse>>();
|
||||
getExternalGenerationJobStatusMock.mockReturnValueOnce(
|
||||
pendingResponse.promise,
|
||||
);
|
||||
const onJobCompleted = vi.fn();
|
||||
const { rerender } = render(
|
||||
const { rerender } = renderWithCanvasActions(
|
||||
<ToolCallView
|
||||
toolCall={createToolCall()}
|
||||
onJobCompleted={onJobCompleted}
|
||||
@@ -101,21 +114,20 @@ describe('ToolCallView', () => {
|
||||
expect(screen.getByText(statusLabel)).toBeTruthy();
|
||||
expect(screen.queryByText('已完成')).toBeNull();
|
||||
expect(onJobCompleted).not.toHaveBeenCalled();
|
||||
expect(refreshCanvasMock).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it('ignores job A after switching to job B and completes job B once', async () => {
|
||||
const jobAResponse = createDeferred<
|
||||
ReturnType<typeof createCompletedJobResponse>
|
||||
>();
|
||||
const jobBResponse = createDeferred<
|
||||
ReturnType<typeof createCompletedJobResponse>
|
||||
>();
|
||||
const jobAResponse =
|
||||
createDeferred<ReturnType<typeof createCompletedJobResponse>>();
|
||||
const jobBResponse =
|
||||
createDeferred<ReturnType<typeof createCompletedJobResponse>>();
|
||||
getExternalGenerationJobStatusMock.mockImplementation((jobId: string) =>
|
||||
jobId === 'job-a' ? jobAResponse.promise : jobBResponse.promise,
|
||||
);
|
||||
const onJobCompleted = vi.fn();
|
||||
const { rerender } = render(
|
||||
const { rerender } = renderWithCanvasActions(
|
||||
<ToolCallView
|
||||
toolCall={createToolCall()}
|
||||
onJobCompleted={onJobCompleted}
|
||||
@@ -150,5 +162,7 @@ describe('ToolCallView', () => {
|
||||
|
||||
expect(await screen.findByText('已完成')).toBeTruthy();
|
||||
expect(onJobCompleted).toHaveBeenCalledTimes(1);
|
||||
expect(refreshCanvasMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import type { EditorAgentToolCall } from '@/packages/shared/src/contracts';
|
||||
import { editorAgentToolLabel } from '@/src/components/image-editor/EditorAgentConversation/toolCallPresentation.ts';
|
||||
import { useImageCanvasActions } from '@/src/components/image-editor/ImageCanvasActionsContext.ts';
|
||||
import { ResolvedAssetAudio } from '@/src/components/ResolvedAssetAudio.tsx';
|
||||
import { ResolvedAssetImage } from '@/src/components/ResolvedAssetImage.tsx';
|
||||
import { ResolvedAssetVideo } from '@/src/components/ResolvedAssetVideo.tsx';
|
||||
@@ -19,6 +20,7 @@ function ToolCallView({
|
||||
onJobCompleted?: () => void;
|
||||
onRightClickMenu?: RightClickMenuHandler;
|
||||
}) {
|
||||
const { refreshCanvas } = useImageCanvasActions();
|
||||
const videos = toolCall.videos ?? [];
|
||||
const audios = toolCall.audios ?? [];
|
||||
const jobId = toolCall.externalJobId?.trim() || null;
|
||||
@@ -37,9 +39,13 @@ function ToolCallView({
|
||||
useState<string | null>(initialDisplayError);
|
||||
const terminalNotifiedRef = useRef(false);
|
||||
const onJobCompletedRef = useRef(onJobCompleted);
|
||||
const refreshCanvasRef = useRef(refreshCanvas);
|
||||
useEffect(() => {
|
||||
onJobCompletedRef.current = onJobCompleted;
|
||||
}, [onJobCompleted]);
|
||||
useEffect(() => {
|
||||
refreshCanvasRef.current = refreshCanvas;
|
||||
}, [refreshCanvas]);
|
||||
useEffect(() => {
|
||||
setDisplayStatus(initialDisplayStatus);
|
||||
setDisplayError(initialDisplayError);
|
||||
@@ -59,6 +65,7 @@ function ToolCallView({
|
||||
setDisplayError(response.job.error ?? null);
|
||||
if (!terminalNotifiedRef.current) {
|
||||
terminalNotifiedRef.current = true;
|
||||
refreshCanvasRef.current();
|
||||
onJobCompletedRef.current?.();
|
||||
}
|
||||
return;
|
||||
|
||||
+37
-13
@@ -1,6 +1,13 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import {
|
||||
act,
|
||||
renderHook as testingLibraryRenderHook,
|
||||
type RenderHookOptions,
|
||||
type RenderHookResult,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
@@ -9,12 +16,37 @@ import {
|
||||
type EditorAgentMessage,
|
||||
type EditorAgentMessageResponse,
|
||||
} from '../../../../packages/shared/src/contracts/editorAgent.ts';
|
||||
import { ImageCanvasActionsProvider } from '../ImageCanvasActionsProvider.tsx';
|
||||
import {
|
||||
EDITOR_AGENT_PATIENCE_NOTICE_DELAY_MS,
|
||||
type EditorAgentConversationClient,
|
||||
useEditorAgentConversation,
|
||||
} from './useEditorAgentConversation.ts';
|
||||
|
||||
const refreshCanvasMock = vi.fn();
|
||||
|
||||
function ImageCanvasActionsTestWrapper({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<ImageCanvasActionsProvider refreshCanvas={refreshCanvasMock}>
|
||||
{children}
|
||||
</ImageCanvasActionsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function renderHook<Result, Props>(
|
||||
callback: (initialProps: Props) => Result,
|
||||
options?: RenderHookOptions<Props>,
|
||||
): RenderHookResult<Result, Props> {
|
||||
return testingLibraryRenderHook(callback, {
|
||||
...options,
|
||||
wrapper: ImageCanvasActionsTestWrapper,
|
||||
});
|
||||
}
|
||||
|
||||
function createEditImageDisplayArgs(prompt: string) {
|
||||
return {
|
||||
stringArgs: [{ name: 'prompt', label: '修改要求', value: prompt }],
|
||||
@@ -124,12 +156,10 @@ describe('useEditorAgentConversation', () => {
|
||||
|
||||
it('loads conversations and applies delta messages', async () => {
|
||||
const client = createClient();
|
||||
const onCanvasRefreshRequested = vi.fn();
|
||||
const { result } = renderHook(() =>
|
||||
useEditorAgentConversation({
|
||||
projectId: 'project-1',
|
||||
client,
|
||||
onCanvasRefreshRequested,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -156,7 +186,7 @@ describe('useEditorAgentConversation', () => {
|
||||
expect(result.current.activeConversation?.title).toBe(
|
||||
'把这个角色改成像素风',
|
||||
);
|
||||
expect(onCanvasRefreshRequested).toHaveBeenCalledTimes(1);
|
||||
expect(refreshCanvasMock).toHaveBeenCalledTimes(1);
|
||||
expect(result.current.messages.map((message) => message.text)).toEqual([
|
||||
'把这个角色改成像素风',
|
||||
'我来处理',
|
||||
@@ -595,7 +625,6 @@ describe('useEditorAgentConversation', () => {
|
||||
|
||||
it('does not apply a completed message response after switching conversations', async () => {
|
||||
const client = createClient();
|
||||
const onCanvasRefreshRequested = vi.fn();
|
||||
let resolveSend!: (response: EditorAgentMessageResponse) => void;
|
||||
vi.mocked(client.sendMessage).mockImplementation(
|
||||
() =>
|
||||
@@ -607,7 +636,6 @@ describe('useEditorAgentConversation', () => {
|
||||
useEditorAgentConversation({
|
||||
projectId: 'project-1',
|
||||
client,
|
||||
onCanvasRefreshRequested,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -682,7 +710,7 @@ describe('useEditorAgentConversation', () => {
|
||||
expect(result.current.activeConversationId).toBe('conversation-2');
|
||||
expect(result.current.messages).toHaveLength(1);
|
||||
expect(result.current.messages[0]?.text).toBe('第二个会话原有消息');
|
||||
expect(onCanvasRefreshRequested).not.toHaveBeenCalled();
|
||||
expect(refreshCanvasMock).not.toHaveBeenCalled();
|
||||
expect(
|
||||
result.current.conversations.find(
|
||||
(conversation) => conversation.conversationId === 'conversation-1',
|
||||
@@ -931,13 +959,11 @@ describe('useEditorAgentConversation', () => {
|
||||
createdAt: '2026-07-03T00:00:00.000Z',
|
||||
};
|
||||
vi.mocked(client.confirmToolCall).mockResolvedValue(undefined);
|
||||
const onCanvasRefreshRequested = vi.fn();
|
||||
const onConfirmSent = vi.fn();
|
||||
const { result } = renderHook(() =>
|
||||
useEditorAgentConversation({
|
||||
projectId: 'project-1',
|
||||
client,
|
||||
onCanvasRefreshRequested,
|
||||
onConfirmSent,
|
||||
}),
|
||||
);
|
||||
@@ -981,7 +1007,7 @@ describe('useEditorAgentConversation', () => {
|
||||
expect(result.current.messages[1]?.toolCall?.externalJobId).toBe(
|
||||
'task-edit-1',
|
||||
);
|
||||
expect(onCanvasRefreshRequested).not.toHaveBeenCalled();
|
||||
expect(refreshCanvasMock).not.toHaveBeenCalled();
|
||||
expect(onConfirmSent).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -1109,12 +1135,10 @@ describe('useEditorAgentConversation', () => {
|
||||
createdAt: '2026-07-03T00:00:00.000Z',
|
||||
updatedAt: '2026-07-03T00:00:00.000Z',
|
||||
});
|
||||
const onCanvasRefreshRequested = vi.fn();
|
||||
const { result } = renderHook(() =>
|
||||
useEditorAgentConversation({
|
||||
projectId: 'project-1',
|
||||
client,
|
||||
onCanvasRefreshRequested,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1135,7 +1159,7 @@ describe('useEditorAgentConversation', () => {
|
||||
);
|
||||
expect(result.current.messages).toHaveLength(1);
|
||||
expect(result.current.messages[0]?.toolCall?.status).toBe('cancelled');
|
||||
expect(onCanvasRefreshRequested).not.toHaveBeenCalled();
|
||||
expect(refreshCanvasMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rethrows fetch errors and rolls back the optimistic message', async () => {
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
EditorAgentMessageRequest,
|
||||
EditorAgentMessageResponse,
|
||||
} from '@/packages/shared/src/contracts';
|
||||
import { useImageCanvasActions } from '@/src/components/image-editor/ImageCanvasActionsContext.ts';
|
||||
|
||||
import {
|
||||
cancelEditorAgentToolCall,
|
||||
@@ -47,7 +48,6 @@ export type EditorAgentConversationClient = {
|
||||
type UseEditorAgentConversationOptions = {
|
||||
projectId?: string | null;
|
||||
client?: EditorAgentConversationClient;
|
||||
onCanvasRefreshRequested?: () => void;
|
||||
onConfirmSent?: () => void;
|
||||
};
|
||||
|
||||
@@ -135,9 +135,9 @@ function upsertConversationSummary(
|
||||
export function useEditorAgentConversation({
|
||||
projectId,
|
||||
client = defaultEditorAgentConversationClient,
|
||||
onCanvasRefreshRequested,
|
||||
onConfirmSent,
|
||||
}: UseEditorAgentConversationOptions) {
|
||||
const { refreshCanvas } = useImageCanvasActions();
|
||||
const normalizedProjectId = projectId?.trim() ?? '';
|
||||
const [conversations, setConversations] = useState<
|
||||
EditorAgentConversationSummary[]
|
||||
@@ -424,10 +424,10 @@ export function useEditorAgentConversation({
|
||||
);
|
||||
})
|
||||
) {
|
||||
onCanvasRefreshRequested?.();
|
||||
refreshCanvas();
|
||||
}
|
||||
},
|
||||
[onCanvasRefreshRequested],
|
||||
[refreshCanvas],
|
||||
);
|
||||
|
||||
const applyDeltaMessages = useCallback(
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
useImageCanvasActions,
|
||||
} from './ImageCanvasActionsContext';
|
||||
import { ImageCanvasActionsProvider } from './ImageCanvasActionsProvider';
|
||||
|
||||
describe('ImageCanvasActionsContext', () => {
|
||||
it('exposes the editor-scoped canvas actions', () => {
|
||||
const refreshCanvas = vi.fn();
|
||||
const { result } = renderHook(useImageCanvasActions, {
|
||||
wrapper: ({ children }: { children: ReactNode }) => (
|
||||
<ImageCanvasActionsProvider refreshCanvas={refreshCanvas}>
|
||||
{children}
|
||||
</ImageCanvasActionsProvider>
|
||||
),
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.refreshCanvas();
|
||||
});
|
||||
|
||||
expect(refreshCanvas).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('fails clearly when consumed outside the editor provider', () => {
|
||||
expect(() => renderHook(useImageCanvasActions)).toThrow(
|
||||
'useImageCanvasActions must be used within ImageCanvasActionsProvider',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
export type ImageCanvasActions = {
|
||||
refreshCanvas: () => void;
|
||||
};
|
||||
|
||||
export const ImageCanvasActionsContext =
|
||||
createContext<ImageCanvasActions | null>(null);
|
||||
|
||||
export function useImageCanvasActions() {
|
||||
const actions = useContext(ImageCanvasActionsContext);
|
||||
if (!actions) {
|
||||
throw new Error(
|
||||
'useImageCanvasActions must be used within ImageCanvasActionsProvider',
|
||||
);
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { type ReactNode, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
type ImageCanvasActions,
|
||||
ImageCanvasActionsContext,
|
||||
} from './ImageCanvasActionsContext';
|
||||
|
||||
export function ImageCanvasActionsProvider({
|
||||
children,
|
||||
refreshCanvas,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
refreshCanvas: ImageCanvasActions['refreshCanvas'];
|
||||
}) {
|
||||
const actions = useMemo<ImageCanvasActions>(
|
||||
() => ({ refreshCanvas }),
|
||||
[refreshCanvas],
|
||||
);
|
||||
|
||||
return (
|
||||
<ImageCanvasActionsContext.Provider value={actions}>
|
||||
{children}
|
||||
</ImageCanvasActionsContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,15 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react';
|
||||
import { createRef } from 'react';
|
||||
import {
|
||||
fireEvent,
|
||||
render as testingLibraryRender,
|
||||
screen,
|
||||
within,
|
||||
} from '@testing-library/react';
|
||||
import { createRef, type ReactElement, type ReactNode } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { ImageCanvasActionsProvider } from './ImageCanvasActionsProvider';
|
||||
import { ImageCanvasEditorShellView } from './ImageCanvasEditorShellView';
|
||||
import type { CanvasLayer } from './ImageCanvasEditorTypes';
|
||||
import type { ImageCanvasMetadataModalViewProps } from './ImageCanvasMetadataModalView';
|
||||
@@ -11,6 +17,16 @@ import type { ImageCanvasSidebarViewProps } from './ImageCanvasSidebarView';
|
||||
import type { ImageCanvasStageViewProps } from './ImageCanvasStageView';
|
||||
import type { ImageCanvasTopbarViewProps } from './ImageCanvasTopbarView';
|
||||
|
||||
function render(ui: ReactElement) {
|
||||
return testingLibraryRender(ui, {
|
||||
wrapper: ({ children }: { children: ReactNode }) => (
|
||||
<ImageCanvasActionsProvider refreshCanvas={vi.fn()}>
|
||||
{children}
|
||||
</ImageCanvasActionsProvider>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
|
||||
return {
|
||||
id: 'layer-1',
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
PlatformRechargePaymentResultDialog,
|
||||
} from '../platform-entry/PlatformRechargePaymentStatusDialogs';
|
||||
import { usePlatformProfileCenterController } from '../platform-entry/usePlatformProfileCenterController';
|
||||
import { ImageCanvasActionsProvider } from './ImageCanvasActionsProvider';
|
||||
import {
|
||||
canvasAssetKindOrNull,
|
||||
DEFAULT_CANVAS_BACKGROUND_COLOR,
|
||||
@@ -1195,7 +1196,7 @@ export function ImageCanvasEditorView({
|
||||
},
|
||||
[applyProjectSnapshot, captureCanvasHistory, refreshAssetLibrary],
|
||||
);
|
||||
const handleEditorAgentCanvasRefreshRequested = useCallback(() => {
|
||||
const refreshCanvas = useCallback(() => {
|
||||
if (!projectId) {
|
||||
return;
|
||||
}
|
||||
@@ -1469,10 +1470,6 @@ export function ImageCanvasEditorView({
|
||||
const handleEditorAgentConfirmSent = useCallback(() => {
|
||||
generationSurface.refreshTaskList();
|
||||
}, [generationSurface]);
|
||||
const handleEditorAgentJobCompleted = useCallback(() => {
|
||||
generationSurface.refreshTaskList();
|
||||
handleEditorAgentCanvasRefreshRequested();
|
||||
}, [generationSurface, handleEditorAgentCanvasRefreshRequested]);
|
||||
const showGenerationWarning = generationSurface.showGenerationWarning;
|
||||
const handleExternalGenerationTasksCompleted = useCallback(
|
||||
(tasks: ExternalGenerationTaskRecord[]) => {
|
||||
@@ -1486,12 +1483,8 @@ export function ImageCanvasEditorView({
|
||||
showGenerationWarning(warning);
|
||||
}
|
||||
refreshEditorWalletState();
|
||||
void loadEditorProject(projectId)
|
||||
.then(applyGeneratedProjectSnapshot)
|
||||
.catch(() => undefined);
|
||||
},
|
||||
[
|
||||
applyGeneratedProjectSnapshot,
|
||||
projectId,
|
||||
refreshEditorWalletState,
|
||||
showGenerationWarning,
|
||||
@@ -2400,7 +2393,6 @@ export function ImageCanvasEditorView({
|
||||
onActivateGenerationDialog: activateCanvasGenerationDialog,
|
||||
onFocusExternalTask: focusExternalGenerationTask,
|
||||
onExternalTasksCompleted: handleExternalGenerationTasksCompleted,
|
||||
onEditorAgentCanvasRefreshRequested: handleEditorAgentJobCompleted,
|
||||
onEditorAgentConfirmSent: handleEditorAgentConfirmSent,
|
||||
onToggleTaskSidebar: toggleTaskSidebar,
|
||||
onToggleAgentConversation: toggleAgentConversation,
|
||||
@@ -2465,7 +2457,7 @@ export function ImageCanvasEditorView({
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ImageCanvasActionsProvider refreshCanvas={refreshCanvas}>
|
||||
<ImageCanvasEditorShellView
|
||||
editorRootRef={editorRootRef}
|
||||
uploadInputRef={uploadInputRef}
|
||||
@@ -2539,7 +2531,7 @@ export function ImageCanvasEditorView({
|
||||
orderId={wechatRechargeOrderConfirmationState.orderId}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
</ImageCanvasActionsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -134,7 +134,6 @@ export type ImageCanvasStageViewProps = {
|
||||
onActivateGenerationDialog: (dialog: CanvasGenerationDialogState) => void;
|
||||
onFocusExternalTask: (task: ExternalGenerationTaskRecord) => void;
|
||||
onExternalTasksCompleted?: (tasks: ExternalGenerationTaskRecord[]) => void;
|
||||
onEditorAgentCanvasRefreshRequested?: () => void;
|
||||
onEditorAgentConfirmSent?: () => void;
|
||||
onToggleTaskSidebar: () => void;
|
||||
onToggleAgentConversation: () => void;
|
||||
@@ -271,7 +270,6 @@ export function ImageCanvasStageView({
|
||||
onActivateGenerationDialog,
|
||||
onFocusExternalTask,
|
||||
onExternalTasksCompleted,
|
||||
onEditorAgentCanvasRefreshRequested,
|
||||
onEditorAgentConfirmSent,
|
||||
onToggleTaskSidebar,
|
||||
onToggleAgentConversation,
|
||||
@@ -512,7 +510,6 @@ export function ImageCanvasStageView({
|
||||
onToggleOpen={onToggleAgentConversation}
|
||||
layers={layers}
|
||||
assets={editorAgentAssets}
|
||||
onCanvasRefreshRequested={onEditorAgentCanvasRefreshRequested}
|
||||
onConfirmSent={onEditorAgentConfirmSent}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -3,17 +3,31 @@
|
||||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
render as testingLibraryRender,
|
||||
screen,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import type { ReactElement, ReactNode } from 'react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { ExternalGenerationTaskRecord } from '../../../packages/shared/src/contracts/externalGeneration';
|
||||
import { listExternalGenerationTasks } from '../../services/external-generation';
|
||||
import { ImageCanvasActionsProvider } from './ImageCanvasActionsProvider.tsx';
|
||||
import { ImageCanvasTaskSidebarView } from './ImageCanvasTaskSidebarView';
|
||||
import { useImageCanvasContextStore } from './useImageCanvasContextStore.ts';
|
||||
|
||||
const refreshCanvasMock = vi.fn();
|
||||
|
||||
function render(ui: ReactElement) {
|
||||
return testingLibraryRender(ui, {
|
||||
wrapper: ({ children }: { children: ReactNode }) => (
|
||||
<ImageCanvasActionsProvider refreshCanvas={refreshCanvasMock}>
|
||||
{children}
|
||||
</ImageCanvasActionsProvider>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
vi.mock('../../services/external-generation', () => ({
|
||||
listExternalGenerationTasks: vi.fn().mockResolvedValue({
|
||||
overview: {
|
||||
@@ -30,6 +44,7 @@ const listExternalGenerationTasksMock = vi.mocked(listExternalGenerationTasks);
|
||||
|
||||
beforeEach(() => {
|
||||
useImageCanvasContextStore.getState().setProjectId('project-1');
|
||||
refreshCanvasMock.mockReset();
|
||||
listExternalGenerationTasksMock.mockClear();
|
||||
listExternalGenerationTasksMock.mockResolvedValue({
|
||||
overview: {
|
||||
@@ -448,6 +463,7 @@ describe('ImageCanvasTaskSidebarView', () => {
|
||||
}),
|
||||
]);
|
||||
expect(onExternalTasksCompleted).toHaveBeenCalledTimes(1);
|
||||
expect(refreshCanvasMock).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
|
||||
import type { ExternalGenerationTaskRecord } from '../../../packages/shared/src/contracts/externalGeneration';
|
||||
import { listExternalGenerationTasks } from '../../services/external-generation';
|
||||
import { useImageCanvasActions } from './ImageCanvasActionsContext.ts';
|
||||
import { EditorIconButton } from './ImageCanvasEditorPrimitives';
|
||||
import type { CanvasTaskStatus } from './ImageCanvasEditorTypes';
|
||||
import { useImageCanvasContextStore } from './useImageCanvasContextStore.ts';
|
||||
@@ -280,6 +281,7 @@ export function ImageCanvasTaskSidebarView({
|
||||
onFocusExternalTask,
|
||||
onExternalTasksCompleted,
|
||||
}: ImageCanvasTaskSidebarViewProps) {
|
||||
const { refreshCanvas } = useImageCanvasActions();
|
||||
const projectId = useImageCanvasContextStore((state) => state.projectId);
|
||||
const normalizedProjectId = projectId?.trim() ?? '';
|
||||
const [activeTab, setActiveTab] = useState<TaskSidebarTab>('active');
|
||||
@@ -334,9 +336,10 @@ export function ImageCanvasTaskSidebarView({
|
||||
for (const task of newlyCompletedTasks) {
|
||||
completedRefreshNotifiedTaskIdsRef.current.add(task.jobId);
|
||||
}
|
||||
refreshCanvas();
|
||||
onExternalTasksCompleted?.(newlyCompletedTasks);
|
||||
},
|
||||
[onExternalTasksCompleted],
|
||||
[onExternalTasksCompleted, refreshCanvas],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user