diff --git a/backend/tests/test_agent_guidance_check.py b/backend/tests/test_agent_guidance_check.py index f0252235db..23dc886d45 100644 --- a/backend/tests/test_agent_guidance_check.py +++ b/backend/tests/test_agent_guidance_check.py @@ -54,6 +54,7 @@ EXPECTED_GUIDANCE_PATHS = { "backend/packages/harness/deerflow/utils/AGENTS.md", "frontend/src/AGENTS.md", "frontend/src/core/memory/AGENTS.md", + "frontend/tests/e2e/AGENTS.md", "scripts/AGENTS.md", "examples/deerflow-extension-agent-teams/AGENTS.md", "examples/deerflow-extension-workflows/AGENTS.md", diff --git a/frontend/tests/e2e/AGENTS.md b/frontend/tests/e2e/AGENTS.md new file mode 100644 index 0000000000..b62b8169fd --- /dev/null +++ b/frontend/tests/e2e/AGENTS.md @@ -0,0 +1,10 @@ +# Browser E2E tests + +Virtualized lists do not keep every fetched row mounted. Assert the pagination +request, then filter or scroll the loaded row into the rendered window before +checking its content. Preserve the real scroll trigger and search-mode guard. + +For message ordering across compaction/refetch, poll an atomic DOM comparison +until both nodes settle. Missing or reversed nodes must still fail; keep history, +refresh and hidden-summary assertions. Avoid fixed sleeps and whole-test retries +as substitutes for synchronization. diff --git a/frontend/tests/e2e/thread-list-infinite-scroll.spec.ts b/frontend/tests/e2e/thread-list-infinite-scroll.spec.ts index 763d3baa2d..cd93a308f5 100644 --- a/frontend/tests/e2e/thread-list-infinite-scroll.spec.ts +++ b/frontend/tests/e2e/thread-list-infinite-scroll.spec.ts @@ -64,6 +64,7 @@ test.describe("Thread list infinite scroll (issue #3482)", () => { test("chats list page loads more threads when scrolling to the bottom", async ({ page, }) => { + const offsets = observeSearchOffsets(page); mockLangGraphAPI(page, { threads: THREADS }); await page.goto("/workspace/chats"); @@ -80,7 +81,12 @@ test.describe("Thread list infinite scroll (issue #3482)", () => { // Scrolling the sentinel into view triggers the next page. const sentinel = page.getByTestId("chats-page-sentinel"); await sentinel.scrollIntoViewIfNeeded(); + await expect.poll(() => offsets).toContain(PAGE_SIZE); + // Crossing the virtualization threshold can unmount Conversation 051 + // while the scroll anchor stays near the bottom of the expanded list. + // Filter the fetched rows to verify the next page reached the UI. + await page.getByPlaceholder("Search chats").fill(SECOND_PAGE_FIRST); await expect(main.getByText(SECOND_PAGE_FIRST)).toBeVisible({ timeout: 15_000, }); diff --git a/frontend/tests/e2e/thread-ordering.spec.ts b/frontend/tests/e2e/thread-ordering.spec.ts index 8399ba0101..a10a8e22df 100644 --- a/frontend/tests/e2e/thread-ordering.spec.ts +++ b/frontend/tests/e2e/thread-ordering.spec.ts @@ -395,28 +395,35 @@ test.describe("Thread message ordering", () => { }); // DOM relative order, not viewport coordinates: stick-to-bottom smooth // scrolling makes two separate boundingBox reads race each other. - const questionBeforeAnswer = await page.evaluate(() => { - const list = document.querySelector('[data-testid="main-message-list"]'); - if (!list) { - return null; - } - const leaf = (text: string) => - [...list.querySelectorAll("div, p")].find( - (element) => - element.children.length === 0 && element.textContent === text, - ); - const question = leaf("final-turn question"); - const answer = leaf("final-turn answer"); - if (!question || !answer) { - return null; - } - return ( - (question.compareDocumentPosition(answer) & - Node.DOCUMENT_POSITION_FOLLOWING) !== - 0 - ); - }); - expect(questionBeforeAnswer).toBe(true); + // Compaction and the finishing history refetch can briefly replace + // these nodes. Re-read both in one DOM snapshot until they settle. + await expect + .poll(() => + page.evaluate(() => { + const list = document.querySelector( + '[data-testid="main-message-list"]', + ); + if (!list) { + return null; + } + const leaf = (text: string) => + [...list.querySelectorAll("div, p")].find( + (element) => + element.children.length === 0 && element.textContent === text, + ); + const question = leaf("final-turn question"); + const answer = leaf("final-turn answer"); + if (!question || !answer) { + return null; + } + return ( + (question.compareDocumentPosition(answer) & + Node.DOCUMENT_POSITION_FOLLOWING) !== + 0 + ); + }), + ) + .toBe(true); // The finishing refetch observed the extended feed; the established // order — including the compacted head — is unchanged.