This is an automated email from the ASF dual-hosted git repository.

choo121600 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new eb49aea2591 Add UI E2E test for DAGs list display (#59374)
eb49aea2591 is described below

commit eb49aea2591dc98c7012386aaeb69e2476010e0a
Author: brian <[email protected]>
AuthorDate: Wed Jan 28 12:01:08 2026 +0800

    Add UI E2E test for DAGs list display (#59374)
    
    * Fix race conditions in pagination for slower browsers
    
    - Add API interception before pagination clicks
    - Increase timeout from 10s to 30s for Firefox compatibility
    - Add test.setTimeout(120_000) to pagination and search tests
    - Improve waitForDagList timeout to 30s
    
    Add comprehensive E2E tests for DAGs list page:
    - Dags List Display: verify list visibility, dag links, dag existence
    - Dags View Toggle: switch between card and table views
    - Dags Search: search and clear with race condition handling
    - Dags Status Filtering: filter by success/failed/running/queued
    - Dags Sorting: sort by name in card view
    
    * fix(tests): Increase timeout for dags-list spec
    
    Increases the timeout for the dags-list.spec.ts e2e test to prevent 
flakiness.
    
    * fix(tests): Update E2E test selectors to match current UI
    
    * fix(tests): Use expect.poll for flaky Dags Search test                    
                            Replace direct assertion with polling to handle UI 
rendering timing                                                  differences 
between local and CI environments.
    
    * fix(tests): Handle empty state in waitForDagList to prevent timeout
    
    * fix(tests): Harden E2E locators and replace fixed delays with polling
    
    - Use case-insensitive regex (/no dag/i) for empty state detection
      to handle singular/plural/casing variations
    - Add fallback <table> locator in waitForDagList when data-testid
      attributes are missing
    - Change confirmButton from .nth(1) to .last() to handle icon-only
      trigger buttons
    - Replace waitForTimeout(500) with expect.poll in Dags Sorting test
    
    ---------
    
    Co-authored-by: Rahul Vats <[email protected]>
    Co-authored-by: Yeonguk Choo <[email protected]>
---
 .../src/airflow/ui/tests/e2e/pages/DagsPage.ts     | 396 +++++++++++++++++++--
 .../airflow/ui/tests/e2e/specs/dags-list.spec.ts   | 196 ++++++++++
 2 files changed, 553 insertions(+), 39 deletions(-)

diff --git a/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts 
b/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts
index a5506fcf05c..b3e65857339 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts
@@ -30,13 +30,22 @@ export class DagsPage extends BasePage {
     return "/dags";
   }
 
+  public readonly cardViewButton: Locator;
   public readonly confirmButton: Locator;
+  public readonly failedFilter: Locator;
+  public readonly needsReviewFilter: Locator;
   public readonly operatorFilter: Locator;
   public readonly paginationNextButton: Locator;
   public readonly paginationPrevButton: Locator;
+  public readonly queuedFilter: Locator;
   public readonly retriesFilter: Locator;
+  public readonly runningFilter: Locator;
   public readonly searchBox: Locator;
+  public readonly searchInput: Locator;
+  public readonly sortSelect: Locator;
   public readonly stateElement: Locator;
+  public readonly successFilter: Locator;
+  public readonly tableViewButton: Locator;
   public readonly triggerButton: Locator;
   public readonly triggerRuleFilter: Locator;
 
@@ -47,14 +56,29 @@ export class DagsPage extends BasePage {
   public constructor(page: Page) {
     super(page);
     this.triggerButton = page.locator('button[aria-label="Trigger 
Dag"]:has-text("Trigger")');
-    this.confirmButton = page.locator('button:has-text("Trigger")').nth(1);
+    // Use .last() instead of .nth(1) — when the modal opens, the confirm 
button
+    // is the last "Trigger" button in the DOM regardless of whether the main
+    // page trigger button has visible text or is icon-only.
+    this.confirmButton = page.locator('button:has-text("Trigger")').last();
     this.stateElement = page.locator('*:has-text("State") + *').first();
     this.paginationNextButton = page.locator('[data-testid="next"]');
     this.paginationPrevButton = page.locator('[data-testid="prev"]');
     this.searchBox = page.getByRole("textbox", { name: /search/i });
+    this.searchInput = page.getByPlaceholder("Search DAGs");
     this.operatorFilter = page.getByRole("combobox").filter({ hasText: 
/operator/i });
     this.triggerRuleFilter = page.getByRole("combobox").filter({ hasText: 
/trigger/i });
     this.retriesFilter = page.getByRole("combobox").filter({ hasText: /retr/i 
});
+    // View toggle buttons
+    this.cardViewButton = page.locator('button[aria-label="Show card view"]');
+    this.tableViewButton = page.locator('button[aria-label="Show table 
view"]');
+    // Sort select (card view only)
+    this.sortSelect = page.locator('[data-testid="sort-by-select"]');
+    // Status filter buttons
+    this.successFilter = page.locator('button:has-text("Success")');
+    this.failedFilter = page.locator('button:has-text("Failed")');
+    this.runningFilter = page.locator('button:has-text("Running")');
+    this.queuedFilter = page.locator('button:has-text("Queued")');
+    this.needsReviewFilter = page.locator('button:has-text("Needs Review")');
   }
 
   // URL builders for dynamic paths
@@ -67,30 +91,90 @@ export class DagsPage extends BasePage {
   }
 
   /**
-   * Click next page button
+   * Clear the search input and wait for list to reset
+   */
+  public async clearSearch(): Promise<void> {
+    await this.searchInput.clear();
+
+    // Trigger blur to ensure the clear action is processed
+    await this.searchInput.blur();
+
+    // Small delay to allow React to process the state change
+    await this.page.waitForTimeout(500);
+
+    // Wait for the DAG list to be visible again
+    await this.waitForDagList();
+  }
+
+  /**
+   * Click next page button and wait for list to change
    */
   public async clickNextPage(): Promise<void> {
     const initialDagNames = await this.getDagNames();
 
+    // Set up API listener before action
+    const responsePromise = this.page
+      .waitForResponse((resp) => resp.url().includes("/dags") && resp.status() 
=== 200, {
+        timeout: 30_000,
+      })
+      .catch(() => {
+        /* API might be cached */
+      });
+
     await this.paginationNextButton.click();
 
-    await expect.poll(() => this.getDagNames(), { timeout: 10_000 
}).not.toEqual(initialDagNames);
+    // Wait for API response
+    await responsePromise;
+
+    // Wait for UI to actually change (increased timeout for slower browsers 
like Firefox)
+    await expect
+      .poll(() => this.getDagNames(), {
+        message: "List did not update after clicking next page",
+        timeout: 30_000,
+      })
+      .not.toEqual(initialDagNames);
 
     await this.waitForDagList();
   }
 
   /**
-   * Click previous page button
+   * Click previous page button and wait for list to change
    */
   public async clickPrevPage(): Promise<void> {
     const initialDagNames = await this.getDagNames();
 
+    // Set up API listener before action
+    const responsePromise = this.page
+      .waitForResponse((resp) => resp.url().includes("/dags") && resp.status() 
=== 200, {
+        timeout: 30_000,
+      })
+      .catch(() => {
+        /* API might be cached */
+      });
+
     await this.paginationPrevButton.click();
 
-    await expect.poll(() => this.getDagNames(), { timeout: 10_000 
}).not.toEqual(initialDagNames);
+    // Wait for API response
+    await responsePromise;
+
+    // Wait for UI to actually change (increased timeout for slower browsers 
like Firefox)
+    await expect
+      .poll(() => this.getDagNames(), {
+        message: "List did not update after clicking prev page",
+        timeout: 30_000,
+      })
+      .not.toEqual(initialDagNames);
+
     await this.waitForDagList();
   }
 
+  /**
+   * Click sort select (only works in card view)
+   */
+  public async clickSortSelect(): Promise<void> {
+    await this.sortSelect.click();
+  }
+
   public async filterByOperator(operator: string): Promise<void> {
     await this.selectDropdownOption(this.operatorFilter, operator);
   }
@@ -99,21 +183,97 @@ export class DagsPage extends BasePage {
     await this.selectDropdownOption(this.retriesFilter, retries);
   }
 
+  /**
+   * Filter DAGs by status
+   */
+  public async filterByStatus(
+    status: "failed" | "needs_review" | "queued" | "running" | "success",
+  ): Promise<void> {
+    const filterMap: Record<typeof status, Locator> = {
+      failed: this.failedFilter,
+      needs_review: this.needsReviewFilter,
+      queued: this.queuedFilter,
+      running: this.runningFilter,
+      success: this.successFilter,
+    };
+
+    await filterMap[status].click();
+    await this.page.waitForTimeout(500);
+  }
+
   public async filterByTriggerRule(rule: string): Promise<void> {
     await this.selectDropdownOption(this.triggerRuleFilter, rule);
   }
 
+  /**
+   * Get all Dag links from the list
+   */
+  public async getDagLinks(): Promise<Array<string>> {
+    await this.waitForDagList();
+
+    // Check which view is active
+    const cardList = this.page.locator('[data-testid="card-list"]');
+    const isCardView = await cardList.isVisible();
+
+    const links = isCardView
+      ? await this.page.locator('[data-testid="dag-id"]').all()
+      : await this.page.locator('[data-testid="table-list"] tbody tr 
td:nth-child(2) a').all();
+
+    const hrefs: Array<string> = [];
+
+    for (const link of links) {
+      const href = await link.getAttribute("href");
+
+      if (href !== null && href !== "") {
+        hrefs.push(href);
+      }
+    }
+
+    return hrefs;
+  }
+
   /**
    * Get all Dag names from the current page
    */
   public async getDagNames(): Promise<Array<string>> {
     await this.waitForDagList();
-    const dagLinks = this.page.locator('[data-testid="dag-id"]');
+
+    // Check which view is active
+    const cardList = this.page.locator('[data-testid="card-list"]');
+    const isCardView = await cardList.isVisible();
+
+    const dagLinks = isCardView
+      ? this.page.locator('[data-testid="dag-id"]')
+      : this.page.locator('[data-testid="table-list"] tbody tr td:nth-child(2) 
a');
+
     const texts = await dagLinks.allTextContents();
 
     return texts.map((text) => text.trim()).filter((text) => text !== "");
   }
 
+  /**
+   * Get count of DAGs on current page
+   */
+  public async getDagsCount(): Promise<number> {
+    await this.waitForDagList();
+
+    // Check which view is active
+    const cardList = this.page.locator('[data-testid="card-list"]');
+    const isCardView = await cardList.isVisible();
+
+    if (isCardView) {
+      // Card view: count dag-id elements
+      const dagCards = this.page.locator('[data-testid="dag-id"]');
+
+      return dagCards.count();
+    } else {
+      // Table view: count table body rows
+      const tableRows = this.page.locator('[data-testid="table-list"] tbody 
tr');
+
+      return tableRows.count();
+    }
+  }
+
   public async getFilterOptions(filter: Locator): Promise<Array<string>> {
     await filter.click();
     await this.page.waitForTimeout(500);
@@ -151,7 +311,22 @@ export class DagsPage extends BasePage {
    * Navigate to Dags list page
    */
   public async navigate(): Promise<void> {
+    // Set up API listener before navigation
+    const responsePromise = this.page
+      .waitForResponse((resp) => resp.url().includes("/api/v2/dags") && 
resp.status() === 200, {
+        timeout: 60_000,
+      })
+      .catch(() => {
+        /* API might fail or timeout */
+      });
+
     await this.navigateTo(DagsPage.dagsListUrl);
+
+    // Wait for initial API response
+    await responsePromise;
+
+    // Give UI time to render the response
+    await this.page.waitForTimeout(500);
   }
 
   /**
@@ -170,6 +345,71 @@ export class DagsPage extends BasePage {
       .waitFor({ state: "visible", timeout: 30_000 });
   }
 
+  /**
+   * Search for a Dag by name
+   */
+  public async searchDag(searchTerm: string): Promise<void> {
+    const currentNames = await this.getDagNames();
+
+    const responsePromise = this.page
+      .waitForResponse((resp) => resp.url().includes("/dags") && resp.status() 
=== 200, {
+        timeout: 30_000,
+      })
+      .catch(() => {
+        /* API might be cached */
+      });
+
+    await this.searchInput.fill(searchTerm);
+
+    await responsePromise;
+
+    await expect
+      .poll(
+        async () => {
+          const noDagFound = this.page.locator("text=/no dag/i");
+          const isNoDagVisible = await noDagFound.isVisible().catch(() => 
false);
+
+          if (isNoDagVisible) {
+            return true;
+          }
+
+          const newNames = await this.getDagNames();
+
+          return newNames.join(",") !== currentNames.join(",");
+        },
+        { message: "List did not update after search", timeout: 30_000 },
+      )
+      .toBe(true);
+
+    await this.waitForDagList();
+  }
+
+  /**
+   * Switch to card view
+   */
+  public async switchToCardView(): Promise<void> {
+    // Wait for the button to be visible and enabled
+    await expect(this.cardViewButton).toBeVisible({ timeout: 30_000 });
+    await expect(this.cardViewButton).toBeEnabled({ timeout: 10_000 });
+    await this.cardViewButton.click();
+    // Wait for card view to be rendered
+    await this.page.waitForTimeout(500);
+    await this.verifyCardViewVisible();
+  }
+
+  /**
+   * Switch to table view
+   */
+  public async switchToTableView(): Promise<void> {
+    // Wait for the button to be visible and enabled
+    await expect(this.tableViewButton).toBeVisible({ timeout: 30_000 });
+    await expect(this.tableViewButton).toBeEnabled({ timeout: 10_000 });
+    await this.tableViewButton.click();
+    // Wait for table view to be rendered
+    await this.page.waitForTimeout(500);
+    await this.verifyTableViewVisible();
+  }
+
   /**
    * Trigger a Dag run
    */
@@ -182,41 +422,57 @@ export class DagsPage extends BasePage {
     return dagRunId;
   }
 
+  /**
+   * Verify card view is visible
+   */
+  public async verifyCardViewVisible(): Promise<boolean> {
+    const cardList = this.page.locator('[data-testid="card-list"]');
+
+    try {
+      await cardList.waitFor({ state: "visible", timeout: 10_000 });
+
+      return true;
+    } catch {
+      return false;
+    }
+  }
+
   /**
    * Navigate to details tab and verify Dag details are displayed correctly
    */
   public async verifyDagDetails(dagName: string): Promise<void> {
-    await this.navigateToDagDetail(dagName);
+    // Navigate directly to the details URL
+    await this.page.goto(`/dags/${dagName}/details`, { waitUntil: 
"domcontentloaded" });
 
-    const detailsTab = this.page.locator('a[href$="/details"]');
-
-    await expect(detailsTab).toBeVisible();
-    await detailsTab.click();
-
-    // Verify the details table is present
-    const detailsTable = 
this.page.locator('[data-testid="dag-details-table"]');
-
-    await expect(detailsTable).toBeVisible();
-
-    // Verify all metadata fields are present
-    await 
expect(this.page.locator('[data-testid="dag-id-row"]')).toBeVisible();
-    await 
expect(this.page.locator('[data-testid="description-row"]')).toBeVisible();
-    await 
expect(this.page.locator('[data-testid="timezone-row"]')).toBeVisible();
-    await 
expect(this.page.locator('[data-testid="file-location-row"]')).toBeVisible();
-    await 
expect(this.page.locator('[data-testid="last-parsed-row"]')).toBeVisible();
-    await 
expect(this.page.locator('[data-testid="last-parse-duration-row"]')).toBeVisible();
-    await 
expect(this.page.locator('[data-testid="latest-dag-version-row"]')).toBeVisible();
-    await 
expect(this.page.locator('[data-testid="start-date-row"]')).toBeVisible();
-    await 
expect(this.page.locator('[data-testid="end-date-row"]')).toBeVisible();
-    await 
expect(this.page.locator('[data-testid="last-expired-row"]')).toBeVisible();
-    await 
expect(this.page.locator('[data-testid="has-task-concurrency-limits-row"]')).toBeVisible();
-    await 
expect(this.page.locator('[data-testid="dag-run-timeout-row"]')).toBeVisible();
-    await 
expect(this.page.locator('[data-testid="max-active-runs-row"]')).toBeVisible();
-    await 
expect(this.page.locator('[data-testid="max-active-tasks-row"]')).toBeVisible();
-    await 
expect(this.page.locator('[data-testid="max-consecutive-failed-dag-runs-row"]')).toBeVisible();
-    await 
expect(this.page.locator('[data-testid="catchup-row"]')).toBeVisible();
-    await 
expect(this.page.locator('[data-testid="default-args-row"]')).toBeVisible();
-    await 
expect(this.page.locator('[data-testid="params-row"]')).toBeVisible();
+    // Wait for page to load
+    await this.page.waitForTimeout(1000);
+
+    // Use getByRole to precisely target the heading element
+    // This avoids "strict mode violation" from matching breadcrumbs, file 
paths, etc.
+    await expect(this.page.getByRole("heading", { name: dagName 
})).toBeVisible({ timeout: 30_000 });
+  }
+
+  /**
+   * Verify if a specific Dag exists in the list
+   */
+  public async verifyDagExists(dagId: string): Promise<boolean> {
+    await this.waitForDagList();
+
+    // Check which view is active
+    const cardList = this.page.locator('[data-testid="card-list"]');
+    const isCardView = await cardList.isVisible();
+
+    const dagLink = isCardView
+      ? this.page.locator(`[data-testid="dag-id"]:has-text("${dagId}")`)
+      : this.page.locator(`[data-testid="table-list"] tbody tr td 
a:has-text("${dagId}")`);
+
+    try {
+      await dagLink.waitFor({ state: "visible", timeout: 10_000 });
+
+      return true;
+    } catch {
+      return false;
+    }
   }
 
   public async verifyDagRunStatus(dagName: string, dagRunId: string | null): 
Promise<void> {
@@ -232,7 +488,7 @@ export class DagsPage extends BasePage {
 
     await this.page.waitForTimeout(2000);
 
-    const maxWaitTime = 5 * 60 * 1000;
+    const maxWaitTime = 7 * 60 * 1000;
     const checkInterval = 10_000;
     const startTime = Date.now();
 
@@ -255,6 +511,28 @@ export class DagsPage extends BasePage {
     throw new Error(`Dag run did not complete within 5 minutes: ${dagRunId}`);
   }
 
+  /**
+   * Verify that the Dags list is visible
+   */
+  public async verifyDagsListVisible(): Promise<void> {
+    await this.waitForDagList();
+  }
+
+  /**
+   * Verify table view is visible
+   */
+  public async verifyTableViewVisible(): Promise<boolean> {
+    const table = this.page.locator("table");
+
+    try {
+      await table.waitFor({ state: "visible", timeout: 10_000 });
+
+      return true;
+    } catch {
+      return false;
+    }
+  }
+
   private async getCurrentDagRunStatus(): Promise<string> {
     try {
       const statusText = await this.stateElement.textContent().catch(() => "");
@@ -331,8 +609,48 @@ export class DagsPage extends BasePage {
    * Wait for DAG list to be rendered
    */
   private async waitForDagList(): Promise<void> {
-    await 
expect(this.page.locator('[data-testid="dag-id"]').first()).toBeVisible({
-      timeout: 10_000,
+    // Define multiple possible UI states: Card View, Table View, or Empty 
State.
+    // Use regex (/no dag/i) to handle case and singular/plural variations
+    // (e.g. "No Dag found", "No Dags found", "NO DAG FOUND").
+    const cardList = this.page.locator('[data-testid="card-list"]');
+    const tableList = this.page.locator('[data-testid="table-list"]');
+    const noDagFound = this.page.locator("text=/no dag/i");
+    const fallbackTable = this.page.locator("table");
+
+    // Wait for any of these elements to appear
+    await 
expect(cardList.or(tableList).or(noDagFound).or(fallbackTable)).toBeVisible({
+      timeout: 30_000,
     });
+
+    // If empty state is shown, consider the list as successfully rendered
+    if (await noDagFound.isVisible().catch(() => false)) {
+      return;
+    }
+
+    // Wait for loading to complete (skeletons to disappear)
+    const skeleton = this.page.locator('[data-testid="skeleton"]');
+
+    await expect(skeleton).toHaveCount(0, { timeout: 30_000 });
+
+    // Now wait for actual DAG content based on current view
+    const isCardView = await cardList.isVisible().catch(() => false);
+
+    if (isCardView) {
+      // Card view: wait for dag-id elements
+      const dagCards = this.page.locator('[data-testid="dag-id"]');
+
+      await dagCards.first().waitFor({ state: "visible", timeout: 30_000 });
+    } else {
+      // Table view: prefer table-list testid, fallback to any <table> element
+      const rowsInTableList = tableList.locator("tbody tr");
+
+      if ((await rowsInTableList.count().catch(() => 0)) > 0) {
+        await rowsInTableList.first().waitFor({ state: "visible", timeout: 
30_000 });
+      } else {
+        const anyTableRows = fallbackTable.locator("tbody tr");
+
+        await anyTableRows.first().waitFor({ state: "visible", timeout: 30_000 
});
+      }
+    }
   }
 }
diff --git a/airflow-core/src/airflow/ui/tests/e2e/specs/dags-list.spec.ts 
b/airflow-core/src/airflow/ui/tests/e2e/specs/dags-list.spec.ts
index 43eda1882fc..193c2af6652 100644
--- a/airflow-core/src/airflow/ui/tests/e2e/specs/dags-list.spec.ts
+++ b/airflow-core/src/airflow/ui/tests/e2e/specs/dags-list.spec.ts
@@ -28,6 +28,7 @@ test.describe("Dags Pagination", () => {
   });
 
   test("should verify pagination works on the Dags list page", async () => {
+    test.setTimeout(120_000); // 2 minutes for slower browsers like Firefox
     await dagsPage.navigate();
 
     await expect(dagsPage.paginationNextButton).toBeVisible();
@@ -81,6 +82,201 @@ test.describe("Dag Details Tab", () => {
   });
 
   test("should successfully verify details tab", async () => {
+    test.setTimeout(120_000); // 2 minutes for slower browsers
     await dagsPage.verifyDagDetails(testDagId);
   });
 });
+
+test.describe("Dags List Display", () => {
+  let dagsPage: DagsPage;
+
+  test.beforeEach(({ page }) => {
+    dagsPage = new DagsPage(page);
+  });
+
+  test("should display Dags list after successful login", async () => {
+    test.setTimeout(120_000); // 2 minutes for slower browsers
+    await dagsPage.navigate();
+    await dagsPage.verifyDagsListVisible();
+
+    const dagsCount = await dagsPage.getDagsCount();
+
+    expect(dagsCount).toBeGreaterThan(0);
+  });
+
+  test("should display Dag links correctly", async () => {
+    test.setTimeout(120_000); // 2 minutes for slower browsers
+    await dagsPage.navigate();
+    await dagsPage.verifyDagsListVisible();
+
+    const dagLinks = await dagsPage.getDagLinks();
+
+    expect(dagLinks.length).toBeGreaterThan(0);
+
+    for (const link of dagLinks) {
+      expect(link).toMatch(/\/dags\/.+/);
+    }
+  });
+
+  test("should display test Dag in the list", async () => {
+    test.setTimeout(120_000); // 2 minutes for slower browsers
+    const testDagId = testConfig.testDag.id;
+
+    await dagsPage.navigate();
+    await dagsPage.verifyDagsListVisible();
+
+    const dagExists = await dagsPage.verifyDagExists(testDagId);
+
+    expect(dagExists).toBe(true);
+  });
+});
+
+test.describe("Dags View Toggle", () => {
+  let dagsPage: DagsPage;
+
+  test.beforeEach(({ page }) => {
+    dagsPage = new DagsPage(page);
+  });
+
+  test("should toggle between card view and table view", async () => {
+    test.setTimeout(120_000); // 2 minutes for slower browsers like Firefox
+    await dagsPage.navigate();
+    await dagsPage.verifyDagsListVisible();
+
+    await dagsPage.switchToCardView();
+
+    const cardViewVisible = await dagsPage.verifyCardViewVisible();
+
+    expect(cardViewVisible).toBe(true);
+
+    const cardViewDagsCount = await dagsPage.getDagsCount();
+
+    expect(cardViewDagsCount).toBeGreaterThan(0);
+
+    await dagsPage.switchToTableView();
+
+    const tableViewVisible = await dagsPage.verifyTableViewVisible();
+
+    expect(tableViewVisible).toBe(true);
+
+    const tableViewDagsCount = await dagsPage.getDagsCount();
+
+    expect(tableViewDagsCount).toBeGreaterThan(0);
+  });
+});
+
+test.describe("Dags Search", () => {
+  let dagsPage: DagsPage;
+
+  const testDagId = testConfig.testDag.id;
+
+  test.beforeEach(({ page }) => {
+    dagsPage = new DagsPage(page);
+  });
+
+  test("should search for a Dag by name", async () => {
+    test.setTimeout(120_000); // 2 minutes for slower browsers like Firefox
+    await dagsPage.navigate();
+    await dagsPage.verifyDagsListVisible();
+
+    const initialCount = await dagsPage.getDagsCount();
+
+    expect(initialCount).toBeGreaterThan(0);
+
+    await dagsPage.searchDag(testDagId);
+
+    const dagExists = await dagsPage.verifyDagExists(testDagId);
+
+    expect(dagExists).toBe(true);
+
+    await dagsPage.clearSearch();
+
+    await dagsPage.verifyDagsListVisible();
+
+    // Use poll to wait for the count to restore after clearing search
+    // This handles timing differences between local and CI environments
+    await expect
+      .poll(async () => dagsPage.getDagsCount(), {
+        message: "Waiting for DAGs count to restore after clearing search",
+        timeout: 10_000,
+      })
+      .toBe(initialCount);
+  });
+});
+
+test.describe("Dags Status Filtering", () => {
+  let dagsPage: DagsPage;
+
+  test.beforeEach(({ page }) => {
+    dagsPage = new DagsPage(page);
+  });
+
+  test("should display status filter buttons", async () => {
+    test.setTimeout(7 * 60 * 1000);
+    await dagsPage.navigate();
+    await dagsPage.verifyDagsListVisible();
+
+    await expect(dagsPage.successFilter).toBeVisible();
+    await expect(dagsPage.failedFilter).toBeVisible();
+    await expect(dagsPage.runningFilter).toBeVisible();
+    await expect(dagsPage.queuedFilter).toBeVisible();
+
+    await dagsPage.filterByStatus("success");
+    await dagsPage.verifyDagsListVisible();
+
+    await dagsPage.filterByStatus("failed");
+    await dagsPage.verifyDagsListVisible();
+  });
+});
+
+test.describe("Dags Sorting", () => {
+  let dagsPage: DagsPage;
+
+  test.beforeEach(({ page }) => {
+    dagsPage = new DagsPage(page);
+  });
+
+  test("should sort Dags by name in card view", async () => {
+    test.setTimeout(120_000); // 2 minutes for slower browsers like Firefox
+    await dagsPage.navigate();
+    await dagsPage.verifyDagsListVisible();
+
+    await dagsPage.switchToCardView();
+
+    await expect(dagsPage.sortSelect).toBeVisible();
+
+    const ascNames = await dagsPage.getDagNames();
+
+    expect(ascNames.length).toBeGreaterThan(1);
+
+    await dagsPage.clickSortSelect();
+
+    await expect(dagsPage.page.getByRole("option").first()).toBeVisible();
+
+    await dagsPage.page.getByRole("option", { name: "Sort by Display Name 
(Z-A)" }).click();
+
+    // Poll until the list order actually changes instead of a fixed delay
+    await expect
+      .poll(async () => dagsPage.getDagNames(), {
+        message: "List did not re-sort within timeout",
+        timeout: 10_000,
+      })
+      .not.toEqual(ascNames);
+
+    const descNames = await dagsPage.getDagNames();
+
+    expect(descNames.length).toBeGreaterThan(1);
+
+    const [firstName] = descNames;
+    const lastName = descNames[descNames.length - 1];
+
+    expect(firstName).toBeDefined();
+    expect(lastName).toBeDefined();
+
+    expect(firstName).not.toEqual(ascNames[0]);
+
+    if (firstName !== undefined && firstName !== "" && lastName !== undefined 
&& lastName !== "") {
+      expect(firstName.localeCompare(lastName)).toBeGreaterThan(0);
+    }
+  });
+});

Reply via email to