Copilot commented on code in PR #63516:
URL: https://github.com/apache/airflow/pull/63516#discussion_r3066477596


##########
airflow-core/src/airflow/ui/tests/e2e/pages/ConnectionsPage.ts:
##########
@@ -309,13 +287,10 @@ export class ConnectionsPage extends BasePage {
     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;
           }

Review 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.



##########
airflow-core/src/airflow/ui/tests/e2e/pages/ConnectionsPage.ts:
##########
@@ -153,50 +137,45 @@ export class ConnectionsPage extends BasePage {
 
   // Delete a connection by connection ID
   public async deleteConnection(connectionId: string): Promise<void> {
-    // await this.navigate();
     const row = await this.findConnectionRow(connectionId);
 
     if (!row) {
       throw new Error(`Connection ${connectionId} not found`);
     }
 
-    // Find delete button in the row
-    await this.page.evaluate(() => {
-      const backdrops = 
document.querySelectorAll<HTMLElement>('[data-scope="dialog"][data-part="backdrop"]');
-
-      backdrops.forEach((backdrop) => {
-        const { state } = backdrop.dataset;
-
-        if (state === "closed") {
-          backdrop.remove();
-        }
-      });
-    });
     const deleteButton = row.getByRole("button", { name: "Delete Connection" 
});
 
     await expect(deleteButton).toBeVisible({ timeout: 10_000 });
     await expect(deleteButton).toBeEnabled({ timeout: 5000 });
     await deleteButton.click();
 
-    await expect(this.confirmDeleteButton).toBeVisible({ timeout: 10_000 });
-    await expect(this.confirmDeleteButton).toBeEnabled({ timeout: 5000 });
-    await this.confirmDeleteButton.click();
+    // Wait for the dialog to finish its open animation (data-state="open" is 
set by
+    // Ark UI once the transition completes). Without this, the backdrop can 
cover the
+    // confirm button during the animation and cause the click to time out.
+    const deleteDialog = 
this.page.locator('[role="dialog"][data-state="open"]');
 
-    await expect(this.emptyState).toBeVisible({ timeout: 5000 });
+    await deleteDialog.waitFor({ state: "visible", timeout: 10_000 });
+    const confirmButton = deleteDialog.getByRole("button", { name: "Yes, 
Delete" });
+
+    await expect(confirmButton).toBeVisible({ timeout: 5000 });
+    // 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(this.getConnectionRow(connectionId)).not.toBeVisible({ 
timeout: 15_000 });
   }
 
   // Edit a connection by connection ID
   public async editConnection(connectionId: string, updates: 
Partial<ConnectionDetails>): Promise<void> {
-    const row = await this.findConnectionRow(connectionId);
-
-    if (!row) {
-      throw new Error(`Connection ${connectionId} not found`);
-    }
-
     await this.clickEditButton(connectionId);
 
-    // Wait for form to load
-    await expect(this.connectionIdInput).toBeVisible({ timeout: 10_000 });
+    // 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 });
 

Review 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).
   ```suggestion
       // 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 });
   ```



##########
airflow-core/src/airflow/ui/tests/e2e/pages/ConnectionsPage.ts:
##########
@@ -153,50 +137,45 @@ export class ConnectionsPage extends BasePage {
 
   // Delete a connection by connection ID
   public async deleteConnection(connectionId: string): Promise<void> {
-    // await this.navigate();
     const row = await this.findConnectionRow(connectionId);
 
     if (!row) {
       throw new Error(`Connection ${connectionId} not found`);
     }
 
-    // Find delete button in the row
-    await this.page.evaluate(() => {
-      const backdrops = 
document.querySelectorAll<HTMLElement>('[data-scope="dialog"][data-part="backdrop"]');
-
-      backdrops.forEach((backdrop) => {
-        const { state } = backdrop.dataset;
-
-        if (state === "closed") {
-          backdrop.remove();
-        }
-      });
-    });
     const deleteButton = row.getByRole("button", { name: "Delete Connection" 
});
 
     await expect(deleteButton).toBeVisible({ timeout: 10_000 });
     await expect(deleteButton).toBeEnabled({ timeout: 5000 });
     await deleteButton.click();
 
-    await expect(this.confirmDeleteButton).toBeVisible({ timeout: 10_000 });
-    await expect(this.confirmDeleteButton).toBeEnabled({ timeout: 5000 });
-    await this.confirmDeleteButton.click();
+    // Wait for the dialog to finish its open animation (data-state="open" is 
set by
+    // Ark UI once the transition completes). Without this, the backdrop can 
cover the
+    // confirm button during the animation and cause the click to time out.
+    const deleteDialog = 
this.page.locator('[role="dialog"][data-state="open"]');
 
-    await expect(this.emptyState).toBeVisible({ timeout: 5000 });
+    await deleteDialog.waitFor({ state: "visible", timeout: 10_000 });
+    const confirmButton = deleteDialog.getByRole("button", { name: "Yes, 
Delete" });
+
+    await expect(confirmButton).toBeVisible({ timeout: 5000 });
+    // 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 });

Review 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.
   ```suggestion
       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();
   ```



##########
airflow-core/src/airflow/ui/tests/e2e/pages/ConnectionsPage.ts:
##########
@@ -84,22 +84,22 @@ export class ConnectionsPage extends BasePage {
     this.loginInput = page.locator('input[name="login"]').first();
     this.passwordInput = page.locator('input[name="password"], 
input[type="password"]').first();
     this.schemaInput = page.locator('input[name="schema"]').first();
-    // Try multiple possible selectors
     this.descriptionInput = page.locator('[name="description"]').first();
-
     // Alerts
     this.successAlert = page.locator('[data-scope="toast"][data-part="root"]');
 
-    // Delete confirmation dialog
-    this.confirmDeleteButton = 
page.locator('button:has-text("Delete")').first();
     this.rowsPerPageSelect = page.locator("select");
 
     // Sorting and filtering
-    this.tableHeader = page.locator('[role="columnheader"]').first();
-    this.connectionIdHeader = page.locator("th:has-text('Connection 
ID')").first();
-    this.connectionTypeHeader = page.locator('th:has-text("Connection 
Type")').first();
-    this.hostHeader = page.locator('th:has-text("Host")').first();
-    this.searchInput = page.locator('input[placeholder*="Search"], 
input[placeholder*="search"]').first();
+    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();

Review 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.
   ```suggestion
       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" });
   ```



##########
airflow-core/src/airflow/ui/tests/e2e/specs/connections.spec.ts:
##########
@@ -281,62 +271,35 @@ test.describe("Connections Page - Search and Filter", () 
=> {
   test("should filter connections by search term", async () => {
     await connectionsPage.navigate();
 
-    const initialCount = await connectionsPage.getConnectionCount();
-
-    expect(initialCount).toBeGreaterThan(0);
-
-    const searchTerm = "production";
+    const targetConnection = searchTestConnections[0]!;
+    const searchTerm = targetConnection.connection_id;
 
+    // Check that we have at least one row before searching (web-first 
assertion)
     await connectionsPage.searchConnections(searchTerm);
 
-    await expect
-      .poll(
-        async () => {
-          const ids = await connectionsPage.getConnectionIds();
-
-          // Verify we have results AND they match the search term
-          return ids.length > 0 && ids.every((id) => 
id.toLowerCase().includes(searchTerm.toLowerCase()));
-        },
-        { intervals: [500], timeout: 10_000 },
-      )
-      .toBe(true);
-
-    const filteredIds = await connectionsPage.getConnectionIds();
-
-    expect(filteredIds.length).toBeGreaterThan(0);
-    for (const id of filteredIds) {
-      expect(id.toLowerCase()).toContain(searchTerm.toLowerCase());
-    }
+    // Verify filtered results contain the search term
+    await expect(connectionsPage.connectionRows).toHaveCount(1);
+    await expect(connectionsPage.getConnectionRow(searchTerm)).toBeVisible();

Review 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.
   ```suggestion
       // 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 is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to