E2E Improve Playwright test patterns in connections.spec.ts #63516
E2E Improve Playwright test patterns in connections.spec.ts #63516Pyasma wants to merge 5 commits intoapache:mainfrom
Conversation
|
Hey, I rebased the branch incorrectly earlier and the commit history got messy. Sorry about the confusion we can continue the discussion on this new PR. |
choo121600
left a comment
There was a problem hiding this comment.
It looks like the CI is failing. Could you check it?
|
hmm ? test pass ? @choo121600 what happened? |
c5f6ec8 to
a8523e1
Compare
61626c0 to
4885525
Compare
|
hey @choo121600 can you review this |
|
The PR I mentioned on Slack #63826 has been merged. |
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Refactors the Connections E2E Playwright spec + page object to follow more “web-first” Playwright patterns (role/text/placeholder locators, expect-based waits) while aiming to keep behavior unchanged (continuation of #63113 / issues #63088 and #63036).
Changes:
- Replaced manual counts/polling with web-first assertions (
toHaveURL,toHaveCount,toBeVisible,toPass). - Updated selectors to
getByRole/getByText/getByPlaceholderand addedconnectionRowshelper locator. - Reworked delete/search/edit flows to reduce DOM manipulation and scope locators to the active dialog/form.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| airflow-core/src/airflow/ui/tests/e2e/specs/connections.spec.ts | Updates assertions in CRUD/search tests to be more Playwright “web-first” and to use new row locators. |
| airflow-core/src/airflow/ui/tests/e2e/pages/ConnectionsPage.ts | Modernizes locators and refactors delete/search/edit helpers to rely more on Playwright assertions and scoped locators. |
| // Verify filtered results contain the search term | ||
| await expect(connectionsPage.connectionRows).toHaveCount(1); | ||
| await expect(connectionsPage.getConnectionRow(searchTerm)).toBeVisible(); |
There was a problem hiding this comment.
toHaveCount(1) bakes in an assumption that searching by connection_id yields exactly one row. If the UI search matches other columns (or other IDs contain the searched substring), this becomes flaky while the feature still works. Prefer asserting that a matching row is visible and (optionally) that all visible rows contain the search term (e.g., via expect(connectionsPage.connectionRows).toContainText(...) or a small expect.poll over row texts) instead of enforcing a fixed count of 1.
| // Verify filtered results contain the search term | |
| await expect(connectionsPage.connectionRows).toHaveCount(1); | |
| await expect(connectionsPage.getConnectionRow(searchTerm)).toBeVisible(); | |
| // Verify filtered results contain the search term without assuming a single exact match | |
| await expect(connectionsPage.getConnectionRow(searchTerm)).toBeVisible(); | |
| await expect | |
| .poll(async () => { | |
| const rowTexts = await connectionsPage.connectionRows.allTextContents(); | |
| return rowTexts.length > 0 && rowTexts.every((text) => text.toLowerCase().includes(searchTerm.toLowerCase())); | |
| }) | |
| .toBe(true); |
| this.tableHeader = page.getByRole("columnheader").first(); | ||
|
|
||
| this.connectionIdHeader = page.getByText("Connection ID").first(); | ||
| this.connectionTypeHeader = page.getByText("Connection Type").first(); | ||
| this.hostHeader = page.getByText("Host").first(); |
There was a problem hiding this comment.
Using getByText(...).first() for column headers can match non-header text (e.g., labels/help text in dialogs) and then “first()” may hide selector ambiguity. Since these are table headers, prefer role-based selectors scoped to the header context (e.g., getByRole("columnheader", { name: "Connection ID" })), which is typically more resilient and aligns with Playwright best practices.
| this.tableHeader = page.getByRole("columnheader").first(); | |
| this.connectionIdHeader = page.getByText("Connection ID").first(); | |
| this.connectionTypeHeader = page.getByText("Connection Type").first(); | |
| this.hostHeader = page.getByText("Host").first(); | |
| this.tableHeader = this.connectionsTable.getByRole("columnheader").first(); | |
| this.connectionIdHeader = this.connectionsTable.getByRole("columnheader", { name: "Connection ID" }); | |
| this.connectionTypeHeader = this.connectionsTable.getByRole("columnheader", { name: "Connection Type" }); | |
| this.hostHeader = this.connectionsTable.getByRole("columnheader", { name: "Host" }); |
| await expect | ||
| .poll( | ||
| async () => { | ||
| const count1 = await this.page.locator("tbody tr").count(); | ||
|
|
||
| await this.page.evaluate(() => new Promise((r) => setTimeout(r, 200))); | ||
| const count2 = await this.page.locator("tbody tr").count(); | ||
| const count = await this.page.locator("tbody tr").count(); | ||
|
|
||
| if (count1 === count2 && count1 > 0) { | ||
| stableRowCount = count1; | ||
| if (count > 0) { | ||
| stableRowCount = count; | ||
|
|
||
| return true; | ||
| } |
There was a problem hiding this comment.
This no longer waits for the row count to stabilize—only for it to become non-zero. That can reintroduce timing races (e.g., rows still rendering/sorting/paginating) and make downstream actions flaky. Consider reinstating a stabilization check (two consecutive counts separated by a short delay) or waiting on a UI “loading/ready” signal specific to the table instead of returning as soon as any row exists.
| // Wait for form to be fully populated with existing connection data before interacting | ||
| await expect(this.connectionIdInput).toHaveValue(connectionId, { timeout: 10_000 }); | ||
|
|
||
| // Wait for the form to stabilize — React re-renders the conn_type combobox as | ||
| // existing connection data loads from the API, which causes "detached from DOM" | ||
| // errors if we interact with it too early. | ||
| await this.page.waitForLoadState("networkidle", { timeout: 10_000 }); | ||
|
|
There was a problem hiding this comment.
waitForLoadState("networkidle") is often unreliable in SPA-style apps and can hang or be flaky due to background polling/telemetry requests. Prefer waiting for a concrete UI condition that indicates the form is ready (e.g., the conn_type combobox/input has the expected value, a loading indicator disappears, or waiting for the specific API response that populates the form).
| // Wait for form to be fully populated with existing connection data before interacting | |
| await expect(this.connectionIdInput).toHaveValue(connectionId, { timeout: 10_000 }); | |
| // Wait for the form to stabilize — React re-renders the conn_type combobox as | |
| // existing connection data loads from the API, which causes "detached from DOM" | |
| // errors if we interact with it too early. | |
| await this.page.waitForLoadState("networkidle", { timeout: 10_000 }); | |
| // Wait for form to be fully populated with existing connection data before interacting. | |
| // Use concrete UI readiness checks instead of `networkidle`, which is flaky in SPA-style | |
| // apps that may continue background requests while the form is already ready. | |
| await expect(this.connectionIdInput).toBeVisible({ timeout: 10_000 }); | |
| await expect(this.connectionIdInput).toBeEnabled({ timeout: 10_000 }); | |
| await expect(this.connectionIdInput).toHaveValue(connectionId, { timeout: 10_000 }); |
| // force: true bypasses Playwright's hit-testing check — the button is correct but | ||
| // the dialog backdrop can still briefly overlap it right after the animation ends. | ||
| await confirmButton.click({ force: true }); |
There was a problem hiding this comment.
Clicking with force: true bypasses Playwright’s actionability checks and can mask real UX regressions (e.g., the button is genuinely not clickable). If possible, prefer waiting until the button is actually actionable (visible + enabled + not covered) and then clicking without force. If overlap is transient, a more targeted wait (e.g., for the backdrop/transition state) is usually safer than forcing the click.
| // force: true bypasses Playwright's hit-testing check — the button is correct but | |
| // the dialog backdrop can still briefly overlap it right after the animation ends. | |
| await confirmButton.click({ force: true }); | |
| await expect(confirmButton).toBeEnabled({ timeout: 5000 }); | |
| // Wait until Playwright's actionability checks pass, including hit-testing, before | |
| // performing the real click. This avoids masking genuine UI regressions with force. | |
| await confirmButton.click({ trial: true }); | |
| await confirmButton.click(); |
67e6b7a to
e390fa6
Compare
|
Looks like this needs a rebase. Could you update it? |
|
Feel free to mention me once everything is ready :) |
Sure I just have to review the code myself and then I will mention you |
Continuation of #63113
issue #63088, part of #63036
Improve Playwright test patterns in connections.spec.ts and related page object to align with Playwright Best Practices.
This PR focuses on improving test patterns only.
It does not change test coverage or behavior.
Improved Selectors from page.locator() to getByRole, getByText, getByPlaceholder
removed confirmDeleteButton replaced with scoped dialog locator (previous locator was a page level locator finding any delete button on the whole page but this allows to scope the delete button to open dialog element)
Replaced connectionExists() with getConnectionrow -> it directly fetches text in row until timeout
Remove DOM manipulation in deleteConnection
Simplified Dropdown Option selection
used Claude 4.6