Unverified 提交 2661ba3b authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

fix: update E2E tests for plan mode questionnaire and chat history (#2507)

## Summary - Fix plan_mode questionnaire test to use `getByText` for Base UI Radio components (hidden inputs can't be clicked with `getByRole`/`getByLabel`) - Update chat_history tests for chronological order (oldest first) - Add `clearChatInput()` and `openChatHistoryMenu()` helpers with `toPass()` retry logic for reliable Lexical editor interactions - Add `Timeout.SHORT` constant for quick retries - Update AGENTS.md with Lexical editor and Base UI Radio testing tips ## Test plan - [x] `npm run e2e e2e-tests/plan_mode.spec.ts` passes - [x] `npm run e2e e2e-tests/chat_history.spec.ts` passes - [x] `npm test` passes 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/2507" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Changes are confined to E2E tests, developer docs, and pre-commit tooling; product/runtime logic is untouched, with minimal risk beyond potential CI formatting or test expectation drift. > > **Overview** > Stabilizes Playwright E2E coverage around plan mode and chat history by switching Base UI Radio selection to clicking visible label text and by adding Lexical-safe input helpers (`clearChatInput`, `openChatHistoryMenu`) with short `toPass()` retries. > > Updates chat history assertions to match **chronological ordering** (oldest-first) and refreshes related snapshots/docs. Also tightens pre-commit formatting via `lint-staged` (limits `oxfmt` to a narrower glob) and simplifies the Husky `pre-commit` hook to only run `lint-staged`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 11c4809a66819c53a19042450b5da93ef3f1b29c. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Fixes flaky E2E tests for plan mode and chat history by using reliable selectors and Lexical-safe helpers. Radios can be selected via visible labels, and chat history tests now match chronological order. - **Bug Fixes** - Plan mode: select Base UI Radio by clicking label text with getByText. - Chat history: update tests for oldest-first order and default selection behavior. - Helpers and docs: add clearChatInput/openChatHistoryMenu with toPass retries and Timeout.SHORT; update AGENTS.md with Lexical and Base UI tips. - Pre-commit: remove YAML from oxfmt to fix hook failures. <sup>Written for commit b03d72d2605d98201e7d68231ac381ffb7d64f63. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: 's avatarClaude Opus 4.5 <noreply@anthropic.com> Co-authored-by: 's avatargithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
上级 485c8b8d
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
......@@ -243,14 +243,34 @@ When running GitHub Actions with `pull_request_target` on cross-repo PRs (from f
### Base UI Radio component selection in Playwright
When testing Base UI Radio components in Playwright E2E tests, use `getByRole('radio', { name: 'Label' })` instead of `getByLabel('Label')`. The `getByLabel` selector may not work correctly with Base UI's Radio component structure.
Base UI Radio components render a hidden native `<input type="radio">` with `aria-hidden="true"`. Both `getByRole('radio', { name: '...' })` and `getByLabel('...')` find this hidden input but can't click it (element is outside viewport). Use `getByText` to click the visible label text instead.
```ts
// Correct: use role selector
await page.getByRole("radio", { name: "React" }).click();
// Correct: click the visible label text
await page.getByText("Vue", { exact: true }).click();
// May not work with Base UI Radio
await page.getByLabel("React").click();
// Won't work: finds hidden input, can't click
await page.getByRole("radio", { name: "Vue" }).click();
await page.getByLabel("Vue").click();
```
### Lexical editor in Playwright E2E tests
The chat input uses a Lexical editor (contenteditable). Standard Playwright methods don't always work:
- **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.
- **Helper methods**: Use `po.clearChatInput()` and `po.openChatHistoryMenu()` from test_helper.ts for reliable Lexical interactions.
```ts
// Wrong: may not clear Lexical editor
await chatInput.fill("");
// Correct: use helper with retry logic
await po.clearChatInput();
// For history menu (needs clear + ArrowUp with retries)
await po.openChatHistoryMenu();
```
### Drizzle migration conflicts during rebase
......
......@@ -23,47 +23,45 @@ test("should open, navigate, and select from history menu", async ({ po }) => {
const historyMenu = po.page.locator('[data-mentions-menu="true"]');
await expect(historyMenu).toBeVisible();
// Verify menu has items (newest at top, oldest at bottom - array is reversed)
// Verify menu has items (oldest at top, newest at bottom - chronological order)
const menuItems = po.page.locator('[data-mentions-menu="true"] li');
await expect(menuItems).toHaveCount(2);
await expect(menuItems.nth(0)).toContainText("Second test message");
await expect(menuItems.nth(1)).toContainText("First test message");
await expect(menuItems.nth(0)).toContainText("First test message");
await expect(menuItems.nth(1)).toContainText("Second test message");
// Verify default selection is the last visible item (oldest message, at bottom)
// Verify default selection is the last visible item (newest message, at bottom)
// After opening, a synthetic ArrowUp is dispatched which wraps to the bottom item
const lastItem = menuItems.nth(1);
await expect(lastItem).toHaveClass(/bg-accent/, { timeout: 500 });
// Navigate up to first item (newest message)
// Navigate up to first item (oldest message)
await po.page.keyboard.press("ArrowUp");
const firstItem = menuItems.nth(0);
await expect(firstItem).toHaveClass(/bg-accent/);
await expect(firstItem).toContainText("Second test message");
await expect(firstItem).toContainText("First test message");
// Navigate up again to wrap to last item (oldest message)
// Navigate up again to wrap to last item (newest message)
await po.page.keyboard.press("ArrowUp");
await expect(lastItem).toHaveClass(/bg-accent/);
// Select with Enter (selects oldest message)
// Select with Enter (selects newest message)
await po.page.keyboard.press("Enter");
// Menu should close and text should be inserted
await expect(historyMenu).not.toBeVisible({ timeout: Timeout.MEDIUM });
await expect(chatInput).toContainText("First test message", {
await expect(chatInput).toContainText("Second test message", {
timeout: Timeout.MEDIUM,
});
// Clear input for mouse click test
await chatInput.fill("");
await po.page.keyboard.press("ArrowUp");
await expect(historyMenu).toBeVisible();
await po.openChatHistoryMenu();
// Click the first item (newest message, at top)
// Click the first item (oldest message, at top)
await menuItems.nth(0).click();
// Verify menu closed and newest message was inserted
// Verify menu closed and oldest message was inserted
await expect(historyMenu).not.toBeVisible({ timeout: Timeout.MEDIUM });
await expect(chatInput).toContainText("Second test message", {
await expect(chatInput).toContainText("First test message", {
timeout: Timeout.MEDIUM,
});
});
......@@ -101,9 +99,7 @@ test("should handle edge cases: guards, escape, and sending after cancel", async
await expect(historyMenu).not.toBeVisible();
// Test 3: Escape closes menu and clears input
await chatInput.fill("");
await po.page.keyboard.press("ArrowUp");
await expect(historyMenu).toBeVisible();
await po.openChatHistoryMenu();
await po.page.keyboard.press("Escape");
await expect(historyMenu).not.toBeVisible();
......
......@@ -18,6 +18,7 @@ export const Timeout = {
EXTRA_LONG: process.env.CI ? 120_000 : 60_000,
LONG: process.env.CI ? 60_000 : 30_000,
MEDIUM: process.env.CI ? 30_000 : 15_000,
SHORT: process.env.CI ? 5_000 : 2_000,
};
/**
......@@ -965,6 +966,35 @@ export class PageObject {
);
}
/**
* Clears the Lexical chat input using keyboard shortcuts (Meta+A, Backspace).
* Uses toPass() for resilience since Lexical may need time to update its state.
*/
async clearChatInput() {
const chatInput = this.getChatInput();
await chatInput.click();
await this.page.keyboard.press("ControlOrMeta+a");
await this.page.keyboard.press("Backspace");
await expect(async () => {
const text = await chatInput.textContent();
expect(text?.trim()).toBe("");
}).toPass({ timeout: Timeout.SHORT });
}
/**
* Opens the chat history menu by clearing the input and pressing ArrowUp.
* Uses toPass() for resilience since the Lexical editor may need time to
* update its state before the history menu can be triggered.
*/
async openChatHistoryMenu() {
const historyMenu = this.page.locator('[data-mentions-menu="true"]');
await expect(async () => {
await this.clearChatInput();
await this.page.keyboard.press("ArrowUp");
await expect(historyMenu).toBeVisible({ timeout: 500 });
}).toPass({ timeout: Timeout.SHORT });
}
clickNewChat({ index = 0 }: { index?: number } = {}) {
// There is two new chat buttons...
return this.page
......
......@@ -66,8 +66,8 @@ test("plan mode - questionnaire flow", async ({ po }) => {
timeout: Timeout.MEDIUM,
});
// Select "React" radio option (using role selector for base-ui Radio component)
await po.page.getByRole("radio", { name: "React" }).click();
// Select "Vue" radio option by clicking the label text (Base UI Radio components)
await po.page.getByText("Vue", { exact: true }).click();
// Click Submit (single question → Submit button shown)
await po.page.getByRole("button", { name: /Submit/ }).click();
......
......@@ -16,8 +16,13 @@
]
},
{
"type": "item_reference",
"id": "[[ITEM_REF_0]]"
"role": "assistant",
"content": [
{
"type": "output_text",
"text": "\n <dyad-write path=\"file1.txt\">\n A file (2)\n </dyad-write>\n More\n EOM"
}
]
},
{
"role": "user",
......@@ -468,6 +473,9 @@
],
"tool_choice": "auto",
"stream": true,
"include": [
"reasoning.encrypted_content"
],
"dyad_options": {
"enable_lazy_edits": true,
"enable_smart_files_context": true
......
......@@ -19,7 +19,7 @@
- paragraph: tc=local-agent/read-logs
- paragraph: Let me check the recent console logs to see what's happening in the application.
- img
- text: LOGSReading 8 logs
- text: /LOGSReading \d+ logs/
- img
- paragraph: Now let me filter for only error logs to identify any issues.
- img
......
- paragraph: tc=local-agent/questionnaire
- paragraph: Let me ask you a few questions to understand your requirements.
- button:
- button "Copy":
- img
- img
- text: Approved
......@@ -8,28 +8,33 @@
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Request ID":
- button "Copy Request ID":
- img
- text: Request ID
- paragraph: "Here are my responses to the questionnaire:"
- paragraph:
- strong: Which framework do you prefer?
- text: React
- text: Vue
- img
- text: file1.txt
- button "Edit":
- img
- text: Edit
- img
- text: file1.txt
- paragraph: More EOM
- button:
- button "Copy":
- img
- img
- text: claude-opus-4-5
- img
- text: less than a minute ago
- button "Request ID":
- button "Copy Request ID":
- img
- text: Request ID
- button "Undo":
- img
- text: Undo
- button "Retry":
- img
- text: Retry
module.exports = {
"**/*.{ts,tsx}": () => "npm run ts",
"**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,vue,astro,svelte}": "oxlint",
"*.{js,css,md,ts,tsx,jsx,json,yml,yaml}": "oxfmt",
"**/*.{js,css,md,ts,tsx,jsx,json}": "oxfmt",
};
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论