merge(feature/curious-star): integrate worktree changes into master
This commit is contained in:
@@ -223,7 +223,7 @@ describe("useCanvasEdgeInsertions", () => {
|
||||
label: "Prompt",
|
||||
width: 320,
|
||||
height: 220,
|
||||
defaultData: { prompt: "", model: "", aspectRatio: "1:1" },
|
||||
defaultData: { prompt: "", aspectRatio: "1:1" },
|
||||
} as CanvasNodeTemplate);
|
||||
});
|
||||
|
||||
@@ -268,7 +268,7 @@ describe("useCanvasEdgeInsertions", () => {
|
||||
label: "Prompt",
|
||||
width: 320,
|
||||
height: 220,
|
||||
defaultData: { prompt: "", model: "", aspectRatio: "1:1" },
|
||||
defaultData: { prompt: "", aspectRatio: "1:1" },
|
||||
} as CanvasNodeTemplate);
|
||||
});
|
||||
|
||||
@@ -319,7 +319,7 @@ describe("useCanvasEdgeInsertions", () => {
|
||||
label: "Prompt",
|
||||
width: 320,
|
||||
height: 220,
|
||||
defaultData: { prompt: "", model: "", aspectRatio: "1:1" },
|
||||
defaultData: { prompt: "", aspectRatio: "1:1" },
|
||||
} as CanvasNodeTemplate);
|
||||
});
|
||||
|
||||
@@ -332,7 +332,7 @@ describe("useCanvasEdgeInsertions", () => {
|
||||
height: 220,
|
||||
data: {
|
||||
prompt: "",
|
||||
model: "",
|
||||
model: "google/gemini-2.5-flash-image",
|
||||
aspectRatio: "1:1",
|
||||
canvasId: "canvas-1",
|
||||
},
|
||||
@@ -650,7 +650,7 @@ describe("useCanvasEdgeInsertions", () => {
|
||||
label: "Prompt",
|
||||
width: 320,
|
||||
height: 220,
|
||||
defaultData: { prompt: "", model: "", aspectRatio: "1:1" },
|
||||
defaultData: { prompt: "", model: "google/gemini-2.5-flash-image", aspectRatio: "1:1" },
|
||||
} as CanvasNodeTemplate);
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
type ReactNode,
|
||||
@@ -84,12 +83,6 @@ export function CanvasGraphProvider({
|
||||
[nodes, previewNodeDataOverrides],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (prunedPreviewNodeDataOverrides !== previewNodeDataOverrides) {
|
||||
setPreviewNodeDataOverrides(prunedPreviewNodeDataOverrides);
|
||||
}
|
||||
}, [previewNodeDataOverrides, prunedPreviewNodeDataOverrides]);
|
||||
|
||||
const graph = useMemo(
|
||||
() =>
|
||||
buildGraphSnapshot(nodes, edges, {
|
||||
|
||||
@@ -18,7 +18,11 @@ import BaseNodeWrapper from "./base-node-wrapper";
|
||||
import { useCanvasPlacement } from "@/components/canvas/canvas-placement-context";
|
||||
import { useCanvasSync } from "@/components/canvas/canvas-sync-context";
|
||||
import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
|
||||
import { DEFAULT_MODEL_ID, getModel } from "@/lib/ai-models";
|
||||
import {
|
||||
DEFAULT_MODEL_ID,
|
||||
getAvailableImageModels,
|
||||
getModel,
|
||||
} from "@/lib/ai-models";
|
||||
import {
|
||||
DEFAULT_ASPECT_RATIO,
|
||||
getAiImageNodeOuterSize,
|
||||
@@ -40,6 +44,7 @@ import { Sparkles, Loader2, Coins } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "@/lib/toast";
|
||||
import { classifyError } from "@/lib/ai-errors";
|
||||
import { normalizePublicTier } from "@/lib/tier-credits";
|
||||
|
||||
type PromptNodeData = {
|
||||
prompt?: string;
|
||||
@@ -63,6 +68,7 @@ export default function PromptNode({
|
||||
const { getEdges, getNode } = useReactFlow();
|
||||
|
||||
const [prompt, setPrompt] = useState(nodeData.prompt ?? "");
|
||||
const [modelId, setModelId] = useState(nodeData.model ?? DEFAULT_MODEL_ID);
|
||||
const [aspectRatio, setAspectRatio] = useState(
|
||||
nodeData.aspectRatio ?? DEFAULT_ASPECT_RATIO
|
||||
);
|
||||
@@ -72,14 +78,20 @@ export default function PromptNode({
|
||||
const nodes = useStore((store) => store.nodes);
|
||||
|
||||
const promptRef = useRef(prompt);
|
||||
const modelIdRef = useRef(modelId);
|
||||
const aspectRatioRef = useRef(aspectRatio);
|
||||
promptRef.current = prompt;
|
||||
modelIdRef.current = modelId;
|
||||
aspectRatioRef.current = aspectRatio;
|
||||
|
||||
useEffect(() => {
|
||||
setPrompt(nodeData.prompt ?? "");
|
||||
}, [nodeData.prompt]);
|
||||
|
||||
useEffect(() => {
|
||||
setModelId(nodeData.model ?? DEFAULT_MODEL_ID);
|
||||
}, [nodeData.model]);
|
||||
|
||||
useEffect(() => {
|
||||
setAspectRatio(nodeData.aspectRatio ?? DEFAULT_ASPECT_RATIO);
|
||||
}, [nodeData.aspectRatio]);
|
||||
@@ -113,7 +125,29 @@ export default function PromptNode({
|
||||
dataRef.current = data;
|
||||
|
||||
const balance = useAuthQuery(api.credits.getBalance);
|
||||
const creditCost = getModel(DEFAULT_MODEL_ID)?.creditCost ?? 4;
|
||||
const subscription = useAuthQuery(api.credits.getSubscription);
|
||||
const userTier = normalizePublicTier(subscription?.tier ?? "free");
|
||||
const availableModels = useMemo(
|
||||
() => getAvailableImageModels(userTier),
|
||||
[userTier],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (availableModels.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!availableModels.some((model) => model.id === modelId)) {
|
||||
setModelId(availableModels[0]!.id);
|
||||
}
|
||||
}, [availableModels, modelId]);
|
||||
|
||||
const selectedModel =
|
||||
getModel(modelId) ??
|
||||
availableModels[0] ??
|
||||
getModel(DEFAULT_MODEL_ID);
|
||||
const resolvedModelId = selectedModel?.id ?? DEFAULT_MODEL_ID;
|
||||
const creditCost = selectedModel?.creditCost ?? 4;
|
||||
|
||||
const availableCredits =
|
||||
balance !== undefined ? balance.balance - balance.reserved : null;
|
||||
@@ -134,6 +168,7 @@ export default function PromptNode({
|
||||
data: {
|
||||
...rest,
|
||||
prompt: promptRef.current,
|
||||
model: modelIdRef.current,
|
||||
aspectRatio: aspectRatioRef.current,
|
||||
},
|
||||
});
|
||||
@@ -156,6 +191,14 @@ export default function PromptNode({
|
||||
[debouncedSave]
|
||||
);
|
||||
|
||||
const handleModelChange = useCallback(
|
||||
(value: string) => {
|
||||
setModelId(value);
|
||||
debouncedSave();
|
||||
},
|
||||
[debouncedSave],
|
||||
);
|
||||
|
||||
const handleGenerate = useCallback(async () => {
|
||||
if (!effectivePrompt.trim() || isGenerating) return;
|
||||
if (status.isOffline) {
|
||||
@@ -229,8 +272,8 @@ export default function PromptNode({
|
||||
height: outer.height,
|
||||
data: {
|
||||
prompt: promptToUse,
|
||||
model: DEFAULT_MODEL_ID,
|
||||
modelTier: "standard",
|
||||
model: resolvedModelId,
|
||||
modelTier: selectedModel?.tier ?? "standard",
|
||||
canvasId,
|
||||
aspectRatio,
|
||||
outputWidth: viewport.width,
|
||||
@@ -249,7 +292,7 @@ export default function PromptNode({
|
||||
prompt: promptToUse,
|
||||
referenceStorageId,
|
||||
referenceImageUrl,
|
||||
model: DEFAULT_MODEL_ID,
|
||||
model: resolvedModelId,
|
||||
aspectRatio,
|
||||
}),
|
||||
{
|
||||
@@ -285,6 +328,7 @@ export default function PromptNode({
|
||||
prompt,
|
||||
effectivePrompt,
|
||||
aspectRatio,
|
||||
resolvedModelId,
|
||||
isGenerating,
|
||||
nodeData.canvasId,
|
||||
id,
|
||||
@@ -292,6 +336,7 @@ export default function PromptNode({
|
||||
getNode,
|
||||
createNodeConnectedFromSource,
|
||||
generateImage,
|
||||
selectedModel?.tier,
|
||||
creditCost,
|
||||
availableCredits,
|
||||
hasEnoughCredits,
|
||||
@@ -338,6 +383,31 @@ export default function PromptNode({
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label
|
||||
htmlFor={`prompt-model-${id}`}
|
||||
className="text-[11px] font-medium text-muted-foreground"
|
||||
>
|
||||
Modell
|
||||
</Label>
|
||||
<Select value={resolvedModelId} onValueChange={handleModelChange}>
|
||||
<SelectTrigger
|
||||
id={`prompt-model-${id}`}
|
||||
className="nodrag nowheel w-full"
|
||||
size="sm"
|
||||
>
|
||||
<SelectValue placeholder="Modell" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="nodrag">
|
||||
{availableModels.map((model) => (
|
||||
<SelectItem key={model.id} value={model.id}>
|
||||
{model.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label
|
||||
htmlFor={`prompt-format-${id}`}
|
||||
|
||||
@@ -15,7 +15,6 @@ export interface OpenRouterModel {
|
||||
|
||||
const IMAGE_AND_TEXT_MODALITIES = ["image", "text"] as const;
|
||||
const IMAGE_ONLY_MODALITIES = ["image"] as const;
|
||||
|
||||
export const IMAGE_MODELS: Record<string, OpenRouterModel> = {
|
||||
"google/gemini-2.5-flash-image": {
|
||||
id: "google/gemini-2.5-flash-image",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef, useCallback, useEffect } from "react";
|
||||
import { useRef, useCallback, useEffect, useMemo } from "react";
|
||||
|
||||
type DebouncedCallback<Args extends unknown[]> = ((...args: Args) => void) & {
|
||||
flush: () => void;
|
||||
@@ -53,8 +53,8 @@ export function useDebouncedCallback<Args extends unknown[]>(
|
||||
}
|
||||
}, []);
|
||||
|
||||
const debouncedFn = useCallback(
|
||||
(...args: Args) => {
|
||||
return useMemo(() => {
|
||||
const debouncedCallback = ((...args: Args) => {
|
||||
argsRef.current = args;
|
||||
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
@@ -65,13 +65,11 @@ export function useDebouncedCallback<Args extends unknown[]>(
|
||||
callbackRef.current(...nextArgs);
|
||||
}
|
||||
}, delay);
|
||||
},
|
||||
[delay],
|
||||
);
|
||||
}) as DebouncedCallback<Args>;
|
||||
|
||||
const debouncedCallback = debouncedFn as DebouncedCallback<Args>;
|
||||
debouncedCallback.flush = flush;
|
||||
debouncedCallback.cancel = cancel;
|
||||
|
||||
return debouncedCallback;
|
||||
}, [cancel, delay, flush]);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export const CANVAS_NODE_TEMPLATES = [
|
||||
label: "Prompt",
|
||||
width: 320,
|
||||
height: 220,
|
||||
defaultData: { prompt: "", model: "", aspectRatio: "1:1" },
|
||||
defaultData: { prompt: "", model: "google/gemini-2.5-flash-image", aspectRatio: "1:1" },
|
||||
},
|
||||
{
|
||||
type: "video-prompt",
|
||||
|
||||
@@ -235,7 +235,11 @@ export const NODE_DEFAULTS: Record<
|
||||
> = {
|
||||
image: { width: 280, height: 200, data: {} },
|
||||
text: { width: 256, height: 120, data: { content: "" } },
|
||||
prompt: { width: 288, height: 220, data: { prompt: "", aspectRatio: "1:1" } },
|
||||
prompt: {
|
||||
width: 288,
|
||||
height: 220,
|
||||
data: { prompt: "", model: "google/gemini-2.5-flash-image", aspectRatio: "1:1" },
|
||||
},
|
||||
"video-prompt": {
|
||||
width: 288,
|
||||
height: 220,
|
||||
|
||||
@@ -38,8 +38,10 @@ function HookHarness(props: HarnessProps) {
|
||||
const nodesRef = useRef<RFNode[]>(props.liveNodes ?? props.nodes);
|
||||
const edgesRef = useRef<RFEdge[]>(props.liveEdges ?? props.edges);
|
||||
|
||||
useEffect(() => {
|
||||
nodesRef.current = props.liveNodes ?? props.nodes;
|
||||
edgesRef.current = props.liveEdges ?? props.edges;
|
||||
}, [props.liveEdges, props.liveNodes, props.edges, props.nodes]);
|
||||
|
||||
const handlers = useCanvasDeleteHandlers({
|
||||
t: ((key: string, values?: Record<string, unknown>) =>
|
||||
|
||||
255
tests/prompt-node.test.ts
Normal file
255
tests/prompt-node.test.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import React from "react";
|
||||
import { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { Id } from "@/convex/_generated/dataModel";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
edges: [] as Array<{ source: string; target: string }>,
|
||||
nodes: [] as Array<{ id: string; type: string; data: Record<string, unknown> }>,
|
||||
balance: { balance: 100, reserved: 0 } as { balance: number; reserved: number } | undefined,
|
||||
subscription: { tier: "starter" as const },
|
||||
queueNodeDataUpdate: vi.fn(async () => undefined),
|
||||
createNodeConnectedFromSource: vi.fn(async () => "ai-image-node-1" as Id<"nodes">),
|
||||
generateImage: vi.fn(async () => ({ queued: true, nodeId: "ai-image-node-1" })),
|
||||
getEdges: vi.fn(() => [] as Array<{ source: string; target: string }>),
|
||||
getNode: vi.fn((id: string) =>
|
||||
id === "prompt-1"
|
||||
? { id, position: { x: 100, y: 50 }, measured: { width: 280, height: 220 } }
|
||||
: null,
|
||||
),
|
||||
push: vi.fn(),
|
||||
toastPromise: vi.fn(async <T,>(promise: Promise<T>) => await promise),
|
||||
toastWarning: vi.fn(),
|
||||
toastAction: vi.fn(),
|
||||
toastError: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("next-intl", () => ({
|
||||
useTranslations: () => (key: string) => key,
|
||||
}));
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({ push: mocks.push }),
|
||||
}));
|
||||
|
||||
vi.mock("convex/react", () => ({
|
||||
useAction: () => mocks.generateImage,
|
||||
}));
|
||||
|
||||
vi.mock("@/convex/_generated/api", () => ({
|
||||
api: {
|
||||
ai: {
|
||||
generateImage: "ai.generateImage",
|
||||
},
|
||||
credits: {
|
||||
getBalance: "credits.getBalance",
|
||||
getSubscription: "credits.getSubscription",
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/use-auth-query", () => ({
|
||||
useAuthQuery: (query: string) => {
|
||||
if (query === "credits.getSubscription") return mocks.subscription;
|
||||
return mocks.balance;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/use-debounced-callback", () => ({
|
||||
useDebouncedCallback: (callback: (...args: Array<unknown>) => void) => callback,
|
||||
}));
|
||||
|
||||
vi.mock("@/components/canvas/canvas-sync-context", () => ({
|
||||
useCanvasSync: () => ({
|
||||
queueNodeDataUpdate: mocks.queueNodeDataUpdate,
|
||||
status: { isOffline: false, isSyncing: false, pendingCount: 0 },
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/canvas/canvas-placement-context", () => ({
|
||||
useCanvasPlacement: () => ({
|
||||
createNodeConnectedFromSource: mocks.createNodeConnectedFromSource,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/toast", () => ({
|
||||
toast: {
|
||||
promise: mocks.toastPromise,
|
||||
warning: mocks.toastWarning,
|
||||
action: mocks.toastAction,
|
||||
error: mocks.toastError,
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/ai-errors", () => ({
|
||||
classifyError: (error: unknown) => ({
|
||||
type: "generic",
|
||||
rawMessage: error instanceof Error ? error.message : String(error),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/ui/label", () => ({
|
||||
Label: ({ children, htmlFor }: { children: React.ReactNode; htmlFor?: string }) =>
|
||||
React.createElement("label", { htmlFor }, children),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/ui/select", () => ({
|
||||
Select: ({
|
||||
value,
|
||||
onValueChange,
|
||||
children,
|
||||
}: {
|
||||
value: string;
|
||||
onValueChange: (value: string) => void;
|
||||
children: React.ReactNode;
|
||||
}) =>
|
||||
React.createElement(
|
||||
"select",
|
||||
{
|
||||
"data-testid": value.includes("/") ? "model-select" : "format-select",
|
||||
value,
|
||||
onChange: (event: Event) => {
|
||||
onValueChange((event.target as HTMLSelectElement).value);
|
||||
},
|
||||
},
|
||||
children,
|
||||
),
|
||||
SelectTrigger: ({ children }: { children: React.ReactNode }) => children,
|
||||
SelectValue: () => null,
|
||||
SelectContent: ({ children }: { children: React.ReactNode }) => children,
|
||||
SelectItem: ({ children, value }: { children: React.ReactNode; value: string }) =>
|
||||
React.createElement("option", { value }, children),
|
||||
SelectGroup: ({ children }: { children: React.ReactNode }) => children,
|
||||
SelectLabel: ({ children }: { children: React.ReactNode }) =>
|
||||
React.createElement("optgroup", { label: String(children) }),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/canvas/nodes/base-node-wrapper", () => ({
|
||||
default: ({ children }: { children: React.ReactNode }) => React.createElement("div", null, children),
|
||||
}));
|
||||
|
||||
vi.mock("@xyflow/react", () => ({
|
||||
Handle: () => null,
|
||||
Position: { Left: "left", Right: "right" },
|
||||
useStore: (selector: (state: { edges: typeof mocks.edges; nodes: typeof mocks.nodes }) => unknown) =>
|
||||
selector({ edges: mocks.edges, nodes: mocks.nodes }),
|
||||
useReactFlow: () => ({
|
||||
getEdges: mocks.getEdges,
|
||||
getNode: mocks.getNode,
|
||||
}),
|
||||
}));
|
||||
|
||||
import PromptNode from "@/components/canvas/nodes/prompt-node";
|
||||
|
||||
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
|
||||
true;
|
||||
|
||||
describe("PromptNode", () => {
|
||||
let container: HTMLDivElement | null = null;
|
||||
let root: Root | null = null;
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.edges = [];
|
||||
mocks.nodes = [];
|
||||
mocks.balance = { balance: 100, reserved: 0 };
|
||||
mocks.subscription = { tier: "starter" };
|
||||
mocks.queueNodeDataUpdate.mockClear();
|
||||
mocks.createNodeConnectedFromSource.mockClear();
|
||||
mocks.generateImage.mockClear();
|
||||
mocks.getEdges.mockClear();
|
||||
mocks.getNode.mockClear();
|
||||
mocks.push.mockClear();
|
||||
mocks.toastPromise.mockClear();
|
||||
mocks.toastWarning.mockClear();
|
||||
mocks.toastAction.mockClear();
|
||||
mocks.toastError.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (root) {
|
||||
act(() => {
|
||||
root?.unmount();
|
||||
});
|
||||
}
|
||||
container?.remove();
|
||||
container = null;
|
||||
root = null;
|
||||
});
|
||||
|
||||
it("propagates selected image model into node creation and generation action", async () => {
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
|
||||
await act(async () => {
|
||||
root?.render(
|
||||
React.createElement(PromptNode, {
|
||||
id: "prompt-1",
|
||||
selected: false,
|
||||
dragging: false,
|
||||
draggable: true,
|
||||
selectable: true,
|
||||
deletable: true,
|
||||
zIndex: 1,
|
||||
isConnectable: true,
|
||||
type: "prompt",
|
||||
data: {
|
||||
prompt: "ein neugieriger hund im regen",
|
||||
aspectRatio: "1:1",
|
||||
canvasId: "canvas-1",
|
||||
},
|
||||
positionAbsoluteX: 0,
|
||||
positionAbsoluteY: 0,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
const modelSelect = container.querySelector('select[data-testid="model-select"]');
|
||||
if (!(modelSelect instanceof HTMLSelectElement)) {
|
||||
throw new Error("Model select not found");
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
modelSelect.value = "openai/gpt-5-image-mini";
|
||||
modelSelect.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
});
|
||||
|
||||
const button = Array.from(container.querySelectorAll("button")).find((element) =>
|
||||
element.textContent?.includes("Bild generieren"),
|
||||
);
|
||||
|
||||
if (!(button instanceof HTMLButtonElement)) {
|
||||
throw new Error("Generate button not found");
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
button.click();
|
||||
});
|
||||
|
||||
expect(mocks.createNodeConnectedFromSource).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.createNodeConnectedFromSource).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: "ai-image",
|
||||
sourceNodeId: "prompt-1",
|
||||
data: expect.objectContaining({
|
||||
model: "openai/gpt-5-image-mini",
|
||||
modelTier: "premium",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mocks.generateImage).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.generateImage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
canvasId: "canvas-1",
|
||||
nodeId: "ai-image-node-1",
|
||||
prompt: "ein neugieriger hund im regen",
|
||||
model: "openai/gpt-5-image-mini",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user