mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-10-07 17:28:03 +08:00
fix: render Automations pages flush to viewport edges (#225)
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { Sidebar } from "#/components/features/sidebar/sidebar";
|
||||
import {
|
||||
NavigationProvider,
|
||||
type NavigationContextValue,
|
||||
} from "#/context/navigation-context";
|
||||
|
||||
vi.mock("#/hooks/query/use-git-user", () => ({
|
||||
useGitUser: () => ({ data: undefined, isFetching: false }),
|
||||
}));
|
||||
|
||||
vi.mock("#/hooks/query/use-config", () => ({
|
||||
useConfig: () => ({ data: { feature_flags: {} } }),
|
||||
}));
|
||||
|
||||
vi.mock("#/hooks/query/use-settings", () => ({
|
||||
useSettings: () => ({
|
||||
data: { email_verified: true },
|
||||
error: null,
|
||||
isError: false,
|
||||
isFetching: false,
|
||||
}),
|
||||
getErrorStatus: () => undefined,
|
||||
}));
|
||||
|
||||
vi.mock("#/components/shared/buttons/styled-tooltip", () => ({
|
||||
StyledTooltip: ({ children }: { children: unknown }) => children,
|
||||
}));
|
||||
|
||||
vi.mock("#/components/shared/buttons/openhands-logo-button", () => ({
|
||||
OpenHandsLogoButton: () => <div data-testid="logo-button" />,
|
||||
}));
|
||||
|
||||
vi.mock("#/components/shared/buttons/new-project-button", () => ({
|
||||
NewProjectButton: () => <div data-testid="new-project-button" />,
|
||||
}));
|
||||
|
||||
vi.mock("#/components/shared/buttons/conversation-panel-button", () => ({
|
||||
ConversationPanelButton: () => (
|
||||
<div data-testid="conversation-panel-button" />
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("#/components/shared/buttons/automations-button", () => ({
|
||||
AutomationsButton: () => <div data-testid="automations-button" />,
|
||||
}));
|
||||
|
||||
vi.mock("#/components/features/sidebar/user-actions", () => ({
|
||||
UserActions: () => <div data-testid="user-actions" />,
|
||||
}));
|
||||
|
||||
vi.mock("#/components/features/conversation-panel/conversation-panel", () => ({
|
||||
ConversationPanel: () => null,
|
||||
}));
|
||||
|
||||
vi.mock(
|
||||
"#/components/features/conversation-panel/conversation-panel-wrapper",
|
||||
() => ({
|
||||
ConversationPanelWrapper: () => null,
|
||||
}),
|
||||
);
|
||||
|
||||
vi.mock("#/components/shared/modals/settings/settings-modal", () => ({
|
||||
SettingsModal: () => null,
|
||||
}));
|
||||
|
||||
function renderSidebar(currentPath: string) {
|
||||
const value: NavigationContextValue = {
|
||||
currentPath,
|
||||
conversationId: null,
|
||||
isNavigating: false,
|
||||
navigate: vi.fn(),
|
||||
};
|
||||
|
||||
return render(
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<NavigationProvider value={value}>
|
||||
<Sidebar />
|
||||
</NavigationProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("Sidebar", () => {
|
||||
it.each([["/automations"], ["/automations/abc-123"]])(
|
||||
"applies the standalone vertical padding on %s so the sidebar has breathing room when the root layout's padding is dropped",
|
||||
(currentPath) => {
|
||||
renderSidebar(currentPath);
|
||||
|
||||
const sidebar = screen.getByRole("navigation").parentElement;
|
||||
expect(sidebar?.className).toMatch(/(^|\s)md:pt-6\.5(\s|$)/);
|
||||
expect(sidebar?.className).toMatch(/(^|\s)md:pb-3(\s|$)/);
|
||||
},
|
||||
);
|
||||
|
||||
it("does not apply the standalone vertical padding on routes that still use the root layout's padding", () => {
|
||||
renderSidebar("/settings");
|
||||
|
||||
const sidebar = screen.getByRole("navigation").parentElement;
|
||||
expect(sidebar?.className).not.toMatch(/(^|\s)md:pt-6\.5(\s|$)/);
|
||||
expect(sidebar?.className).not.toMatch(/(^|\s)md:pb-3(\s|$)/);
|
||||
});
|
||||
});
|
||||
@@ -39,7 +39,9 @@ vi.mock("#/components/features/sidebar/sidebar", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("#/components/features/analytics/analytics-consent-form-modal", () => ({
|
||||
AnalyticsConsentFormModal: () => <div data-testid="analytics-consent-modal" />,
|
||||
AnalyticsConsentFormModal: () => (
|
||||
<div data-testid="analytics-consent-modal" />
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("#/components/features/alerts/alert-banner", () => ({
|
||||
@@ -61,6 +63,18 @@ const RouterStub = createRoutesStub([
|
||||
path: "/",
|
||||
Component: () => <div data-testid="outlet-content" />,
|
||||
},
|
||||
{
|
||||
path: "/automations",
|
||||
Component: () => <div data-testid="outlet-content" />,
|
||||
},
|
||||
{
|
||||
path: "/automations/:id",
|
||||
Component: () => <div data-testid="outlet-content" />,
|
||||
},
|
||||
{
|
||||
path: "/settings",
|
||||
Component: () => <div data-testid="outlet-content" />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
@@ -116,4 +130,31 @@ describe("root layout", () => {
|
||||
expect(screen.getByTestId("analytics-consent-modal")).toBeInTheDocument();
|
||||
expect(migrateUserConsentMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([["/automations"], ["/automations/abc-123"]])(
|
||||
"drops the outer layout padding on %s so the page can render flush to the viewport edges",
|
||||
(path) => {
|
||||
render(
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<RouterStub initialEntries={[path]} />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
const layout = screen.getByTestId("root-layout");
|
||||
expect(layout.className).not.toMatch(/(^|\s)md:p-3(\s|$)/);
|
||||
expect(layout.className).not.toMatch(/(^|\s)md:pl-0(\s|$)/);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps the outer layout padding on routes other than home and automations", () => {
|
||||
render(
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<RouterStub initialEntries={["/settings"]} />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
const layout = screen.getByTestId("root-layout");
|
||||
expect(layout.className).toMatch(/(^|\s)md:p-3(\s|$)/);
|
||||
expect(layout.className).toMatch(/(^|\s)md:pl-0(\s|$)/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -67,7 +67,8 @@ export function Sidebar() {
|
||||
aria-label={t(I18nKey.SIDEBAR$NAVIGATION_LABEL)}
|
||||
className={cn(
|
||||
"h-[54px] p-3 md:p-0 md:h-[40px] md:h-auto flex flex-row md:flex-col gap-1 bg-base md:w-[75px] md:min-w-[75px] sm:pt-0 sm:px-2 md:pt-[14px] md:px-0",
|
||||
currentPath === "/" && "md:pt-6.5 md:pb-3",
|
||||
(currentPath === "/" || currentPath.startsWith("/automations")) &&
|
||||
"md:pt-6.5 md:pb-3",
|
||||
)}
|
||||
>
|
||||
<nav className="flex flex-row md:flex-col items-center justify-between w-full h-auto md:w-auto md:h-full">
|
||||
|
||||
@@ -97,7 +97,9 @@ export default function MainApp() {
|
||||
data-testid="root-layout"
|
||||
className={cn(
|
||||
"h-screen lg:min-w-5xl flex flex-col md:flex-row bg-base overflow-hidden",
|
||||
pathname === "/" ? "p-0" : "p-0 md:p-3 md:pl-0",
|
||||
pathname === "/" || pathname.startsWith("/automations")
|
||||
? "p-0"
|
||||
: "p-0 md:p-3 md:pl-0",
|
||||
)}
|
||||
>
|
||||
<title>{appTitle}</title>
|
||||
|
||||
Reference in New Issue
Block a user