refactor: Update Blank Flow Name (#9089)

* refactor: Standardize import statements and improve code formatting in reactflowUtils.ts

* [autofix.ci] apply automated fixes

* refactor: Standardize string quotes and improve test readability across multiple test files

- Updated string quotes from double to single in various test files for consistency.
- Enhanced test readability by replacing specific text selectors with test IDs.
- Adjusted wait conditions and element interactions to align with updated selectors.
- Ensured all tests maintain functionality while improving code clarity.

* [autofix.ci] apply automated fixes

* test: Add comprehensive Jest tests for createNewFlow function

- Introduced a new test file for the createNewFlow function to validate its behavior.
- Covered various scenarios including default value handling, flow parameter processing, edge cases, and special property handling.
- Ensured immutability of input parameters and consistency of output for the same inputs.
- Mocked dependencies to isolate the function's logic and improve test reliability.

* [autofix.ci] apply automated fixes

* test: Standardize string quotes and improve readability in auto-save and MCP server tests

- Updated string quotes from double to single for consistency across test files.
- Enhanced test readability by utilizing test IDs for element selection.
- Adjusted wait conditions and interactions to align with updated selectors while maintaining test functionality.

* [autofix.ci] apply automated fixes

* chore: update ESLint configuration and improve test selectors in auto-save-off.spec.ts

- Simplified ESLint configuration by removing unnecessary plugins and options.
- Enhanced test selectors in auto-save-off.spec.ts to use test IDs for better reliability and clarity.

* chore: enhance ESLint configuration with Prettier integration and improved rules

- Added Prettier as a plugin to the ESLint configuration for better code formatting.
- Updated parser options and extended rules for improved code quality and consistency.
- Ensured compatibility with TypeScript and React environments.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Deon Sanchez 2025-07-28 10:13:40 -06:00 committed by GitHub
commit 51715ea0c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 513 additions and 18 deletions

View file

@ -0,0 +1,479 @@
/**
* Jest test for the createNewFlow function from utils/reactflowUtils.ts
*
* This test file reimplements the function inline to avoid complex dependency issues
* with imports that use import.meta and other Node.js-specific features that Jest
* has trouble parsing in the jsdom environment.
*
* The test verifies all aspects of the createNewFlow function behavior, including:
* - Default value handling
* - Flow parameter processing
* - Edge cases and nullish coalescing behavior
* - Immutability and consistency
*/
import type { ReactFlowJsonObject } from "@xyflow/react";
// Define minimal types needed for testing
type FlowType = {
name: string;
id: string;
data: ReactFlowJsonObject<any, any> | null;
description: string;
endpoint_name?: string | null;
is_component?: boolean;
icon?: string;
gradient?: string;
tags?: string[];
folder_id?: string;
mcp_enabled?: boolean;
};
// Mock the getRandomDescription function
const mockGetRandomDescription = jest.fn(() => "Random Description");
// Define the function to test directly
const createNewFlow = (
flowData: ReactFlowJsonObject<any, any>,
folderId: string,
flow?: FlowType,
) => {
return {
description: flow?.description ?? mockGetRandomDescription(),
name: flow?.name ? flow.name : "New Flow",
data: flowData,
id: "",
icon: flow?.icon ?? undefined,
gradient: flow?.gradient ?? undefined,
is_component: flow?.is_component ?? false,
folder_id: folderId,
endpoint_name: flow?.endpoint_name ?? undefined,
tags: flow?.tags ?? [],
mcp_enabled: true,
};
};
describe("createNewFlow", () => {
// Mock data setup
const mockFlowData: ReactFlowJsonObject<any, any> = {
nodes: [],
edges: [],
viewport: { x: 0, y: 0, zoom: 1 },
};
const mockFolderId = "test-folder-id";
const mockFlow: FlowType = {
id: "test-flow-id",
name: "Test Flow",
description: "Test Description",
data: mockFlowData,
icon: "test-icon",
gradient: "test-gradient",
is_component: true,
folder_id: "original-folder-id",
endpoint_name: "test-endpoint",
tags: ["tag1", "tag2"],
mcp_enabled: false,
};
beforeEach(() => {
jest.clearAllMocks();
mockGetRandomDescription.mockReturnValue("Random Description");
});
describe("when no flow parameter is provided", () => {
it("should create a new flow with default values", () => {
const result = createNewFlow(mockFlowData, mockFolderId);
expect(result).toEqual({
description: "Random Description",
name: "New Flow",
data: mockFlowData,
id: "",
icon: undefined,
gradient: undefined,
is_component: false,
folder_id: mockFolderId,
endpoint_name: undefined,
tags: [],
mcp_enabled: true,
});
expect(mockGetRandomDescription).toHaveBeenCalledTimes(1);
});
it("should use the provided flowData and folderId", () => {
const customFlowData: ReactFlowJsonObject<any, any> = {
nodes: [
{
id: "node-1",
type: "genericNode",
position: { x: 0, y: 0 },
data: {},
},
],
edges: [
{
id: "edge-1",
source: "node-1",
target: "node-2",
},
],
viewport: { x: 10, y: 20, zoom: 1.5 },
};
const customFolderId = "custom-folder-id";
const result = createNewFlow(customFlowData, customFolderId);
expect(result.data).toBe(customFlowData);
expect(result.folder_id).toBe(customFolderId);
});
});
describe("when flow parameter is provided", () => {
it("should use flow properties when available", () => {
const result = createNewFlow(mockFlowData, mockFolderId, mockFlow);
expect(result).toEqual({
description: "Test Description",
name: "Test Flow",
data: mockFlowData,
id: "",
icon: "test-icon",
gradient: "test-gradient",
is_component: true,
folder_id: mockFolderId, // Should use new folderId, not original
endpoint_name: "test-endpoint",
tags: ["tag1", "tag2"],
mcp_enabled: true, // Always true regardless of input
});
expect(mockGetRandomDescription).not.toHaveBeenCalled();
});
it("should fallback to defaults when flow properties are undefined", () => {
const partialFlow: FlowType = {
id: "test-id",
name: "Test Name",
description: "", // Empty string, not undefined, so it won't fall back
data: null,
// All optional properties undefined
};
const result = createNewFlow(mockFlowData, mockFolderId, partialFlow);
expect(result).toEqual({
description: "", // Empty string is preserved, doesn't trigger fallback
name: "Test Name",
data: mockFlowData,
id: "",
icon: undefined,
gradient: undefined,
is_component: false,
folder_id: mockFolderId,
endpoint_name: undefined,
tags: [],
mcp_enabled: true,
});
expect(mockGetRandomDescription).not.toHaveBeenCalled();
});
it("should handle flow with empty description", () => {
const flowWithEmptyDescription: FlowType = {
...mockFlow,
description: "",
};
const result = createNewFlow(
mockFlowData,
mockFolderId,
flowWithEmptyDescription,
);
expect(result.description).toBe(""); // Empty string is preserved
expect(mockGetRandomDescription).not.toHaveBeenCalled();
});
it("should handle flow with null/undefined description", () => {
const flowWithNullDescription: FlowType = {
...mockFlow,
description: undefined as any,
};
const result = createNewFlow(
mockFlowData,
mockFolderId,
flowWithNullDescription,
);
expect(result.description).toBe("Random Description");
expect(mockGetRandomDescription).toHaveBeenCalledTimes(1);
});
it("should handle flow with empty name", () => {
const flowWithEmptyName: FlowType = {
...mockFlow,
name: "",
};
const result = createNewFlow(
mockFlowData,
mockFolderId,
flowWithEmptyName,
);
expect(result.name).toBe("New Flow");
});
it("should handle flow with whitespace-only name", () => {
const flowWithWhitespaceName: FlowType = {
...mockFlow,
name: " ",
};
const result = createNewFlow(
mockFlowData,
mockFolderId,
flowWithWhitespaceName,
);
expect(result.name).toBe(" "); // Preserves whitespace as per current logic
});
});
describe("special properties handling", () => {
it("should always set id to empty string", () => {
const result = createNewFlow(mockFlowData, mockFolderId, mockFlow);
expect(result.id).toBe("");
});
it("should always set mcp_enabled to true", () => {
const flowWithMcpDisabled: FlowType = {
...mockFlow,
mcp_enabled: false,
};
const result = createNewFlow(
mockFlowData,
mockFolderId,
flowWithMcpDisabled,
);
expect(result.mcp_enabled).toBe(true);
});
it("should always use the provided folderId, not the flows folder_id", () => {
const newFolderId = "different-folder-id";
const result = createNewFlow(mockFlowData, newFolderId, mockFlow);
expect(result.folder_id).toBe(newFolderId);
expect(result.folder_id).not.toBe(mockFlow.folder_id);
});
it("should handle undefined tags as empty array", () => {
const flowWithUndefinedTags: FlowType = {
...mockFlow,
tags: undefined,
};
const result = createNewFlow(
mockFlowData,
mockFolderId,
flowWithUndefinedTags,
);
expect(result.tags).toEqual([]);
});
it("should preserve non-empty tags array", () => {
const flowWithTags: FlowType = {
...mockFlow,
tags: ["ai", "ml", "workflow"],
};
const result = createNewFlow(mockFlowData, mockFolderId, flowWithTags);
expect(result.tags).toEqual(["ai", "ml", "workflow"]);
});
});
describe("edge cases", () => {
it("should handle empty folder id", () => {
const result = createNewFlow(mockFlowData, "", mockFlow);
expect(result.folder_id).toBe("");
});
it("should handle complex flowData structure", () => {
const complexFlowData: ReactFlowJsonObject<any, any> = {
nodes: [
{
id: "node-1",
type: "genericNode",
position: { x: 100, y: 200 },
data: {
id: "node-1",
type: "TestNode",
node: {
display_name: "Test Node",
description: "A test node",
},
},
},
],
edges: [
{
id: "edge-1",
source: "node-1",
target: "node-2",
sourceHandle: "output",
targetHandle: "input",
},
],
viewport: { x: -50, y: 100, zoom: 0.8 },
};
const result = createNewFlow(complexFlowData, mockFolderId, mockFlow);
expect(result.data).toBe(complexFlowData);
expect(result.data.nodes).toHaveLength(1);
expect(result.data.edges).toHaveLength(1);
expect(result.data.viewport).toEqual({ x: -50, y: 100, zoom: 0.8 });
});
it("should handle flow with all optional properties as null", () => {
const minimalFlow: FlowType = {
id: "minimal-id",
name: "Minimal Flow",
description: "Minimal description",
data: null,
icon: null as any,
gradient: null as any,
is_component: null as any,
endpoint_name: null,
tags: null as any,
mcp_enabled: null as any,
};
const result = createNewFlow(mockFlowData, mockFolderId, minimalFlow);
expect(result).toEqual({
description: "Minimal description",
name: "Minimal Flow",
data: mockFlowData,
id: "",
icon: undefined, // null becomes undefined due to ?? operator
gradient: undefined, // null becomes undefined due to ?? operator
is_component: false, // null becomes false due to ?? operator
folder_id: mockFolderId,
endpoint_name: undefined, // null becomes undefined due to ?? operator
tags: [], // null becomes [] due to ?? operator
mcp_enabled: true,
});
});
});
describe("function behavior consistency", () => {
it("should produce the same result for the same inputs", () => {
mockGetRandomDescription.mockReturnValue("Consistent Description");
const result1 = createNewFlow(mockFlowData, mockFolderId, mockFlow);
const result2 = createNewFlow(mockFlowData, mockFolderId, mockFlow);
expect(result1).toEqual(result2);
});
it("should not mutate input parameters", () => {
const originalFlowData = { ...mockFlowData };
const originalFlow = { ...mockFlow };
createNewFlow(mockFlowData, mockFolderId, mockFlow);
expect(mockFlowData).toEqual(originalFlowData);
expect(mockFlow).toEqual(originalFlow);
});
});
describe("description logic", () => {
it("should use flow description when truthy", () => {
const flowWithDescription = {
...mockFlow,
description: "Custom Description",
};
const result = createNewFlow(
mockFlowData,
mockFolderId,
flowWithDescription,
);
expect(result.description).toBe("Custom Description");
expect(mockGetRandomDescription).not.toHaveBeenCalled();
});
it("should use random description for null/undefined descriptions only", () => {
const nullishTestCases = [
{ ...mockFlow, description: null as any },
{ ...mockFlow, description: undefined as any },
];
nullishTestCases.forEach((testFlow) => {
mockGetRandomDescription.mockClear();
const result = createNewFlow(mockFlowData, mockFolderId, testFlow);
expect(result.description).toBe("Random Description");
expect(mockGetRandomDescription).toHaveBeenCalledTimes(1);
});
// These falsy values should NOT trigger random description
const falsyTestCases = [
{ ...mockFlow, description: "" },
{ ...mockFlow, description: 0 as any },
{ ...mockFlow, description: false as any },
];
falsyTestCases.forEach((testFlow) => {
mockGetRandomDescription.mockClear();
const result = createNewFlow(mockFlowData, mockFolderId, testFlow);
expect(result.description).toBe(testFlow.description);
expect(mockGetRandomDescription).not.toHaveBeenCalled();
});
});
});
describe("name logic", () => {
it("should use flow name when truthy", () => {
const flowWithName = { ...mockFlow, name: "Custom Name" };
const result = createNewFlow(mockFlowData, mockFolderId, flowWithName);
expect(result.name).toBe("Custom Name");
});
it("should use default name for falsy names", () => {
const testCases = [
{ ...mockFlow, name: "" },
{ ...mockFlow, name: null as any },
{ ...mockFlow, name: undefined as any },
{ ...mockFlow, name: 0 as any },
{ ...mockFlow, name: false as any },
];
testCases.forEach((testFlow) => {
const result = createNewFlow(mockFlowData, mockFolderId, testFlow);
expect(result.name).toBe("New Flow");
});
});
it("should preserve whitespace in names", () => {
const flowWithWhitespace = { ...mockFlow, name: " Test " };
const result = createNewFlow(
mockFlowData,
mockFolderId,
flowWithWhitespace,
);
expect(result.name).toBe(" Test ");
});
});
});

View file

@ -1946,7 +1946,7 @@ export const createNewFlow = (
) => {
return {
description: flow?.description ?? getRandomDescription(),
name: flow?.name ? flow.name : "Untitled document",
name: flow?.name ? flow.name : "New Flow",
data: flowData,
id: "",
icon: flow?.icon ?? undefined,

View file

@ -50,7 +50,7 @@ test("search flows", { tag: ["@release"] }, async ({ page }) => {
await page.getByTestId("icon-ChevronLeft").first().click();
await page.getByText("New Flow").isVisible();
await page.getByText("New Flow", { exact: true }).click();
await page.getByTestId("new-project-btn").click();
await page.getByTestId("side_nav_options_all-templates").click();
await page.getByRole("heading", { name: "Memory Chatbot" }).click();
@ -59,7 +59,7 @@ test("search flows", { tag: ["@release"] }, async ({ page }) => {
});
await page.getByTestId("icon-ChevronLeft").first().click();
await page.getByText("New Flow", { exact: true }).click();
await page.getByTestId("new-project-btn").click();
await page.getByTestId("side_nav_options_all-templates").click();
await page.getByRole("heading", { name: "Document Q&A" }).click();

View file

@ -102,7 +102,7 @@ test(
try {
await page.getByTestId("new_project_btn_empty_page").click();
} catch (_error) {
await page.getByText("New Flow", { exact: true }).click();
await page.getByTestId("new-project-btn").click();
}
await page.waitForSelector('[data-testid="modal-title"]', {

View file

@ -70,7 +70,11 @@ test(
console.error("Warning text not visible, skipping dialog confirmation");
}
await page.getByText("Untitled document").first().click();
const newFlowDiv = await page
.getByTestId("flow-name-div")
.filter({ hasText: "New Flow" })
.first();
await newFlowDiv.click();
await page.waitForSelector('[data-testid="icon-ChevronLeft"]', {
timeout: 5000,
@ -114,7 +118,11 @@ test(
await page.getByText("Save And Exit", { exact: true }).click();
await page.getByText("Untitled document").first().click();
const newFlow = await page
.getByTestId("flow-name-div")
.filter({ hasText: "New Flow" })
.first();
await newFlow.click();
await page.waitForSelector("text=loading", {
state: "hidden",
@ -162,7 +170,11 @@ test(
await page.getByText("Save And Exit", { exact: true }).last().click();
}
await page.getByText("Untitled document").first().click();
const newFlow2 = await page
.getByTestId("flow-name-div")
.filter({ hasText: "New Flow" })
.first();
await newFlow2.click();
await page.waitForSelector('[data-testid="icon-ChevronLeft"]', {
timeout: 5000,

View file

@ -13,7 +13,7 @@ test.describe(
await awaitBootstrapTest(page, {
skipModal: true,
});
await page.getByText("New Flow", { exact: true }).click();
await page.getByTestId("new-project-btn").click();
},
);
@ -24,24 +24,24 @@ test.describe(
await awaitBootstrapTest(page, {
skipModal: true,
});
await page.getByText("New Flow", { exact: true }).click();
await page.getByTestId("new-project-btn").click();
await page.waitForSelector('[data-testid="modal-title"]', {
timeout: 5000,
});
await page.goto("/login");
await page.getByText("New Flow", { exact: true }).click();
await page.getByTestId("new-project-btn").click();
await page.waitForSelector('[data-testid="modal-title"]', {
timeout: 5000,
});
await page.goto("/admin");
await page.getByText("New Flow", { exact: true }).click();
await page.getByTestId("new-project-btn").click();
await page.waitForSelector('[data-testid="modal-title"]', {
timeout: 5000,
});
await page.goto("/admin/login");
await page.getByText("New Flow", { exact: true }).click();
await page.getByTestId("new-project-btn").click();
await page.waitForSelector('[data-testid="modal-title"]', {
timeout: 5000,
});

View file

@ -20,7 +20,7 @@ test(
await page.getByTestId("icon-ChevronLeft").first().click();
await page.getByText("Projects").first().isVisible();
await page.getByText("New Flow", { exact: true }).click();
await page.getByTestId("new-project-btn").click();
await page.getByTestId("side_nav_options_all-templates").click();
await page.getByRole("heading", { name: "Document Q&A" }).click();
await page.waitForSelector('[data-testid="icon-ChevronLeft"]', {
@ -29,7 +29,7 @@ test(
await page.getByTestId("icon-ChevronLeft").first().click();
await page.getByText("Projects").first().isVisible();
await page.getByText("New Flow", { exact: true }).click();
await page.getByTestId("new-project-btn").click();
await page.getByTestId("side_nav_options_all-templates").click();
await page.getByRole("heading", { name: "Basic Prompting" }).click();
await page.waitForSelector('[data-testid="icon-ChevronLeft"]', {

View file

@ -184,7 +184,11 @@ test(
});
await awaitBootstrapTest(page, { skipModal: true });
await page.getByText("Untitled document").first().click();
const newFlowDiv = await page
.getByTestId("flow-name-div")
.filter({ hasText: "New Flow" })
.first();
await newFlowDiv.click();
await page.waitForTimeout(1000);

View file

@ -34,7 +34,7 @@ test(
).isVisible(),
);
await page.getByText("New Flow", { exact: true }).click();
await page.getByTestId("new-project-btn").click();
await page.getByTestId("side_nav_options_all-templates").click();
await page.getByRole("heading", { name: "Basic Prompting" }).click();

View file

@ -47,7 +47,7 @@ test(
timeout: 3000,
});
await page.getByText("New Flow", { exact: true }).click();
await page.getByTestId("new-project-btn").click();
await page.getByTestId("side_nav_options_all-templates").click();
await page.getByRole("heading", { name: "Basic Prompting" }).click();

View file

@ -39,7 +39,7 @@ export const awaitBootstrapTest = async (
}
while (modalCount === 0) {
await page.getByText("New Flow", { exact: true }).click();
await page.getByTestId("new-project-btn").click();
await page.waitForSelector('[data-testid="modal-title"]', {
timeout: 3000,
});