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 ( +
+
Scrollable content
+
+ ); +} + +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