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


##########
airflow-core/src/airflow/ui/tests/e2e/pages/RequiredActionsPage.ts:
##########
@@ -76,10 +76,22 @@ export class RequiredActionsPage extends BasePage {
   }
 
   private async clickOnTaskInGrid(dagRunId: string, taskId: string): 
Promise<void> {
+    const gridContainer = this.page.locator('[data-testid="dag-graph"], 
[data-testid^="grid-"]').first();
+
+    await expect(gridContainer).toBeVisible({ timeout: 30_000 });
+

Review Comment:
   `gridContainer` uses `[data-testid="dag-graph"], [data-testid^="grid-"]` 
with `.first()`. `[data-testid^="grid-"]` is broad (it can match individual 
task nodes like `grid-${dagRunId}-${taskId}`), so `.first()` can become 
non-deterministic and make this readiness check ineffective. Prefer targeting a 
stable container test id (e.g. the graph container) or otherwise scoping the 
selector so it cannot match task elements.
   ```suggestion
   
   ```



##########
airflow-core/src/airflow/ui/tests/e2e/pages/RequiredActionsPage.ts:
##########
@@ -76,10 +76,22 @@ export class RequiredActionsPage extends BasePage {
   }
 
   private async clickOnTaskInGrid(dagRunId: string, taskId: string): 
Promise<void> {
+    const gridContainer = this.page.locator('[data-testid="dag-graph"], 
[data-testid^="grid-"]').first();
+
+    await expect(gridContainer).toBeVisible({ timeout: 30_000 });
+
     const taskLocator = this.page.getByTestId(`grid-${dagRunId}-${taskId}`);
 
-    await expect(taskLocator).toBeVisible({ timeout: 5000 });
-    await taskLocator.click();
+    await expect(taskLocator).toBeVisible({ timeout: 30_000 });
+    await taskLocator.click().catch(async () => {
+      const fallbackLocator = this.page
+        .locator(`[data-testid*="${taskId}"]`)
+        .filter({ hasText: taskId })
+        .first();
+
+      await expect(fallbackLocator).toBeVisible({ timeout: 30_000 });
+      await fallbackLocator.click();
+    });

Review Comment:
   Using `await taskLocator.click().catch(async () => { ... })` can swallow 
relevant Playwright click failures (e.g. element detached, intercepted clicks) 
and makes retries harder to reason about. For flaky clicks, prefer Playwright’s 
built-in retries via `expect(taskLocator).toBeVisible/toBeEnabled` plus 
`taskLocator.click({ trial: true })`/`toPass`, or implement a controlled retry 
that re-resolves locators and fails with the original error if the fallback 
also fails.
   ```suggestion
   
       let originalClickError: unknown;
   
       try {
         await taskLocator.click();
       } catch (error) {
         originalClickError = error;
   
         const fallbackLocator = this.page
           .locator(`[data-testid*="${taskId}"]`)
           .filter({ hasText: taskId })
           .first();
   
         await expect(fallbackLocator).toBeVisible({ timeout: 30_000 });
         try {
           await fallbackLocator.click();
         } catch {
           throw originalClickError;
         }
       }
   ```



##########
airflow-core/src/airflow/ui/tests/e2e/pages/DagCalendarTab.ts:
##########
@@ -109,22 +109,18 @@ export class DagCalendarTab extends BasePage {
   }
 
   private async waitForCalendarReady(): Promise<void> {
-    await this.page.getByTestId("dag-calendar-root").waitFor({ state: 
"visible", timeout: 120_000 });
+    await expect(this.page.getByTestId("dag-calendar-root")).toBeVisible({ 
timeout: 120_000 });
 
-    await this.page.getByTestId("calendar-current-period").waitFor({ state: 
"visible", timeout: 120_000 });
+    await 
expect(this.page.getByTestId("calendar-current-period")).toBeVisible({ timeout: 
120_000 });
 
     const overlay = this.page.getByTestId("calendar-loading-overlay");
 
     if (await overlay.isVisible().catch(() => false)) {
-      await overlay.waitFor({ state: "hidden", timeout: 120_000 });
+      await expect(overlay).toBeHidden({ timeout: 120_000 });
     }
 
-    await this.page.getByTestId("calendar-grid").waitFor({ state: "visible", 
timeout: 120_000 });
+    await expect(this.page.getByTestId("calendar-grid")).toBeVisible({ 
timeout: 120_000 });
 
-    await this.page.waitForFunction(() => {
-      const cells = document.querySelectorAll('[data-testid="calendar-cell"]');
-
-      return cells.length > 0;
-    });
+    await expect(this.calendarCells.first()).toBeVisible({ timeout: 120_000 });

Review Comment:
   `await expect(this.calendarCells.first()).toBeVisible(...)` verifies at 
least one cell is visible, but it doesn’t ensure the grid is fully 
populated/stable (e.g. only a skeleton cell rendered). If the intent is 
“calendar fully loaded”, consider asserting on an expected minimum count of 
cells (via `toHaveCount` with a range/known value) or waiting for a specific UI 
marker that indicates render completion.
   ```suggestion
       await expect(this.calendarCells).toHaveCount(42, { timeout: 120_000 });
   ```



##########
airflow-core/src/airflow/ui/tests/e2e/pages/ConnectionsPage.ts:
##########
@@ -215,7 +215,12 @@ export class ConnectionsPage extends BasePage {
 
       await expect(selectCombobox).toBeEnabled({ timeout: 25_000 });
 
-      await selectCombobox.click({ timeout: 3000 });
+      await selectCombobox.click({ timeout: 15_000 });
+
+      // Wait for the listbox to be visible before selecting
+      const listbox = this.page.locator('[role="listbox"]');

Review Comment:
   `const listbox = this.page.locator('[role="listbox"]')` is unscoped and may 
match multiple listboxes (or a hidden one) elsewhere on the page, causing 
`toBeVisible` to wait on the wrong element and making the subsequent option 
selection flaky. Consider scoping the listbox to the opened combobox (e.g. via 
`aria-controls`/`aria-owns` on the combobox), or use 
`this.page.getByRole('listbox').first()` together with checks that it appears 
after the click.
   ```suggestion
         const listbox = this.page.getByRole("listbox").first();
   ```



##########
airflow-core/src/airflow/ui/tests/e2e/pages/DagCalendarTab.ts:
##########
@@ -109,22 +109,18 @@ export class DagCalendarTab extends BasePage {
   }
 
   private async waitForCalendarReady(): Promise<void> {
-    await this.page.getByTestId("dag-calendar-root").waitFor({ state: 
"visible", timeout: 120_000 });
+    await expect(this.page.getByTestId("dag-calendar-root")).toBeVisible({ 
timeout: 120_000 });
 
-    await this.page.getByTestId("calendar-current-period").waitFor({ state: 
"visible", timeout: 120_000 });
+    await 
expect(this.page.getByTestId("calendar-current-period")).toBeVisible({ timeout: 
120_000 });
 
     const overlay = this.page.getByTestId("calendar-loading-overlay");
 
     if (await overlay.isVisible().catch(() => false)) {
-      await overlay.waitFor({ state: "hidden", timeout: 120_000 });
+      await expect(overlay).toBeHidden({ timeout: 120_000 });
     }
 
-    await this.page.getByTestId("calendar-grid").waitFor({ state: "visible", 
timeout: 120_000 });
+    await expect(this.page.getByTestId("calendar-grid")).toBeVisible({ 
timeout: 120_000 });
 
-    await this.page.waitForFunction(() => {
-      const cells = document.querySelectorAll('[data-testid="calendar-cell"]');
-
-      return cells.length > 0;
-    });
+    await expect(this.calendarCells.first()).toBeVisible({ timeout: 120_000 });

Review Comment:
   The overlay wait is conditional on a one-time `overlay.isVisible()` check. 
If the overlay appears shortly after this check (race during render), the 
method will skip waiting for it to disappear and proceed while the grid is 
still loading. Prefer an unconditional web-first wait that tolerates both cases 
(e.g. `await expect(overlay).toBeHidden()` without the `if`, or wait for the 
grid/cells to be visible+stable in a way that also covers late-appearing 
overlays).



##########
airflow-core/src/airflow/ui/tests/e2e/pages/XComsPage.ts:
##########
@@ -61,9 +61,11 @@ export class XComsPage extends BasePage {
   }
 
   public async navigate(): Promise<void> {
-    await this.navigateTo(XComsPage.xcomsUrl);
-    await this.page.waitForURL(/.*xcoms/, { timeout: 15_000 });
-    await this.xcomsTable.waitFor({ state: "visible", timeout: 10_000 });
+    await expect(async () => {
+      await this.navigateTo(XComsPage.xcomsUrl);
+      await expect(this.page).toHaveURL(/.*xcoms/, { timeout: 15_000 });
+    }).toPass({ intervals: [2000], timeout: 60_000 });

Review Comment:
   Wrapping navigation in `expect(async () => { ... }).toPass(...)` retries a 
full `page.goto()` on any failure. If the page loads but the second attempt 
starts, this can add unnecessary navigation churn and hide the underlying 
failure. Where possible, prefer a single `navigateTo` and then use Expect’s 
web-first assertions to wait for stable UI state (URL + table visibility). If 
you do need retries, consider adding a message or narrowing the retried 
assertions to avoid repeated navigation.
   ```suggestion
       await this.navigateTo(XComsPage.xcomsUrl);
       await expect(this.page).toHaveURL(/.*xcoms/, { timeout: 60_000 });
   ```



##########
airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts:
##########
@@ -345,8 +345,10 @@ export class DagsPage extends BasePage {
   public async triggerDag(dagName: string): Promise<string | null> {
     await expect(async () => {
       await this.navigateToDagDetail(dagName);
-      await expect(this.triggerButton).toBeVisible({ timeout: 5000 });
-    }).toPass({ intervals: [2000], timeout: 60_000 });
+      await this.page.waitForLoadState("domcontentloaded");
+      await expect(this.triggerButton).toBeAttached({ timeout: 15_000 });
+      await expect(this.triggerButton).toBeEnabled({ timeout: 15_000 });
+    }).toPass({ intervals: [2000], timeout: 90_000 });
     await this.triggerButton.click();

Review Comment:
   `triggerDag` now waits for the trigger button to be attached+enabled inside 
the 90s `toPass`, but the actual `click()` is outside that block and will use 
Playwright’s default action timeout. If the button is attached/enabled but 
still not visible/interactable (e.g. covered by a loading overlay), the click 
can time out earlier and become flaky. Consider asserting `toBeVisible()` (or 
performing the click) inside the same `toPass`, or pass an explicit `timeout` 
to `this.triggerButton.click()` consistent with the outer retry timeout.
   ```suggestion
         await expect(this.triggerButton).toBeVisible({ timeout: 15_000 });
       }).toPass({ intervals: [2000], timeout: 90_000 });
       await this.triggerButton.click({ timeout: 90_000 });
   ```



##########
airflow-core/src/airflow/ui/tests/e2e/pages/DagCalendarTab.ts:
##########
@@ -85,7 +85,7 @@ export class DagCalendarTab extends BasePage {
   public async navigateToCalendar(dagId: string) {
     await expect(async () => {
       await this.page.goto(`/dags/${dagId}/calendar`);
-      await this.page.getByTestId("dag-calendar-root").waitFor({ state: 
"visible", timeout: 5000 });
+      await expect(this.page.getByTestId("dag-calendar-root")).toBeVisible({ 
timeout: 5000 });
     }).toPass({ intervals: [2000], timeout: 60_000 });
     await this.waitForCalendarReady();

Review Comment:
   PR description says the change “specifically” focuses on `DagCalendarTab`, 
but this PR also changes other page objects (`XComsPage`, `DagsPage`, 
`RequiredActionsPage`, `ConnectionsPage`). Please either update the PR 
description/scope or split the unrelated refactors so reviewers can validate 
intent and risk more easily.



##########
airflow-core/src/airflow/ui/tests/e2e/pages/ConnectionsPage.ts:
##########
@@ -215,7 +215,12 @@ export class ConnectionsPage extends BasePage {
 
       await expect(selectCombobox).toBeEnabled({ timeout: 25_000 });
 
-      await selectCombobox.click({ timeout: 3000 });
+      await selectCombobox.click({ timeout: 15_000 });
+
+      // Wait for the listbox to be visible before selecting
+      const listbox = this.page.locator('[role="listbox"]');
+
+      await expect(listbox).toBeVisible({ timeout: 15_000 });
 
       // Wait for options to appear and click the matching option
       const option = this.page.getByRole("option", { name: new 
RegExp(details.conn_type, "i") }).first();

Review Comment:
   The listbox wait uses an unscoped selector (`[role="listbox"]`), which can 
match multiple listboxes or a hidden one on the page. This can make 
`toBeVisible` pass/fail against the wrong element and lead to flaky option 
selection. Prefer scoping the listbox to the opened combobox (e.g. via the 
combobox `aria-controls` target) or at least selecting the specific listbox 
that appears after the click.
   ```suggestion
         // Resolve the listbox associated with this combobox and wait for it 
to be visible
         const ariaControls = await 
selectCombobox.getAttribute("aria-controls");
         const listbox = ariaControls
           ? this.page.locator(`#${ariaControls}`)
           : this.page.locator('[role="listbox"]').first();
   
         await expect(listbox).toBeVisible({ timeout: 15_000 });
   
         // Wait for options to appear and click the matching option within 
this listbox
         const option = listbox.getByRole("option", { name: new 
RegExp(details.conn_type, "i") }).first();
   ```



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