From 1a60832fad6e21d4ee2dfa0963963fdb2cac953b Mon Sep 17 00:00:00 2001 From: Will Chen <7344640+wwwillchen@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:03:20 -0700 Subject: [PATCH 1/3] deflake Lexical chat input interactions in e2e tests Stabilize sendPrompt by retrying click/fill assertions against a single scoped editor locator and add a short settle wait in chat tab navigation to avoid intermittent prompt entry races on slower machines. Made-with: Cursor --- e2e-tests/chat_tabs.spec.ts | 1 + .../page-objects/components/ChatActions.ts | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/e2e-tests/chat_tabs.spec.ts b/e2e-tests/chat_tabs.spec.ts index 3d93a2b736..f8446d05b5 100644 --- a/e2e-tests/chat_tabs.spec.ts +++ b/e2e-tests/chat_tabs.spec.ts @@ -11,6 +11,7 @@ test("tabs appear after navigating between chats", async ({ po }) => { // Chat 2 await po.chatActions.clickNewChat(); + await po.sleep(2_000); await po.sendPrompt("[dump] build a weather app"); await po.chatActions.waitForChatCompletion(); diff --git a/e2e-tests/helpers/page-objects/components/ChatActions.ts b/e2e-tests/helpers/page-objects/components/ChatActions.ts index 4755d03a5b..b7c27859a7 100644 --- a/e2e-tests/helpers/page-objects/components/ChatActions.ts +++ b/e2e-tests/helpers/page-objects/components/ChatActions.ts @@ -88,9 +88,20 @@ export class ChatActions { timeout, }: { skipWaitForCompletion?: boolean; timeout?: number } = {}, ) { - await this.getChatInput().click(); - await this.getChatInput().fill(prompt); - await this.page.getByRole("button", { name: "Send message" }).click(); + const chatInput = this.getChatInputContainer().locator( + '[data-lexical-editor="true"]', + ); + const sendButton = this.page.getByRole("button", { name: "Send message" }); + + await expect(chatInput).toBeVisible(); + await expect(async () => { + await chatInput.click(); + await chatInput.fill(prompt); + await expect(chatInput).toContainText(prompt); + await expect(sendButton).toBeEnabled(); + }).toPass({ timeout: Timeout.SHORT }); + + await sendButton.click(); if (!skipWaitForCompletion) { await this.waitForChatCompletion({ timeout }); } From 517765286082e290ff2eb6a97f0a0230b28f6fe9 Mon Sep 17 00:00:00 2001 From: Will Chen <7344640+wwwillchen@users.noreply.github.com> Date: Thu, 9 Apr 2026 12:07:08 -0700 Subject: [PATCH 2/3] docs: record session learnings Made-with: Cursor --- rules/e2e-testing.md | 1 + 1 file changed, 1 insertion(+) diff --git a/rules/e2e-testing.md b/rules/e2e-testing.md index 82769add39..1ab2ca51ea 100644 --- a/rules/e2e-testing.md +++ b/rules/e2e-testing.md @@ -61,6 +61,7 @@ The chat input uses a Lexical editor (contenteditable). Standard Playwright meth - **Clearing input**: `fill("")` doesn't reliably clear Lexical. Use keyboard shortcuts instead: `Meta+a` then `Backspace`. - **Timing issues**: Lexical may need time to update its internal state. Use `toPass()` with retries for resilient tests. +- **Avoid locator drift**: When both home/chat inputs may exist, scope the editor locator to the specific container (for example `chat-input-container`) and reuse one locator instance for click/fill/assertions. - **Helper methods**: Use `po.clearChatInput()` and `po.openChatHistoryMenu()` from test_helper.ts for reliable Lexical interactions. ```ts From c2d660a9d9c7efece98e99f6e45eb5677cf6c799 Mon Sep 17 00:00:00 2001 From: Will Chen <7344640+wwwillchen@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:46:28 -0700 Subject: [PATCH 3/3] test: keep resilient prompt entry across chat inputs --- e2e-tests/chat_tabs.spec.ts | 13 ++++++------- .../helpers/page-objects/components/ChatActions.ts | 6 ++---- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/e2e-tests/chat_tabs.spec.ts b/e2e-tests/chat_tabs.spec.ts index f8446d05b5..ea7e71f277 100644 --- a/e2e-tests/chat_tabs.spec.ts +++ b/e2e-tests/chat_tabs.spec.ts @@ -3,7 +3,7 @@ import { expect } from "@playwright/test"; test("tabs appear after navigating between chats", async ({ po }) => { await po.setUp({ autoApprove: true }); - await po.importApp("minimal"); + await po.importApp("minimal-with-ai-rules"); // Chat 1 await po.sendPrompt("[dump] build a todo app"); @@ -11,7 +11,6 @@ test("tabs appear after navigating between chats", async ({ po }) => { // Chat 2 await po.chatActions.clickNewChat(); - await po.sleep(2_000); await po.sendPrompt("[dump] build a weather app"); await po.chatActions.waitForChatCompletion(); @@ -25,7 +24,7 @@ test("tabs appear after navigating between chats", async ({ po }) => { test("clicking a tab switches to that chat", async ({ po }) => { await po.setUp({ autoApprove: true }); - await po.importApp("minimal"); + await po.importApp("minimal-with-ai-rules"); // Chat 1 - send a unique message await po.sendPrompt("First chat unique message alpha"); @@ -58,7 +57,7 @@ test("clicking a tab switches to that chat", async ({ po }) => { test("closing a tab removes it and selects adjacent tab", async ({ po }) => { await po.setUp({ autoApprove: true }); - await po.importApp("minimal"); + await po.importApp("minimal-with-ai-rules"); // Chat 1 await po.sendPrompt("First chat message gamma"); @@ -100,7 +99,7 @@ test("closing a tab removes it and selects adjacent tab", async ({ po }) => { test("right-click context menu: Close other tabs", async ({ po }) => { await po.setUp({ autoApprove: true }); - await po.importApp("minimal"); + await po.importApp("minimal-with-ai-rules"); // Chat 1 await po.sendPrompt("[dump] Chat one context menu"); @@ -139,7 +138,7 @@ test("right-click context menu: Close other tabs", async ({ po }) => { test("right-click context menu: Close tabs to the right", async ({ po }) => { await po.setUp({ autoApprove: true }); - await po.importApp("minimal"); + await po.importApp("minimal-with-ai-rules"); // Chat 1 await po.sendPrompt("[dump] Left tab one"); @@ -183,7 +182,7 @@ test("right-click context menu: Close tabs to the right", async ({ po }) => { test("only shows tabs for chats opened in current session", async ({ po }) => { await po.setUp({ autoApprove: true }); - await po.importApp("minimal"); + await po.importApp("minimal-with-ai-rules"); // Initially no tabs should be visible (no chats opened yet in this session) const closeButtons = po.page.getByLabel(/^Close tab:/); diff --git a/e2e-tests/helpers/page-objects/components/ChatActions.ts b/e2e-tests/helpers/page-objects/components/ChatActions.ts index b7c27859a7..12c457d9fe 100644 --- a/e2e-tests/helpers/page-objects/components/ChatActions.ts +++ b/e2e-tests/helpers/page-objects/components/ChatActions.ts @@ -88,9 +88,7 @@ export class ChatActions { timeout, }: { skipWaitForCompletion?: boolean; timeout?: number } = {}, ) { - const chatInput = this.getChatInputContainer().locator( - '[data-lexical-editor="true"]', - ); + const chatInput = this.getChatInput(); const sendButton = this.page.getByRole("button", { name: "Send message" }); await expect(chatInput).toBeVisible(); @@ -99,7 +97,7 @@ export class ChatActions { await chatInput.fill(prompt); await expect(chatInput).toContainText(prompt); await expect(sendButton).toBeEnabled(); - }).toPass({ timeout: Timeout.SHORT }); + }).toPass({ timeout: Timeout.MEDIUM }); await sendButton.click(); if (!skipWaitForCompletion) {