Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { axe } from "@/utils/a11y-test";
import CrashErrorComponent from "../index";

const renderCrashScreen = (resetErrorBoundary = jest.fn()) =>
render(
<CrashErrorComponent
error={{ message: "boom", stack: "at boom" }}
resetErrorBoundary={resetErrorBoundary}
/>,
);

describe("CrashErrorComponent accessibility", () => {
it("should_have_no_axe_violations", async () => {
const { container } = renderCrashScreen();

expect(await axe(container)).toHaveNoViolations();
});

// The crash screen replaces the whole app, so it owns the page's heading
// structure. Rendering the title as a <p> left the page with zero headings
// (WCAG 1.3.1 / 2.4.6).
it("should_expose_the_title_as_the_page_heading", () => {
renderCrashScreen();

expect(
screen.getByRole("heading", { level: 1, name: /unexpected error/i }),
).toBeInTheDocument();
});

// Nothing announced the failure to a screen reader: the boundary swaps the
// tree without moving focus, so the region has to assert itself (WCAG 4.1.3).
it("should_announce_the_failure_through_an_alert_region", () => {
renderCrashScreen();

expect(screen.getByRole("alert")).toContainElement(
screen.getByRole("heading", { level: 1 }),
);
});

// The report action used to be a <button> nested inside an <a href>, which
// is a nested-interactive violation and gives the anchor no reachable name
// of its own (WCAG 4.1.2).
it("should_render_the_report_action_as_a_single_link", () => {
renderCrashScreen();

const report = screen.getByRole("link", { name: /report on github/i });
expect(report).toHaveAttribute(
"href",
"https://github.com/langflow-ai/langflow/issues/new",
);
expect(report.querySelector("button")).toBeNull();
expect(
screen.queryByRole("button", { name: /report on github/i }),
).not.toBeInTheDocument();
});

it("should_reset_the_error_boundary_from_the_restart_button", async () => {
const user = userEvent.setup();
const resetErrorBoundary = jest.fn();
renderCrashScreen(resetErrorBoundary);

await user.click(screen.getByRole("button", { name: /restart/i }));

expect(resetErrorBoundary).toHaveBeenCalled();
});
});
28 changes: 16 additions & 12 deletions src/frontend/src/components/common/crashErrorComponent/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,19 @@ export default function CrashErrorComponent({
<div className="z-50 flex h-screen w-screen items-center justify-center bg-foreground bg-opacity-50">
<div className="flex h-screen w-screen flex-col bg-background text-start shadow-lg">
<main className="m-auto grid w-1/2 justify-center gap-5 text-center">
<Card className="p-8">
<Card className="p-8" role="alert">
<CardHeader>
<div className="m-auto">
<XCircle strokeWidth={1.5} className="h-16 w-16" />
<XCircle
strokeWidth={1.5}
className="h-16 w-16"
aria-hidden="true"
/>
</div>
<div>
<p className="mb-4 text-xl text-foreground">
<h1 className="mb-4 text-xl text-foreground">
{t("crash.title")}
</p>
</h1>
</div>
</CardHeader>

Expand Down Expand Up @@ -50,15 +54,15 @@ export default function CrashErrorComponent({
{t("crash.restartButton")}
</Button>

<a
href="https://github.com/langflow-ai/langflow/issues/new"
target="_blank"
rel="noopener noreferrer"
>
<Button className="ml-3" ignoreTitleCase variant={"outline"}>
<Button className="ml-3" variant="outline" asChild>
<a
href="https://github.com/langflow-ai/langflow/issues/new"
target="_blank"
rel="noopener noreferrer"
>
{t("crash.reportButton")}
</Button>
</a>
</a>
</Button>
</div>
</CardFooter>
</Card>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render } from "@testing-library/react";
import { render, screen } from "@testing-library/react";
import type { ReactNode } from "react";
import type { APIClassType } from "@/types/api";
import Dropdown from "../index";
Expand Down Expand Up @@ -190,3 +190,26 @@ describe("Dropdown value reset bug", () => {
expect(mockOnSelect).not.toHaveBeenCalledWith("", undefined, true);
});
});

describe("Dropdown accessibility", () => {
// The search box is a bare <input> that only carried a placeholder, which is
// not an accessible name (WCAG 4.1.2 / 3.3.2).
it("should_name_the_search_input", () => {
render(
<Dropdown
value="tool_a"
options={["tool_a", "tool_b"]}
onSelect={jest.fn()}
name="tool"
nodeId="test-node"
nodeClass={mockNodeClass}
handleNodeClass={jest.fn()}
id="test-dropdown"
/>,
);

expect(screen.getByTestId("dropdown_search_input")).toHaveAccessibleName(
"Search options...",
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,7 @@ export default function Dropdown({
onChange={searchRoleByTerm}
onKeyDown={handleInputKeyDown}
placeholder={t("input.searchOptions")}
aria-label={t("input.searchOptions")}
className="flex h-9 w-full rounded-md bg-transparent py-3 text-[13px] outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
autoComplete="off"
data-testid="dropdown_search_input"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,34 +192,38 @@ export const EditFlowSettings: React.FC<
<Form.Message match="valueMissing" className="field-invalid">
{t("flow.pleaseEnterDescription")}
</Form.Message>
<div className="mt-3">
<div className="flex items-center gap-2">
<div>
<div className="flex items-center gap-2">
<Form.Label className="text-mmd font-medium">
{t("flow.lockFlow")}
</Form.Label>
{/* Callers that only display the flow (no setLocked) would otherwise
render a focusable switch that cannot change anything. */}
{setLocked && (
<div className="mt-3">
<div className="flex items-center gap-2">
<div>
<div className="flex items-center gap-2">
<Form.Label className="text-mmd font-medium">
{t("flow.lockFlow")}
</Form.Label>

<ForwardedIconComponent
name={locked ? "Lock" : "Unlock"}
className="text-muted-foreground !w-5 !h-5"
/>
<ForwardedIconComponent
name={locked ? "Lock" : "Unlock"}
className="text-muted-foreground !w-5 !h-5"
/>
</div>

<p className="text-xs text-muted-foreground/70 mt-1 font-normal">
{t("flow.lockFlowDescription")}
</p>
</div>

<p className="text-xs text-muted-foreground/70 mt-1 font-normal">
{t("flow.lockFlowDescription")}
</p>
<Switch
checked={!!locked}
onCheckedChange={(v) => setLocked(v)}
disabled={readOnly}
className="data-[state=checked]:bg-primary ml-auto"
data-testid="lock-flow-switch"
/>
</div>

<Switch
checked={!!locked}
onCheckedChange={(v) => setLocked?.(v)}
disabled={readOnly}
className="data-[state=checked]:bg-primary ml-auto"
data-testid="lock-flow-switch"
/>
</div>
</div>
)}
</Form.Field>
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { PopoverAnchor } from "@radix-ui/react-popover";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import ForwardedIconComponent from "@/components/common/genericIconComponent";
import {
Command,
Expand Down Expand Up @@ -43,6 +44,7 @@ const CustomInputPopoverObject = ({
showOptions,
inspectionPanel,
}) => {
const { t } = useTranslation();
const PopoverContentInput =
editNode || inspectionPanel ? PopoverContent : PopoverContentWithoutPortal;

Expand Down Expand Up @@ -122,6 +124,7 @@ const CustomInputPopoverObject = ({
align="center"
>
<Command
label={optionsPlaceholder || t("input.searchOptions")}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -e json . -p 'locales' src/frontend/src | xargs -I{} sh -c 'echo === {} ===; jq ".input.searchOptions" {} 2>/dev/null || grep -n "searchOptions" {}'

Repository: langflow-ai/langflow

Length of output: 539


Add input.searchOptions to all shipped locale files.

The locale files under src/frontend/src/locales/ do not define this key, so non-English users will get a raw key or blank label instead of the translated search-options label.

🤖 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/parameterRenderComponent/components/inputComponent/components/popoverObject/index.tsx`
at line 127, Add the input.searchOptions translation key to every shipped locale
file under src/frontend/src/locales/, providing the appropriate localized
search-options label while preserving the existing locale structure and key
naming.

filter={(value, search) => {
if (
value.toLowerCase().includes(search.toLowerCase()) ||
Expand Down
Loading
Loading