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);
+ }
+ });
+});