Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@ jest.mock("../../../ui/select", () => {
children: React.ReactNode;
value: string;
}) => <option value={value}>{children}</option>,
SelectTrigger: ({ children }: { children: React.ReactNode }) => (
<>{children}</>
SelectTrigger: ({ children, ...rest }: any) => (
<div data-testid="page-select-trigger" aria-label={rest["aria-label"]}>
{children}
</div>
),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use a semantic trigger mock that forwards props.

Suggested test-fidelity fix
-    SelectTrigger: ({ children, ...rest }: any) => (
-      <div data-testid="page-select-trigger" aria-label={rest["aria-label"]}>
-        {children}
-      </div>
-    ),
+    SelectTrigger: ({ children, ...rest }: any) => (
+      <button type="button" data-testid="page-select-trigger" {...rest}>
+        {children}
+      </button>
+    ),

As per coding guidelines: "Ensure mocks are used appropriately for external dependencies, not core logic" and "Warn when mocks are used instead of testing real behavior and interactions."

Also applies to: 70-76

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/src/components/common/paginatorComponent/__tests__/PaginatorComponent.test.tsx`
around lines 43 - 47, The SelectTrigger mock in the test file does not properly
forward all props to the underlying div element, which reduces test fidelity.
Instead of only explicitly handling the aria-label prop, use the spread operator
to forward all rest props to the div element (for example, replace
aria-label={rest["aria-label"]} with {...rest}). This change should be applied
at the anchor location in
src/frontend/src/components/common/paginatorComponent/__tests__/PaginatorComponent.test.tsx
at lines 43-47 in the SelectTrigger mock, and at the sibling location at lines
70-76 where the same mock pattern appears, to ensure consistent prop forwarding
behavior across all mocks.

Source: Coding guidelines

SelectValue: () => null,
};
Expand All @@ -64,6 +66,16 @@ const renderPaginator = (
};

describe("PaginatorComponent", () => {
describe("accessibility", () => {
it("page select trigger has an aria-label", () => {
renderPaginator({ totalRowsCount: 45, pageIndex: 1 });
const trigger = screen.getByTestId("page-select-trigger");
// i18n resolves "paginator.pageLabel" → "Page"
expect(trigger).toHaveAttribute("aria-label");
expect(trigger.getAttribute("aria-label")).toBeTruthy();
});
});

describe("empty state", () => {
it("renders '0 items' instead of '1-0 of 0 items' when there are no rows", () => {
renderPaginator({ totalRowsCount: 0 });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export default function PaginatorComponent({
<SelectTrigger
direction="up"
className="h-7 w-fit gap-1 border-none p-1 pl-1.5 text-mmd focus:border-none focus:ring-0 focus:!ring-offset-0"
aria-label={t("paginator.pageLabel")}
>
<SelectValue placeholder={t("paginator.placeholder")} />
</SelectTrigger>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { render, screen } from "@testing-library/react";
import AppHeader from "../index";

// i18n — returns the key so aria-label values are predictable
jest.mock("react-i18next", () => ({
useTranslation: () => ({ t: (key: string) => key }),
}));

jest.mock("@/components/common/genericIconComponent", () => ({
__esModule: true,
default: ({ name }: { name: string }) => (
<span data-testid={`icon-${name}`}>{name}</span>
),
}));

jest.mock("@/components/ui/button", () => ({
Button: ({
children,
"aria-label": ariaLabel,
"data-testid": testId,
unstyled: _unstyled,
...props
}: React.ButtonHTMLAttributes<HTMLButtonElement> & {
children?: React.ReactNode;
"data-testid"?: string;
unstyled?: boolean;
}) => (
<button aria-label={ariaLabel} data-testid={testId} {...props}>
{children}
</button>
),
}));

jest.mock("@/components/ui/separator", () => ({
Separator: () => <hr />,
}));

jest.mock("@/components/common/shadTooltipComponent", () => ({
__esModule: true,
default: ({ children }: { children?: React.ReactNode }) => <>{children}</>,
}));

jest.mock("@/components/common/modelProviderCountComponent", () => ({
__esModule: true,
default: () => null,
}));

jest.mock("@/alerts/alertDropDown", () => ({
__esModule: true,
default: ({ children }: { children?: React.ReactNode }) => <>{children}</>,
}));

jest.mock("@/assets/LangflowLogo.svg?react", () => ({
__esModule: true,
default: () => <svg data-testid="langflow-logo" />,
}));

jest.mock("@/customization/components/custom-AccountMenu", () => ({
__esModule: true,
default: () => null,
}));

jest.mock("@/customization/components/custom-langflow-counts", () => ({
__esModule: true,
default: () => null,
}));

jest.mock("@/customization/components/custom-org-selector", () => ({
CustomOrgSelector: () => null,
}));

jest.mock("@/customization/hooks/use-custom-navigate", () => ({
useCustomNavigate: () => jest.fn(),
}));

jest.mock("@/customization/hooks/use-custom-theme", () => ({
__esModule: true,
default: () => ({ dark: false }),
}));

jest.mock("@/stores/alertStore", () => ({
__esModule: true,
default: () => ({
notificationCenter: false,
setNotificationCenter: jest.fn(),
}),
}));

jest.mock("./components/FlowMenu", () => ({ FlowMenu: () => null }), {
virtual: true,
});
jest.mock("../components/FlowMenu", () => ({
__esModule: true,
default: () => null,
}));

describe("AppHeader — accessible button names", () => {
beforeEach(() => {
render(<AppHeader />);
});

it("home/back button has an aria-label", () => {
const btn = screen.getByTestId("icon-ChevronLeft");
expect(btn.closest("button")).toHaveAttribute("aria-label", "header.home");
});

it("notification bell button has an aria-label", () => {
const btn = screen.getByTestId("notification_button");
expect(btn).toHaveAttribute("aria-label", "header.notifications");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export default function AppHeader(): JSX.Element {
onClick={() => navigate("/")}
className="mr-1 flex h-8 w-8 items-center"
data-testid="icon-ChevronLeft"
aria-label={t("header.home")}
>
<LangflowLogo className="h-5 w-5" />
</Button>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { render, screen } from "@testing-library/react";
import { AddFolderButton } from "../add-folder-button";

jest.mock("react-i18next", () => ({
useTranslation: () => ({ t: (key: string) => key }),
}));

jest.mock("@/components/common/genericIconComponent", () => ({
__esModule: true,
default: ({ name }: { name: string }) => (
<span data-testid={`icon-${name}`}>{name}</span>
),
}));

jest.mock("@/components/common/shadTooltipComponent", () => ({
__esModule: true,
default: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}));

jest.mock("@/components/ui/button", () => ({
Button: ({
children,
"aria-label": ariaLabel,
disabled,
loading,
...props
}: any) => (
<button
aria-label={ariaLabel}
disabled={disabled}
data-loading={loading}
{...props}
>
{children}
</button>
),
}));

describe("AddFolderButton", () => {
const defaultProps = { onClick: jest.fn(), disabled: false, loading: false };

it("renders the add project button", () => {
render(<AddFolderButton {...defaultProps} />);
expect(screen.getByTestId("add-project-button")).toBeInTheDocument();
});

it("has an aria-label for screen readers", () => {
render(<AddFolderButton {...defaultProps} />);
const btn = screen.getByTestId("add-project-button");
expect(btn).toHaveAttribute("aria-label", "folder.createNewProject");
});

it("is disabled when disabled prop is true", () => {
render(<AddFolderButton {...defaultProps} disabled />);
expect(screen.getByTestId("add-project-button")).toBeDisabled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { render, screen } from "@testing-library/react";
import { UploadFolderButton } from "../upload-folder-button";

jest.mock("react-i18next", () => ({
useTranslation: () => ({ t: (key: string) => key }),
}));

jest.mock("@/components/common/genericIconComponent", () => ({
__esModule: true,
default: ({ name }: { name: string }) => (
<span data-testid={`icon-${name}`}>{name}</span>
),
}));

jest.mock("@/components/common/shadTooltipComponent", () => ({
__esModule: true,
default: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}));

jest.mock("@/components/ui/button", () => ({
Button: ({
children,
"aria-label": ariaLabel,
disabled,
...props
}: React.ButtonHTMLAttributes<HTMLButtonElement> & {
children?: React.ReactNode;
}) => (
<button aria-label={ariaLabel} disabled={disabled} {...props}>
{children}
</button>
),
}));
Comment on lines +4 to +33

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify frontend test framework usage and mock density (read-only).
# Expected: if guideline is enforced, frontend *.test.ts/tsx should primarily use Playwright APIs.

echo "== Frontend test files =="
fd -e ts -e tsx '.test$' src/frontend/src

echo
echo "== Files importing Jest/RTL in frontend tests =="
rg -n --type=ts --type=tsx '(`@testing-library/react`|jest\.mock|describe\(|it\()' src/frontend/src --iglob '**/*.test.ts' --iglob '**/*.test.tsx'

echo
echo "== Files importing Playwright in frontend tests =="
rg -n --type=ts --type=tsx '(`@playwright/test`|test\(|expect\()' src/frontend/src --iglob '**/*.test.ts' --iglob '**/*.test.tsx'

Repository: langflow-ai/langflow

Length of output: 266


🏁 Script executed:

#!/bin/bash
# Find all test files in frontend with correct syntax
echo "== All test files in frontend =="
find src/frontend -name "*.test.ts" -o -name "*.test.tsx" 2>/dev/null | head -20

echo
echo "== Check specific file existence =="
ls -la "src/frontend/src/components/core/folderSidebarComponent/components/sideBarFolderButtons/components/__tests__/upload-folder-button.test.tsx" 2>&1

echo
echo "== Checking package.json for testing framework dependencies =="
cat package.json | grep -A 10 -B 2 '"jest"\|"playwright"\|"vitest"' 2>/dev/null || echo "Not found in top-level search"

Repository: langflow-ai/langflow

Length of output: 2229


🏁 Script executed:

#!/bin/bash
# Search for test framework usage without type flags
echo "== Jest usage in frontend tests =="
grep -r "jest.mock\|describe(\|it(" src/frontend --include="*.test.ts*" 2>/dev/null | head -5

echo
echo "== Playwright usage in frontend tests =="
grep -r "`@playwright/test`\|test(" src/frontend --include="*.test.ts*" 2>/dev/null | head -5

echo
echo "== Looking for jest/vitest in package.json =="
grep -i "jest\|vitest\|playwright" package*.json 2>/dev/null | head -20

Repository: langflow-ai/langflow

Length of output: 1565


🏁 Script executed:

#!/bin/bash
# Read package.json to check testing framework dependencies
echo "== Frontend package.json test dependencies =="
cat src/frontend/package.json | grep -A 30 '"devDependencies"' | head -40

Repository: langflow-ai/langflow

Length of output: 1197


🏁 Script executed:

#!/bin/bash
# Read the specific test file to assess mock usage
echo "== Content of upload-folder-button.test.tsx =="
cat src/frontend/src/components/core/folderSidebarComponent/components/sideBarFolderButtons/components/__tests__/upload-folder-button.test.tsx

Repository: langflow-ai/langflow

Length of output: 1655


Reduce internal component mocks to test real interaction behavior.

The test mocks internal UI components (Button, shadTooltipComponent, genericIconComponent) that should be rendered for real to verify actual behavior. Per coding guidelines, mocks should be limited to external dependencies (like react-i18next); mocking core UI logic obscures what's actually being tested. Consider importing and rendering the real components, or replacing mocks with minimal test doubles that preserve behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/src/components/core/folderSidebarComponent/components/sideBarFolderButtons/components/__tests__/upload-folder-button.test.tsx`
around lines 4 - 26, Remove the jest.mock calls for the internal UI components
Button, shadTooltipComponent, and genericIconComponent in the test setup. These
mocks are obscuring actual component behavior and should not be mocked according
to testing guidelines. Keep the mock for react-i18next since it is an external
dependency. Either allow the real components to render naturally (by removing
their mock definitions) or replace the mocks with minimal test doubles that
preserve the expected behavior without hiding implementation details. This will
ensure the test verifies real interaction behavior with the actual UI
components.

Source: Coding guidelines


describe("UploadFolderButton", () => {
const defaultProps = { onClick: jest.fn(), disabled: false };

it("renders the upload project button", () => {
render(<UploadFolderButton {...defaultProps} />);
expect(screen.getByTestId("upload-project-button")).toBeInTheDocument();
});

it("has an aria-label for screen readers", () => {
render(<UploadFolderButton {...defaultProps} />);
const btn = screen.getByTestId("upload-project-button");
expect(btn).toHaveAttribute("aria-label", "folder.uploadFlow");
});

it("is disabled when disabled prop is true", () => {
render(<UploadFolderButton {...defaultProps} disabled />);
expect(screen.getByTestId("upload-project-button")).toBeDisabled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const AddFolderButton = ({
className="h-7 w-7 border-0 text-muted-foreground hover:bg-muted"
onClick={onClick}
data-testid="add-project-button"
aria-label={t("folder.createNewProject")}
disabled={disabled}
loading={loading}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,12 @@ export const SelectOptions = ({
styleClasses="z-50"
>
<SelectTrigger
className="w-fit"
className="h-6 w-6 min-h-[24px] min-w-[24px]"
id={`options-trigger-${item.name}`}
data-testid={
"more-options-button" + `_${convertTestName(item?.name ?? "")}`
}
aria-label={t("folder.optionsFor", { name: item.name })}
>
<IconComponent
name={"MoreHorizontal"}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const UploadFolderButton = ({ onClick, disabled }) => {
className="h-7 w-7 border-0 text-muted-foreground hover:bg-muted"
onClick={onClick}
data-testid="upload-project-button"
aria-label={t("folder.uploadFlow")}
disabled={disabled}
>
<IconComponent name="Upload" className="h-4 w-4" />
Expand Down
22 changes: 18 additions & 4 deletions src/frontend/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"assistant.loading": "Ladevorgang läuft...",
"assistant.maxSessionsLabel": "Max. Sitzungen",
"assistant.maxSessionsTooltip": "Die maximale Anzahl von {{max}} -Sitzungen wurde erreicht. Löschen Sie eine Sitzung, um eine neue zu erstellen.",
"assistant.mentionNoMatches": "Keine passenden Komponenten auf der Arbeitsfläche",
"assistant.messageCounts": "{{count}} Nachrichten",
"assistant.newConversation": "Neue Konversation",
"assistant.newPill": "Neu",
Expand Down Expand Up @@ -143,7 +144,6 @@
"assistant.welcomeText": "So kann ich Ihnen helfen",
"assistant.working": "Wird geladen...",
"assistant.workingOnIt": "Ich arbeite daran...",
"assistant.mentionNoMatches": "Keine passenden Komponenten auf der Arbeitsfläche",
"auth.adminLoginButton": "Anmeldung",
"auth.adminTitle": "Administrator",
"auth.alertSaveWithApi": "⚠️ Achtung: Durch das Exportieren dieses Flows können vertrauliche Anmeldedaten offengelegt werden.",
Expand Down Expand Up @@ -709,8 +709,6 @@
"files.uploadedSuccessfully": "Datei erfolgreich hochgeladen",
"flow.addDescription": "Beschreibung hinzufügen...",
"flow.brokenEdgesWarning": "Einige Verbindungen wurden entfernt, da sie ungültig waren:",
"flow.exclusiveComponentsNotPasted": "Einige Komponenten wurden nicht eingefügt, da sie nicht zusammen mit bereits im Flow vorhandenen Komponenten verwendet werden können.",
"flow.duplicateComponentsNotPasted": "Einige Komponenten wurden nicht eingefügt, da pro Flow nur eine Instanz erlaubt ist.",
"flow.buildStatus": "Erstellen, um den Status zu überprüfen.",
"flow.buildSuccess": "Erfolgreich erstellt ✨",
"flow.characterLimitReached": "Zeichenlimit erreicht",
Expand Down Expand Up @@ -779,10 +777,12 @@
"flow.deletedSuccessfully": "Die ausgewählten Elemente wurden erfolgreich gelöscht",
"flow.descriptionLabel": "Beschreibung",
"flow.descriptionPlaceholder": "Beschreibung des Ablaufs",
"flow.duplicateComponentsNotPasted": "Einige Komponenten wurden nicht eingefügt, da in einem Ablauf jeweils nur eine davon zulässig ist.",
"flow.duplicatedSuccess": "{{type}} Erfolgreich dupliziert",
"flow.enableAutoSaving": "Automatisches Speichern aktivieren",
"flow.errorDeleting": "Fehler beim Löschen von Elementen",
"flow.errorDeletingRetry": "Wiederholen Sie die Operation",
"flow.exclusiveComponentsNotPasted": "Einige Komponenten wurden nicht eingefügt, da sie nicht zusammen mit den bereits im Ablauf vorhandenen Komponenten verwendet werden können.",
"flow.exitAnyway": "Trotzdem beenden",
"flow.lastSaved": "Zuletzt gespeichert: {{time}}",
"flow.lastSavedNever": "Niemals",
Expand Down Expand Up @@ -1148,8 +1148,11 @@
"mcp.configErrorFix": "Bitte passen Sie die Einstellung „ OAuth “ in Ihren Projekteinstellungen an, um die MCP-Serverkonfiguration zu generieren.",
"mcp.editAuth": "Bearbeitung Auth",
"mcp.editTools": "Bearbeitungswerkzeuge",
"mcp.failedToInstall": "Die Installation des MCP-Servers unter {{client}} ist fehlgeschlagen",
"mcp.flowsTools": "Abläufe/Tools",
"mcp.flowsTooltip": "Die Abläufe in diesem Projekt können als aufrufbare MCP-Tools bereitgestellt werden.",
"mcp.installDisabledWarning": "Die Ein-Klick-Installation ist deaktiviert, da der Langflow-Server nicht auf Ihrem lokalen Rechner läuft. Verwenden Sie die Registerkarte „JSON“, um Ihren Client manuell zu konfigurieren.",
"mcp.installTooltip": "Installieren Sie „ {{name}} “, um die automatische Installation zu aktivieren.",
"mcp.installedSuccessfully": "Der MCP-Server wurde erfolgreich unter {{client}} installiert. Möglicherweise müssen Sie Ihren Client neu starten, damit die Änderungen sichtbar werden.",
"mcp.loading": "Ladevorgang läuft...",
"mcp.loadingServers": "Server werden geladen...",
Expand Down Expand Up @@ -1195,6 +1198,7 @@
"mcp.selectServer": "Wähle einen Server aus...",
"mcp.serverDescription": "Greifen Sie auf die Abläufe Ihres Projekts als Tools innerhalb eines MCP-Servers zu. Erfahren Sie mehr in unserem",
"mcp.serverGuideLink": "Leitfaden für Projekte als MCP-Server.",
"mcp.serverNotRunning": "Der MCP-Server läuft nicht: {{message}}",
"mcp.serverTitle": "MCP-Server",
"mcp.servers.addButton": "MCP-Server hinzufügen",
"mcp.servers.addedServers": "MCP-Server hinzugefügt",
Expand Down Expand Up @@ -1551,6 +1555,8 @@
"paramRender.addOrEditData": "Daten hinzufügen oder bearbeiten",
"playground.cancelButton": "Abbrechen",
"playground.defaultSession": "Standardsitzung",
"playground.noInputHint": "Fügen Sie Ihrem Ablauf eine „ <1>Chat “ ( Input</1> )-Komponente hinzu, um Nachrichten zu versenden.",
"playground.runFlow": "Ablauf",
"playground.sendButton": "Senden",
"playgroundComponent.chatSessions": "Chat-Sitzungen",
"playgroundComponent.clearChat": "Chat löschen",
Expand Down Expand Up @@ -2060,5 +2066,13 @@
"voiceAssistant.apiKeyError": "Bitte überprüfen Sie Ihren API-Schlüssel und versuchen Sie es erneut",
"voiceAssistant.connected": "Verbunden",
"voiceAssistant.connectionError": "Verbindungsfehler",
"voiceAssistant.connectionFailed": "Verbindung fehlgeschlagen"
"voiceAssistant.connectionFailed": "Verbindung fehlgeschlagen",
"header.home": "Go to home",
"flows.viewList": "List view",
"flows.viewGrid": "Grid view",
"flows.downloadSelected": "Download selected flows",
"flows.selectFlow": "Select {{name}}",
"flows.moreOptions": "More options for {{name}}",
"folder.optionsFor": "Options for {{name}}",
"paginator.pageLabel": "Page"
}
Loading
Loading