diff --git a/.openhands/microagents/repo.md b/.openhands/microagents/repo.md
index cd3ef33074..c9dba61f31 100644
--- a/.openhands/microagents/repo.md
+++ b/.openhands/microagents/repo.md
@@ -51,7 +51,27 @@ Frontend:
- Testing:
- Run tests: `npm run test`
- To run specific tests: `npm run test -- -t "TestName"`
+ - To run a specific test file: `npm run test -- __tests__/path/to/test.tsx`
- Our test framework is vitest
+- E2E/Integration Testing with Playwright:
+ - Playwright can be used for end-to-end testing of the frontend UI
+ - Install Playwright: `npm install -D playwright && npx playwright install chromium`
+ - Run the frontend with mock API: `npm run dev:mock`
+ - Mock handlers are located in `frontend/src/mocks/` directory
+ - Example test script using Playwright:
+ ```typescript
+ import { chromium } from 'playwright';
+
+ async function testFeature() {
+ const browser = await chromium.launch({ headless: true });
+ const page = await browser.newPage();
+ await page.goto('http://localhost:3001');
+ // Interact with the page using Playwright API
+ await browser.close();
+ }
+ ```
+ - Run TypeScript test scripts with: `npx tsx test-script.ts`
+ - For video recording, use: `const context = await browser.newContext({ recordVideo: { dir: './videos' } });`
- Building:
- Build for production: `npm run build`
- Environment Variables:
diff --git a/frontend/__tests__/hooks/use-infinite-scroll.test.tsx b/frontend/__tests__/hooks/use-infinite-scroll.test.tsx
new file mode 100644
index 0000000000..22e87c371b
--- /dev/null
+++ b/frontend/__tests__/hooks/use-infinite-scroll.test.tsx
@@ -0,0 +1,175 @@
+import { render, screen, act } from "@testing-library/react";
+import { expect, test, vi, beforeEach, afterEach } from "vitest";
+import { useInfiniteScroll } from "#/hooks/use-infinite-scroll";
+
+interface InfiniteScrollTestComponentProps {
+ hasNextPage: boolean;
+ isFetchingNextPage: boolean;
+ fetchNextPage: () => void;
+ threshold?: number;
+}
+
+function InfiniteScrollTestComponent({
+ hasNextPage,
+ isFetchingNextPage,
+ fetchNextPage,
+ threshold = 100,
+}: InfiniteScrollTestComponentProps) {
+ const { ref } = useInfiniteScroll({
+ hasNextPage,
+ isFetchingNextPage,
+ fetchNextPage,
+ threshold,
+ });
+
+ return (
+
+ );
+}
+
+beforeEach(() => {
+ // Mock scrollHeight, clientHeight, and scrollTop
+ Object.defineProperty(HTMLElement.prototype, "scrollHeight", {
+ configurable: true,
+ get() {
+ return 1000;
+ },
+ });
+ Object.defineProperty(HTMLElement.prototype, "clientHeight", {
+ configurable: true,
+ get() {
+ return 200;
+ },
+ });
+});
+
+afterEach(() => {
+ vi.restoreAllMocks();
+});
+
+test("should call fetchNextPage when scrolled near bottom", async () => {
+ const fetchNextPage = vi.fn();
+
+ render(
+ ,
+ );
+
+ const container = screen.getByTestId("scroll-container");
+
+ // Simulate scrolling near the bottom (scrollTop + clientHeight + threshold >= scrollHeight)
+ // scrollHeight = 1000, clientHeight = 200, threshold = 100
+ // Need scrollTop >= 1000 - 200 - 100 = 700
+ await act(async () => {
+ Object.defineProperty(container, "scrollTop", {
+ configurable: true,
+ value: 750,
+ });
+ container.dispatchEvent(new Event("scroll"));
+ });
+
+ expect(fetchNextPage).toHaveBeenCalled();
+});
+
+test("should not call fetchNextPage when not scrolled near bottom", async () => {
+ const fetchNextPage = vi.fn();
+
+ render(
+ ,
+ );
+
+ const container = screen.getByTestId("scroll-container");
+
+ // Simulate scrolling but not near the bottom
+ await act(async () => {
+ Object.defineProperty(container, "scrollTop", {
+ configurable: true,
+ value: 100,
+ });
+ container.dispatchEvent(new Event("scroll"));
+ });
+
+ expect(fetchNextPage).not.toHaveBeenCalled();
+});
+
+test("should not call fetchNextPage when hasNextPage is false", async () => {
+ const fetchNextPage = vi.fn();
+
+ render(
+ ,
+ );
+
+ const container = screen.getByTestId("scroll-container");
+
+ // Simulate scrolling near the bottom
+ await act(async () => {
+ Object.defineProperty(container, "scrollTop", {
+ configurable: true,
+ value: 750,
+ });
+ container.dispatchEvent(new Event("scroll"));
+ });
+
+ expect(fetchNextPage).not.toHaveBeenCalled();
+});
+
+test("should not call fetchNextPage when already fetching", async () => {
+ const fetchNextPage = vi.fn();
+
+ render(
+ ,
+ );
+
+ const container = screen.getByTestId("scroll-container");
+
+ // Simulate scrolling near the bottom
+ await act(async () => {
+ Object.defineProperty(container, "scrollTop", {
+ configurable: true,
+ value: 750,
+ });
+ container.dispatchEvent(new Event("scroll"));
+ });
+
+ expect(fetchNextPage).not.toHaveBeenCalled();
+});
+
+test("should return a callback ref that can be assigned to elements", () => {
+ const fetchNextPage = vi.fn();
+
+ render(
+ ,
+ );
+
+ const container = screen.getByTestId("scroll-container");
+ expect(container).toBeInTheDocument();
+});
diff --git a/frontend/src/mocks/settings-handlers.ts b/frontend/src/mocks/settings-handlers.ts
index c08cd8dc36..a87fab4522 100644
--- a/frontend/src/mocks/settings-handlers.ts
+++ b/frontend/src/mocks/settings-handlers.ts
@@ -29,7 +29,8 @@ export const MOCK_DEFAULT_USER_SETTINGS: Settings = {
const MOCK_USER_PREFERENCES: {
settings: Settings | null;
} = {
- settings: null,
+ // Initialize with default settings to avoid the settings modal popup during testing
+ settings: { ...MOCK_DEFAULT_USER_SETTINGS },
};
// Reset mock