Browse Source

Merge branch 'master' into sync-test-s

feature/canvas-chatbot-copilot
jiajia 2 months ago
parent
commit
9c050bc37b
  1. 27
      src/components/__tests__/GenerateImageNode.test.tsx
  2. 4
      src/components/nodes/GenerateImageNode.tsx
  3. 39
      src/store/execution/__tests__/nanoBananaExecutor.test.ts
  4. 4
      src/store/execution/nanoBananaExecutor.ts
  5. 3
      src/types/nodes.ts

27
src/components/__tests__/GenerateImageNode.test.tsx

@ -379,6 +379,33 @@ describe("GenerateImageNode", () => {
expect(screen.getByText("2 / 3")).toBeInTheDocument();
});
it("should switch carousel images using inline history data without fetching", async () => {
render(
<TestWrapper>
<GenerateImageNode {...createNodeProps({
outputImage: "data:image/png;base64,current",
imageHistory: [
{ id: "img1", image: "data:image/png;base64,current", timestamp: Date.now(), prompt: "test1", aspectRatio: "1:1", model: "nano-banana" },
{ id: "img2", image: "data:image/png;base64,next", timestamp: Date.now(), prompt: "test2", aspectRatio: "1:1", model: "nano-banana" },
],
selectedHistoryIndex: 0,
})} />
</TestWrapper>
);
fireEvent.click(screen.getByTitle("Next image"));
await waitFor(() => {
expect(mockUpdateNodeData).toHaveBeenCalledWith("test-node-1", {
outputImage: "data:image/png;base64,next",
selectedHistoryIndex: 1,
status: "idle",
error: null,
});
});
expect(mockFetch).not.toHaveBeenCalledWith("/api/load-generation", expect.anything());
});
});
describe("Legacy Data Migration", () => {

4
src/components/nodes/GenerateImageNode.tsx

@ -371,7 +371,7 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
const imageItem = history[newIndex];
setIsLoadingCarouselImage(true);
const image = await loadImageById(imageItem.id);
const image = imageItem.image ?? await loadImageById(imageItem.id);
setIsLoadingCarouselImage(false);
if (image) {
@ -393,7 +393,7 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
const imageItem = history[newIndex];
setIsLoadingCarouselImage(true);
const image = await loadImageById(imageItem.id);
const image = imageItem.image ?? await loadImageById(imageItem.id);
setIsLoadingCarouselImage(false);
if (image) {

39
src/store/execution/__tests__/nanoBananaExecutor.test.ts

@ -232,6 +232,45 @@ describe("executeNanoBanana", () => {
expect((completeCall![1] as Record<string, unknown>).outputImage).toBe("data:image/png;base64,result");
});
it("should keep generated image data in node carousel history for immediate switching", async () => {
const node = makeNode();
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ success: true, image: "data:image/png;base64,result" }),
});
const ctx = makeCtx(node);
await executeNanoBanana(ctx);
const completeCall = (ctx.updateNodeData as ReturnType<typeof vi.fn>).mock.calls.find(
(c: unknown[]) => (c[1] as Record<string, unknown>).status === "complete"
);
expect(completeCall?.[1]).toMatchObject({
imageHistory: [
expect.objectContaining({
image: "data:image/png;base64,result",
}),
],
});
});
it("should not call server save-generation for browser-local generation paths", async () => {
const node = makeNode();
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ success: true, image: "data:image/png;base64,result" }),
});
const ctx = makeCtx(node, {
generationsPath: "browserfs://root/project/generations",
});
await executeNanoBanana(ctx);
expect(mockFetch).toHaveBeenCalledTimes(1);
expect(mockFetch).toHaveBeenCalledWith("/api/generate", expect.anything());
expect(ctx.trackSaveGeneration).not.toHaveBeenCalled();
});
it("should add to global history on success", async () => {
const node = makeNode();
mockFetch.mockResolvedValueOnce({

4
src/store/execution/nanoBananaExecutor.ts

@ -19,6 +19,7 @@ import {
import { pollGenerateTask } from "./pollTaskCompletion";
import { runWithFallback } from "./runWithFallback";
import type { NodeExecutionContext } from "./types";
import { isBrowserFileSystemPath } from "@/utils/browserFileSystem";
export interface NanoBananaOptions {
/** When true, falls back to stored inputImages/inputPrompt if no connections provide them. */
@ -196,6 +197,7 @@ export async function executeNanoBanana(
// Add to node's carousel history
const newHistoryItem = {
id: imageId,
image: result.image,
timestamp,
prompt: finalPrompt,
aspectRatio: nodeData.aspectRatio,
@ -235,7 +237,7 @@ export async function executeNanoBanana(
}
// Auto-save to generations folder if configured
if (generationsPath) {
if (generationsPath && !isBrowserFileSystemPath(generationsPath)) {
const savePromise = fetch("/api/save-generation", {
method: "POST",
headers: { "Content-Type": "application/json" },

3
src/types/nodes.ts

@ -151,10 +151,11 @@ export interface ImageHistoryItem {
}
/**
* Carousel image item for per-node history (IDs only, images stored externally)
* Carousel image item for per-node history.
*/
export interface CarouselImageItem {
id: string;
image?: string; // Current-session fallback; stripped when externalizing workflow media
timestamp: number;
prompt: string;
aspectRatio: AspectRatio;

Loading…
Cancel
Save