mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-25 11:10:43 +08:00
Merge branch 'lfoss-1889' of https://github.com/langflow-ai/langflow into lfoss-1889
This commit is contained in:
@ -1,3 +1,6 @@
|
||||
import { memo, useMemo, useRef, useState } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import IconComponent from "@/components/common/genericIconComponent";
|
||||
import ShadTooltip from "@/components/common/shadTooltipComponent";
|
||||
import FlowSettingsComponent from "@/components/core/flowSettingsComponent";
|
||||
@ -20,9 +23,6 @@ import useFlowsManagerStore from "@/stores/flowsManagerStore";
|
||||
import { useShortcutsStore } from "@/stores/shortcuts";
|
||||
import { swatchColors } from "@/utils/styleUtils";
|
||||
import { cn, getNumberFromString } from "@/utils/utils";
|
||||
import { memo, useMemo, useRef, useState } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
|
||||
export const MenuBar = memo((): JSX.Element => {
|
||||
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import CanvasControls from '../index';
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import CanvasControls from "../index";
|
||||
|
||||
// Capture flow functions for assertions
|
||||
const reactFlowFns = {
|
||||
@ -10,8 +10,12 @@ const reactFlowFns = {
|
||||
};
|
||||
|
||||
// Mocks for external dependencies used internally
|
||||
jest.mock('@xyflow/react', () => ({
|
||||
Panel: ({ children, ...props }: any) => <div data-testid="panel" {...props}>{children}</div>,
|
||||
jest.mock("@xyflow/react", () => ({
|
||||
Panel: ({ children, ...props }: any) => (
|
||||
<div data-testid="panel" {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
useReactFlow: () => reactFlowFns,
|
||||
useStore: (_selector: any) => ({
|
||||
isInteractive: true,
|
||||
@ -22,38 +26,40 @@ jest.mock('@xyflow/react', () => ({
|
||||
useStoreApi: () => ({ setState: jest.fn() }),
|
||||
}));
|
||||
|
||||
jest.mock('@/stores/flowStore', () => ({
|
||||
jest.mock("@/stores/flowStore", () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(() => false),
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/separator', () => ({
|
||||
Separator: ({ orientation }: { orientation: 'vertical' | 'horizontal' }) => (
|
||||
jest.mock("@/components/ui/separator", () => ({
|
||||
Separator: ({ orientation }: { orientation: "vertical" | "horizontal" }) => (
|
||||
<div data-testid={`separator-${orientation}`} />
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock dropdowns to a simple render that exposes props for assertions
|
||||
jest.mock('../dropdowns', () => ({
|
||||
jest.mock("../dropdowns", () => ({
|
||||
CanvasControlsDropdown: (props: any) => (
|
||||
<div data-testid="controls-dropdown" {...props} />
|
||||
),
|
||||
HelpDropdown: (props: any) => (
|
||||
<div data-testid="help-dropdown" {...props} />
|
||||
),
|
||||
HelpDropdown: (props: any) => <div data-testid="help-dropdown" {...props} />,
|
||||
}));
|
||||
|
||||
describe('CanvasControls', () => {
|
||||
it('renders panel and separators when children present', () => {
|
||||
render(<CanvasControls><div>child</div></CanvasControls>);
|
||||
expect(screen.getByTestId('main_canvas_controls')).toBeInTheDocument();
|
||||
const seps = screen.getAllByTestId('separator-vertical');
|
||||
describe("CanvasControls", () => {
|
||||
it("renders panel and separators when children present", () => {
|
||||
render(
|
||||
<CanvasControls>
|
||||
<div>child</div>
|
||||
</CanvasControls>,
|
||||
);
|
||||
expect(screen.getByTestId("main_canvas_controls")).toBeInTheDocument();
|
||||
const seps = screen.getAllByTestId("separator-vertical");
|
||||
expect(seps.length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getByTestId('controls-dropdown')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('help-dropdown')).toBeInTheDocument();
|
||||
expect(screen.getByTestId("controls-dropdown")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("help-dropdown")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles keyboard shortcuts with meta/ctrl keys', () => {
|
||||
it("handles keyboard shortcuts with meta/ctrl keys", () => {
|
||||
render(<CanvasControls />);
|
||||
reactFlowFns.fitView.mockClear();
|
||||
reactFlowFns.zoomIn.mockClear();
|
||||
@ -61,13 +67,13 @@ describe('CanvasControls', () => {
|
||||
reactFlowFns.zoomTo.mockClear();
|
||||
|
||||
// Press + (Equal) with meta
|
||||
fireEvent.keyDown(document, { key: '+', code: 'Equal', metaKey: true });
|
||||
fireEvent.keyDown(document, { key: "+", code: "Equal", metaKey: true });
|
||||
// Press - (Minus) with ctrl
|
||||
fireEvent.keyDown(document, { key: '-', code: 'Minus', ctrlKey: true });
|
||||
fireEvent.keyDown(document, { key: "-", code: "Minus", ctrlKey: true });
|
||||
// Press 1 (Digit1) with meta
|
||||
fireEvent.keyDown(document, { key: '1', code: 'Digit1', metaKey: true });
|
||||
fireEvent.keyDown(document, { key: "1", code: "Digit1", metaKey: true });
|
||||
// Press 0 (Digit0) with ctrl
|
||||
fireEvent.keyDown(document, { key: '0', code: 'Digit0', ctrlKey: true });
|
||||
fireEvent.keyDown(document, { key: "0", code: "Digit0", ctrlKey: true });
|
||||
|
||||
expect(reactFlowFns.zoomIn).toHaveBeenCalled();
|
||||
expect(reactFlowFns.zoomOut).toHaveBeenCalled();
|
||||
@ -75,5 +81,3 @@ describe('CanvasControls', () => {
|
||||
expect(reactFlowFns.zoomTo).toHaveBeenCalledWith(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -1,52 +1,68 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter, useNavigate } from 'react-router-dom';
|
||||
import { CanvasControlsDropdown, HelpDropdown } from '../dropdowns';
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { MemoryRouter, useNavigate } from "react-router-dom";
|
||||
import { CanvasControlsDropdown, HelpDropdown } from "../dropdowns";
|
||||
|
||||
jest.mock('@/components/ui/button', () => ({
|
||||
Button: ({ children, ...props }: any) => <button {...props}>{children}</button>,
|
||||
jest.mock("@/components/ui/button", () => ({
|
||||
Button: ({ children, ...props }: any) => (
|
||||
<button {...props}>{children}</button>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/dropdown-menu', () => ({
|
||||
DropdownMenu: ({ children, ...props }: any) => <div data-testid="dropdown-menu" {...props}>{children}</div>,
|
||||
DropdownMenuTrigger: ({ children, ...props }: any) => <div data-testid="dropdown-trigger" {...props}>{children}</div>,
|
||||
DropdownMenuContent: ({ children, ...props }: any) => <div data-testid="dropdown-content" {...props}>{children}</div>,
|
||||
jest.mock("@/components/ui/dropdown-menu", () => ({
|
||||
DropdownMenu: ({ children, ...props }: any) => (
|
||||
<div data-testid="dropdown-menu" {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
DropdownMenuTrigger: ({ children, ...props }: any) => (
|
||||
<div data-testid="dropdown-trigger" {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
DropdownMenuContent: ({ children, ...props }: any) => (
|
||||
<div data-testid="dropdown-content" {...props}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/components/ui/separator', () => ({
|
||||
jest.mock("@/components/ui/separator", () => ({
|
||||
Separator: () => <div data-testid="separator" />,
|
||||
}));
|
||||
|
||||
jest.mock('@/components/common/genericIconComponent', () => ({
|
||||
jest.mock("@/components/common/genericIconComponent", () => ({
|
||||
__esModule: true,
|
||||
default: () => <span data-testid="icon" />,
|
||||
ForwardedIconComponent: ({ name }: { name: string }) => <span data-testid={`icon-${name}`} />,
|
||||
ForwardedIconComponent: ({ name }: { name: string }) => (
|
||||
<span data-testid={`icon-${name}`} />
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@/constants/constants', () => ({
|
||||
jest.mock("@/constants/constants", () => ({
|
||||
__esModule: true,
|
||||
DATASTAX_DOCS_URL: 'https://docs.datastax.com',
|
||||
DOCS_URL: 'https://docs.langflow.org',
|
||||
DESKTOP_URL: 'https://desktop.langflow.org',
|
||||
DATASTAX_DOCS_URL: "https://docs.datastax.com",
|
||||
DOCS_URL: "https://docs.langflow.org",
|
||||
DESKTOP_URL: "https://desktop.langflow.org",
|
||||
}));
|
||||
|
||||
jest.mock('@/customization/feature-flags', () => ({
|
||||
jest.mock("@/customization/feature-flags", () => ({
|
||||
ENABLE_DATASTAX_LANGFLOW: false,
|
||||
}));
|
||||
|
||||
jest.mock('@/utils/utils', () => ({
|
||||
cn: (...args: any[]) => args.filter(Boolean).join(' '),
|
||||
getOS: () => 'macos',
|
||||
jest.mock("@/utils/utils", () => ({
|
||||
cn: (...args: any[]) => args.filter(Boolean).join(" "),
|
||||
getOS: () => "macos",
|
||||
}));
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
const actual = jest.requireActual('react-router-dom');
|
||||
jest.mock("react-router-dom", () => {
|
||||
const actual = jest.requireActual("react-router-dom");
|
||||
return {
|
||||
...actual,
|
||||
useNavigate: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('CanvasControlsDropdown', () => {
|
||||
describe("CanvasControlsDropdown", () => {
|
||||
const baseProps = {
|
||||
zoom: 1,
|
||||
minZoomReached: false,
|
||||
@ -58,21 +74,21 @@ describe('CanvasControlsDropdown', () => {
|
||||
onResetZoom: jest.fn(),
|
||||
onFitView: jest.fn(),
|
||||
shortcuts: {
|
||||
ZOOM_IN: { key: '+' },
|
||||
ZOOM_OUT: { key: '-' },
|
||||
RESET_ZOOM: { key: '0' },
|
||||
FIT_VIEW: { key: '1' },
|
||||
ZOOM_IN: { key: "+" },
|
||||
ZOOM_OUT: { key: "-" },
|
||||
RESET_ZOOM: { key: "0" },
|
||||
FIT_VIEW: { key: "1" },
|
||||
},
|
||||
};
|
||||
|
||||
it('renders zoom percentage and calls handlers', () => {
|
||||
it("renders zoom percentage and calls handlers", () => {
|
||||
render(<CanvasControlsDropdown {...baseProps} />);
|
||||
expect(screen.getByText('100%')).toBeInTheDocument();
|
||||
expect(screen.getByText("100%")).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByTestId('zoom_in_dropdown'));
|
||||
fireEvent.click(screen.getByTestId('zoom_out_dropdown'));
|
||||
fireEvent.click(screen.getByTestId('reset_zoom_dropdown'));
|
||||
fireEvent.click(screen.getByTestId('fit_view_dropdown'));
|
||||
fireEvent.click(screen.getByTestId("zoom_in_dropdown"));
|
||||
fireEvent.click(screen.getByTestId("zoom_out_dropdown"));
|
||||
fireEvent.click(screen.getByTestId("reset_zoom_dropdown"));
|
||||
fireEvent.click(screen.getByTestId("fit_view_dropdown"));
|
||||
|
||||
expect(baseProps.onZoomIn).toHaveBeenCalled();
|
||||
expect(baseProps.onZoomOut).toHaveBeenCalled();
|
||||
@ -80,43 +96,49 @@ describe('CanvasControlsDropdown', () => {
|
||||
expect(baseProps.onFitView).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('disables buttons based on min/max zoom flags', () => {
|
||||
it("disables buttons based on min/max zoom flags", () => {
|
||||
render(
|
||||
<CanvasControlsDropdown
|
||||
{...baseProps}
|
||||
minZoomReached={true}
|
||||
maxZoomReached={true}
|
||||
/>
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByTestId('zoom_in_dropdown')).toBeDisabled();
|
||||
expect(screen.getByTestId('zoom_out_dropdown')).toBeDisabled();
|
||||
expect(screen.getByTestId("zoom_in_dropdown")).toBeDisabled();
|
||||
expect(screen.getByTestId("zoom_out_dropdown")).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('HelpDropdown', () => {
|
||||
describe("HelpDropdown", () => {
|
||||
beforeEach(() => {
|
||||
(window.open as jest.Mock).mockClear();
|
||||
});
|
||||
|
||||
it('opens docs in new tab and navigates to shortcuts', () => {
|
||||
it("opens docs in new tab and navigates to shortcuts", () => {
|
||||
const mockNavigate = jest.fn();
|
||||
(useNavigate as unknown as jest.Mock).mockReturnValue(mockNavigate);
|
||||
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<HelpDropdown isOpen={true} onOpenChange={() => {}} />
|
||||
</MemoryRouter>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByTestId('canvas_controls_dropdown_docs'));
|
||||
expect(window.open).toHaveBeenCalledWith('https://docs.langflow.org', '_blank');
|
||||
fireEvent.click(screen.getByTestId("canvas_controls_dropdown_docs"));
|
||||
expect(window.open).toHaveBeenCalledWith(
|
||||
"https://docs.langflow.org",
|
||||
"_blank",
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByTestId('canvas_controls_dropdown_shortcuts'));
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/settings/shortcuts');
|
||||
fireEvent.click(screen.getByTestId("canvas_controls_dropdown_shortcuts"));
|
||||
expect(mockNavigate).toHaveBeenCalledWith("/settings/shortcuts");
|
||||
|
||||
fireEvent.click(screen.getByTestId('canvas_controls_dropdown_get_langflow_desktop'));
|
||||
expect(window.open).toHaveBeenCalledWith('https://desktop.langflow.org', '_blank');
|
||||
fireEvent.click(
|
||||
screen.getByTestId("canvas_controls_dropdown_get_langflow_desktop"),
|
||||
);
|
||||
expect(window.open).toHaveBeenCalledWith(
|
||||
"https://desktop.langflow.org",
|
||||
"_blank",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -1,15 +1,19 @@
|
||||
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import * as Form from "@radix-ui/react-form";
|
||||
import type React from "react";
|
||||
import { useState } from "react";
|
||||
import ForwardedIconComponent from "@/components/common/genericIconComponent";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import type { InputProps } from "../../../types/components";
|
||||
import { cn } from "../../../utils/utils";
|
||||
import { Input } from "../../ui/input";
|
||||
import { Textarea } from "../../ui/textarea";
|
||||
|
||||
export const EditFlowSettings: React.FC<
|
||||
InputProps & { submitForm?: () => void; locked?: boolean; setLocked?: (v: boolean) => void }
|
||||
InputProps & {
|
||||
submitForm?: () => void;
|
||||
locked?: boolean;
|
||||
setLocked?: (v: boolean) => void;
|
||||
}
|
||||
> = ({
|
||||
name,
|
||||
invalidNameList = [],
|
||||
@ -22,7 +26,11 @@ export const EditFlowSettings: React.FC<
|
||||
submitForm,
|
||||
locked = false,
|
||||
setLocked,
|
||||
}: InputProps & { submitForm?: () => void; locked?: boolean; setLocked?: (v: boolean) => void }): JSX.Element => {
|
||||
}: InputProps & {
|
||||
submitForm?: () => void;
|
||||
locked?: boolean;
|
||||
setLocked?: (v: boolean) => void;
|
||||
}): JSX.Element => {
|
||||
const [isMaxLength, setIsMaxLength] = useState(false);
|
||||
const [isMaxDescriptionLength, setIsMaxDescriptionLength] = useState(false);
|
||||
const [isMinLength, setIsMinLength] = useState(false);
|
||||
@ -171,8 +179,11 @@ export const EditFlowSettings: React.FC<
|
||||
<Form.Message match="valueMissing" className="field-invalid">
|
||||
Please enter a description
|
||||
</Form.Message>
|
||||
<div className="flex items-center gap-2 mt-3">
|
||||
<ForwardedIconComponent name={locked ? "Lock" : "Unlock"} className="text-muted-foreground !w-5 !h-5" />
|
||||
<div className="flex items-center gap-2 mt-3">
|
||||
<ForwardedIconComponent
|
||||
name={locked ? "Lock" : "Unlock"}
|
||||
className="text-muted-foreground !w-5 !h-5"
|
||||
/>
|
||||
<Form.Label className="text-mmd font-medium">Lock Flow</Form.Label>
|
||||
<Switch
|
||||
checked={!!locked}
|
||||
@ -182,7 +193,6 @@ export const EditFlowSettings: React.FC<
|
||||
/>
|
||||
</div>
|
||||
</Form.Field>
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user