Add unit tests for useInfiniteScroll hook and Playwright documentation

- Add 5 unit tests for useInfiniteScroll hook covering:
  - Scroll near bottom triggers fetchNextPage
  - Scroll not near bottom doesn't trigger fetch
  - hasNextPage=false doesn't trigger fetch
  - isFetchingNextPage=true doesn't trigger fetch
  - Callback ref properly attaches to container
- Add Playwright E2E testing documentation to repo.md
- Initialize mock settings with defaults to avoid modal popup during testing

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
openhands 2025-12-18 17:13:35 +00:00
parent a2a24a753a
commit 02f69f02c0
3 changed files with 197 additions and 1 deletions

View File

@ -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:

View File

@ -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 (
<div
data-testid="scroll-container"
ref={ref}
style={{ height: "200px", overflow: "auto" }}
>
<div style={{ height: "1000px" }}>Scrollable content</div>
</div>
);
}
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(
<InfiniteScrollTestComponent
hasNextPage
isFetchingNextPage={false}
fetchNextPage={fetchNextPage}
threshold={100}
/>,
);
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(
<InfiniteScrollTestComponent
hasNextPage
isFetchingNextPage={false}
fetchNextPage={fetchNextPage}
threshold={100}
/>,
);
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(
<InfiniteScrollTestComponent
hasNextPage={false}
isFetchingNextPage={false}
fetchNextPage={fetchNextPage}
threshold={100}
/>,
);
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(
<InfiniteScrollTestComponent
hasNextPage
isFetchingNextPage
fetchNextPage={fetchNextPage}
threshold={100}
/>,
);
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(
<InfiniteScrollTestComponent
hasNextPage
isFetchingNextPage={false}
fetchNextPage={fetchNextPage}
/>,
);
const container = screen.getByTestId("scroll-container");
expect(container).toBeInTheDocument();
});

View File

@ -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