Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 18 additions & 12 deletions airflow-core/src/airflow/ui/src/pages/DagRuns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ const runColumns = (translate: TFunction, dagId?: string): Array<ColumnDef<DAGRu
accessorKey: "dag_run_id",
cell: ({ row: { original } }: DagRunRow) => (
<Link asChild color="fg.info" fontWeight="bold">
<RouterLink to={`/dags/${original.dag_id}/runs/${original.dag_run_id}`}>
<RouterLink data-testid="run-id" to={`/dags/${original.dag_id}/runs/${original.dag_run_id}`}>
<TruncatedText text={original.dag_run_id} />
</RouterLink>
</Link>
Expand All @@ -103,7 +103,11 @@ const runColumns = (translate: TFunction, dagId?: string): Array<ColumnDef<DAGRu
row: {
original: { state },
},
}) => <StateBadge state={state}>{translate(`common:states.${state}`)}</StateBadge>,
}) => (
<Flex data-testid="run-state">
<StateBadge state={state}>{translate(`common:states.${state}`)}</StateBadge>
</Flex>
),
header: () => translate("state"),
},
{
Expand Down Expand Up @@ -242,16 +246,18 @@ export const DagRuns = () => {
return (
<>
<DagRunsFilters dagId={dagId} />
<DataTable
columns={columns}
data={data?.dag_runs ?? []}
errorMessage={<ErrorAlert error={error} />}
initialState={tableURLState}
isLoading={isLoading}
modelName={translate("common:dagRun_other")}
onStateChange={setTableURLState}
total={data?.total_entries}
/>
<div data-testid="dag-runs-table">
<DataTable
columns={columns}
data={data?.dag_runs ?? []}
errorMessage={<ErrorAlert error={error} />}
initialState={tableURLState}
isLoading={isLoading}
modelName={translate("common:dagRun_other")}
onStateChange={setTableURLState}
total={data?.total_entries}
/>
</div>
</>
);
};
209 changes: 136 additions & 73 deletions airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,33 +30,29 @@
return "/dags";
}

// Core page elements
public readonly confirmButton: Locator;
public readonly operatorFilter: Locator;
public readonly dagsTable: Locator;
// Pagination elements
public readonly paginationNextButton: Locator;
public readonly paginationPrevButton: Locator;
public readonly retriesFilter: Locator;
public readonly searchBox: Locator;
// Runs tab elements
public readonly runsTab: Locator;
public readonly runsTable: Locator;

public readonly stateElement: Locator;
public readonly triggerButton: Locator;
public readonly triggerRuleFilter: Locator;

public get taskCards(): Locator {
// CardList component renders a SimpleGrid with data-testid="card-list"
// Individual cards are direct children (Box elements)
return this.page.locator('[data-testid="card-list"] > div');
}

public constructor(page: Page) {
super(page);
this.dagsTable = page.locator('div:has(a[href*="/dags/"])');
this.triggerButton = page.locator('button[aria-label="Trigger Dag"]:has-text("Trigger")');
this.confirmButton = page.locator('button:has-text("Trigger")').nth(1);
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.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 });
this.runsTab = page.locator('a[href$="/runs"]');
this.runsTable = page.locator('[data-testid="dag-runs-table"]');
}

// URL builders for dynamic paths
Expand All @@ -72,37 +68,72 @@
* Click next page button
*/
public async clickNextPage(): Promise<void> {
const initialDagNames = await this.getDagNames();

await this.paginationNextButton.click();

await expect.poll(() => this.getDagNames(), { timeout: 10_000 }).not.toEqual(initialDagNames);

await this.waitForDagList();
}

/**
* Click previous page button
*/
public async clickPrevPage(): Promise<void> {
const initialDagNames = await this.getDagNames();

await this.paginationPrevButton.click();

await expect.poll(() => this.getDagNames(), { timeout: 10_000 }).not.toEqual(initialDagNames);
await this.waitForDagList();
}

public async filterByOperator(operator: string): Promise<void> {
await this.selectDropdownOption(this.operatorFilter, operator);
/**
* Click on a specific run to view details
*/
public async clickRun(runId: string): Promise<void> {
const runLink = this.page.locator(`a:has-text("${runId}")`).first();

await runLink.waitFor({ state: "visible" });
await runLink.click();
await this.page.waitForLoadState("networkidle");
}

/**
* Click next page button for runs
*/
public async clickRunPaginationNext(): Promise<void> {
await this.paginationNextButton.click();

Check failure on line 98 in airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts

View workflow job for this annotation

GitHub Actions / Additional PROD image tests / Firefox UI e2e tests with PROD image / Firefox UI e2e tests

[firefox] › tests/e2e/specs/dag-runs.spec.ts:107:3 › Dag Run Tests › verify pagination

1) [firefox] › tests/e2e/specs/dag-runs.spec.ts:107:3 › Dag Run Tests › verify pagination ──────── Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── TimeoutError: locator.click: Timeout 10000ms exceeded. Call log: - waiting for locator('[data-testid="next"]') at ../pages/DagsPage.ts:98 96 | */ 97 | public async clickRunPaginationNext(): Promise<void> { > 98 | await this.paginationNextButton.click(); | ^ 99 | await this.waitForRunList(); 100 | } 101 | at DagsPage.clickRunPaginationNext (/home/runner/work/airflow/airflow/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts:98:37) at /home/runner/work/airflow/airflow/airflow-core/src/airflow/ui/tests/e2e/specs/dag-runs.spec.ts:118:20

Check failure on line 98 in airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts

View workflow job for this annotation

GitHub Actions / Additional PROD image tests / Firefox UI e2e tests with PROD image / Firefox UI e2e tests

[firefox] › tests/e2e/specs/dag-runs.spec.ts:107:3 › Dag Run Tests › verify pagination

1) [firefox] › tests/e2e/specs/dag-runs.spec.ts:107:3 › Dag Run Tests › verify pagination ──────── Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── TimeoutError: locator.click: Timeout 10000ms exceeded. Call log: - waiting for locator('[data-testid="next"]') at ../pages/DagsPage.ts:98 96 | */ 97 | public async clickRunPaginationNext(): Promise<void> { > 98 | await this.paginationNextButton.click(); | ^ 99 | await this.waitForRunList(); 100 | } 101 | at DagsPage.clickRunPaginationNext (/home/runner/work/airflow/airflow/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts:98:37) at /home/runner/work/airflow/airflow/airflow-core/src/airflow/ui/tests/e2e/specs/dag-runs.spec.ts:118:20

Check failure on line 98 in airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts

View workflow job for this annotation

GitHub Actions / Additional PROD image tests / Firefox UI e2e tests with PROD image / Firefox UI e2e tests

[firefox] › tests/e2e/specs/dag-runs.spec.ts:107:3 › Dag Run Tests › verify pagination

1) [firefox] › tests/e2e/specs/dag-runs.spec.ts:107:3 › Dag Run Tests › verify pagination ──────── TimeoutError: locator.click: Timeout 10000ms exceeded. Call log: - waiting for locator('[data-testid="next"]') at ../pages/DagsPage.ts:98 96 | */ 97 | public async clickRunPaginationNext(): Promise<void> { > 98 | await this.paginationNextButton.click(); | ^ 99 | await this.waitForRunList(); 100 | } 101 | at DagsPage.clickRunPaginationNext (/home/runner/work/airflow/airflow/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts:98:37) at /home/runner/work/airflow/airflow/airflow-core/src/airflow/ui/tests/e2e/specs/dag-runs.spec.ts:118:20

Check failure on line 98 in airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts

View workflow job for this annotation

GitHub Actions / Additional PROD image tests / WebKit UI e2e tests with PROD image / WebKit UI e2e tests

[webkit] › tests/e2e/specs/dag-runs.spec.ts:107:3 › Dag Run Tests › verify pagination

1) [webkit] › tests/e2e/specs/dag-runs.spec.ts:107:3 › Dag Run Tests › verify pagination ───────── Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── TimeoutError: locator.click: Timeout 10000ms exceeded. Call log: - waiting for locator('[data-testid="next"]') at ../pages/DagsPage.ts:98 96 | */ 97 | public async clickRunPaginationNext(): Promise<void> { > 98 | await this.paginationNextButton.click(); | ^ 99 | await this.waitForRunList(); 100 | } 101 | at DagsPage.clickRunPaginationNext (/home/runner/work/airflow/airflow/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts:98:37) at /home/runner/work/airflow/airflow/airflow-core/src/airflow/ui/tests/e2e/specs/dag-runs.spec.ts:118:20

Check failure on line 98 in airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts

View workflow job for this annotation

GitHub Actions / Additional PROD image tests / WebKit UI e2e tests with PROD image / WebKit UI e2e tests

[webkit] › tests/e2e/specs/dag-runs.spec.ts:107:3 › Dag Run Tests › verify pagination

1) [webkit] › tests/e2e/specs/dag-runs.spec.ts:107:3 › Dag Run Tests › verify pagination ───────── Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── TimeoutError: locator.click: Timeout 10000ms exceeded. Call log: - waiting for locator('[data-testid="next"]') at ../pages/DagsPage.ts:98 96 | */ 97 | public async clickRunPaginationNext(): Promise<void> { > 98 | await this.paginationNextButton.click(); | ^ 99 | await this.waitForRunList(); 100 | } 101 | at DagsPage.clickRunPaginationNext (/home/runner/work/airflow/airflow/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts:98:37) at /home/runner/work/airflow/airflow/airflow-core/src/airflow/ui/tests/e2e/specs/dag-runs.spec.ts:118:20

Check failure on line 98 in airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts

View workflow job for this annotation

GitHub Actions / Additional PROD image tests / WebKit UI e2e tests with PROD image / WebKit UI e2e tests

[webkit] › tests/e2e/specs/dag-runs.spec.ts:107:3 › Dag Run Tests › verify pagination

1) [webkit] › tests/e2e/specs/dag-runs.spec.ts:107:3 › Dag Run Tests › verify pagination ───────── TimeoutError: locator.click: Timeout 10000ms exceeded. Call log: - waiting for locator('[data-testid="next"]') at ../pages/DagsPage.ts:98 96 | */ 97 | public async clickRunPaginationNext(): Promise<void> { > 98 | await this.paginationNextButton.click(); | ^ 99 | await this.waitForRunList(); 100 | } 101 | at DagsPage.clickRunPaginationNext (/home/runner/work/airflow/airflow/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts:98:37) at /home/runner/work/airflow/airflow/airflow-core/src/airflow/ui/tests/e2e/specs/dag-runs.spec.ts:118:20

Check failure on line 98 in airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts

View workflow job for this annotation

GitHub Actions / Additional PROD image tests / Chromium UI e2e tests with PROD image / Chromium UI e2e tests

[chromium] › tests/e2e/specs/dag-runs.spec.ts:107:3 › Dag Run Tests › verify pagination

1) [chromium] › tests/e2e/specs/dag-runs.spec.ts:107:3 › Dag Run Tests › verify pagination ─────── Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── TimeoutError: locator.click: Timeout 10000ms exceeded. Call log: - waiting for locator('[data-testid="next"]') at ../pages/DagsPage.ts:98 96 | */ 97 | public async clickRunPaginationNext(): Promise<void> { > 98 | await this.paginationNextButton.click(); | ^ 99 | await this.waitForRunList(); 100 | } 101 | at DagsPage.clickRunPaginationNext (/home/runner/work/airflow/airflow/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts:98:37) at /home/runner/work/airflow/airflow/airflow-core/src/airflow/ui/tests/e2e/specs/dag-runs.spec.ts:118:20

Check failure on line 98 in airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts

View workflow job for this annotation

GitHub Actions / Additional PROD image tests / Chromium UI e2e tests with PROD image / Chromium UI e2e tests

[chromium] › tests/e2e/specs/dag-runs.spec.ts:107:3 › Dag Run Tests › verify pagination

1) [chromium] › tests/e2e/specs/dag-runs.spec.ts:107:3 › Dag Run Tests › verify pagination ─────── Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── TimeoutError: locator.click: Timeout 10000ms exceeded. Call log: - waiting for locator('[data-testid="next"]') at ../pages/DagsPage.ts:98 96 | */ 97 | public async clickRunPaginationNext(): Promise<void> { > 98 | await this.paginationNextButton.click(); | ^ 99 | await this.waitForRunList(); 100 | } 101 | at DagsPage.clickRunPaginationNext (/home/runner/work/airflow/airflow/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts:98:37) at /home/runner/work/airflow/airflow/airflow-core/src/airflow/ui/tests/e2e/specs/dag-runs.spec.ts:118:20

Check failure on line 98 in airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts

View workflow job for this annotation

GitHub Actions / Additional PROD image tests / Chromium UI e2e tests with PROD image / Chromium UI e2e tests

[chromium] › tests/e2e/specs/dag-runs.spec.ts:107:3 › Dag Run Tests › verify pagination

1) [chromium] › tests/e2e/specs/dag-runs.spec.ts:107:3 › Dag Run Tests › verify pagination ─────── TimeoutError: locator.click: Timeout 10000ms exceeded. Call log: - waiting for locator('[data-testid="next"]') at ../pages/DagsPage.ts:98 96 | */ 97 | public async clickRunPaginationNext(): Promise<void> { > 98 | await this.paginationNextButton.click(); | ^ 99 | await this.waitForRunList(); 100 | } 101 | at DagsPage.clickRunPaginationNext (/home/runner/work/airflow/airflow/airflow-core/src/airflow/ui/tests/e2e/pages/DagsPage.ts:98:37) at /home/runner/work/airflow/airflow/airflow-core/src/airflow/ui/tests/e2e/specs/dag-runs.spec.ts:118:20
await this.waitForRunList();
}

public async filterByRetries(retries: string): Promise<void> {
await this.selectDropdownOption(this.retriesFilter, retries);
/**
* Click previous page button for runs
*/
public async clickRunPaginationPrev(): Promise<void> {
await this.paginationPrevButton.click();
await this.waitForRunList();
}

public async filterByTriggerRule(rule: string): Promise<void> {
await this.selectDropdownOption(this.triggerRuleFilter, rule);
/**
* Filter runs by state
*/
public async filterByState(state: string): Promise<void> {
// Use URL-based filtering instead of UI interaction for reliability
// The FilterBar component uses search params, so we can navigate directly
const currentUrl = this.page.url();
const url = new URL(currentUrl);

// Set the state parameter (convert to lowercase as API expects lowercase)
url.searchParams.set("state", state.toLowerCase());

// Navigate to the URL with the filter applied
await this.page.goto(url.toString(), { waitUntil: "networkidle" });

// Wait for the table to update with filtered results
await this.runsTable.waitFor({ state: "visible" });

// Wait for data to load - check for either rows or "no data" message
const runRows = this.page.locator('[data-testid="dag-runs-table"] table tbody tr');

try {
await runRows.first().waitFor({ state: "visible", timeout: 15_000 });
} catch {
// If no rows appear, that's okay - might be no matching results
// The test will handle this
}
}

/**
Expand All @@ -116,37 +147,32 @@
return texts.map((text) => text.trim()).filter((text) => text !== "");
}

public async getFilterOptions(filter: Locator): Promise<Array<string>> {
await filter.click();
await this.page.waitForTimeout(500);

const controlsId = await filter.getAttribute("aria-controls");
let options;
/**
* Get run details from the runs table
*/
public async getRunDetails(): Promise<
Array<{
runId: string;
state: string;
}>
> {
const runRows = this.page.locator('[data-testid="dag-runs-table"] table tbody tr');

if (controlsId === null) {
const listbox = this.page.locator('div[role="listbox"]').first();
await runRows.first().waitFor({ state: "visible", timeout: 10_000 });

await listbox.waitFor({ state: "visible", timeout: 5000 });
options = listbox.locator('div[role="option"]');
} else {
options = this.page.locator(`[id="${controlsId}"] div[role="option"]`);
}
const runCount = await runRows.count();
const runs: Array<{ runId: string; state: string }> = [];

const count = await options.count();
const dataValues: Array<string> = [];
for (let i = 0; i < runCount; i++) {
const row = runRows.nth(i);

for (let i = 0; i < count; i++) {
const value = await options.nth(i).getAttribute("data-value");
const runId = (await row.locator('[data-testid="run-id"]').textContent()) ?? "";
const state = (await row.locator('[data-testid="run-state"]').textContent()) ?? "";

if (value !== null && value.trim().length > 0) {
dataValues.push(value);
}
runs.push({ runId: runId.trim(), state: state.trim() });
}

await this.page.keyboard.press("Escape");
await this.page.waitForTimeout(300);

return dataValues;
return runs;
}

/**
Expand All @@ -163,30 +189,50 @@
await this.navigateTo(DagsPage.getDagDetailUrl(dagName));
}

public async navigateToDagTasks(dagId: string): Promise<void> {
await this.page.goto(`/dags/${dagId}/tasks`);
await this.page
.locator("h2")
.filter({ hasText: /^Operator$/ })
.first()
.waitFor({ state: "visible", timeout: 30_000 });
/**
* Navigate to the Runs tab for a specific DAG
*/
public async navigateToRunsTab(dagName: string): Promise<void> {
await this.navigateToDagDetail(dagName);
await this.runsTab.waitFor({ state: "visible" });
await this.runsTab.click();
await this.runsTable.waitFor({ state: "visible", timeout: 30_000 });
}

/**
* Search for dag runs by run ID pattern
*/
public async searchRun(searchTerm: string): Promise<void> {
Comment thread
Sahil-Shadwal marked this conversation as resolved.
// Find the run ID pattern input field
const searchInput = this.page
.locator('input[placeholder*="Run ID"]')
.or(this.page.locator('input[name="runIdPattern"]'));

await searchInput.waitFor({ state: "visible" });
await searchInput.fill(searchTerm);

// Wait for the search to take effect
await this.page.waitForResponse(
(response) =>
response.url().includes("dagRuns") &&
response.request().method() === "GET" &&
response.status() === 200,
);
await this.page.waitForLoadState("networkidle");
}

/**
* Trigger a Dag run
*/
public async triggerDag(dagName: string): Promise<string | null> {
await this.navigateToDagDetail(dagName);
await expect(this.triggerButton).toBeVisible({ timeout: 10_000 });
await this.triggerButton.waitFor({ state: "visible", timeout: 30_000 });
await this.triggerButton.click();
const dagRunId = await this.handleTriggerDialog();

return dagRunId;
}

/**
* Navigate to details tab and verify Dag details are displayed correctly
*/
public async verifyDagDetails(dagName: string): Promise<void> {
await this.navigateToDagDetail(dagName);

Expand Down Expand Up @@ -241,10 +287,8 @@
while (Date.now() - startTime < maxWaitTime) {
const currentStatus = await this.getCurrentDagRunStatus();

if (currentStatus === "success") {
if (currentStatus === "success" || currentStatus === "failed") {
return;
} else if (currentStatus === "failed") {
throw new Error(`Dag run failed: ${dagRunId}`);
}

await this.page.waitForTimeout(checkInterval);
Expand All @@ -257,6 +301,25 @@
throw new Error(`Dag run did not complete within 5 minutes: ${dagRunId}`);
}

/**
* Verify we're on the run details page
*/
public async verifyRunDetailsPage(runId: string): Promise<void> {
// Wait for the page to load
await this.page.waitForLoadState("networkidle");

// Verify URL contains the run ID
await expect(this.page).toHaveURL(new RegExp(`/runs/${runId.replaceAll(/[$()*+.?[\\\]^{|}]/g, "\\$&")}`));
}

/**
* Verify the Runs tab is displayed correctly
*/
public async verifyRunsTabDisplayed(): Promise<void> {
// Verify the runs table is present
await expect(this.runsTable).toBeVisible();
}

private async getCurrentDagRunStatus(): Promise<string> {
try {
const statusText = await this.stateElement.textContent().catch(() => "");
Expand Down Expand Up @@ -323,12 +386,6 @@
return null;
}

private async selectDropdownOption(filter: Locator, value: string): Promise<void> {
await filter.click();
await this.page.locator(`div[role="option"][data-value="${value}"]`).dispatchEvent("click");
await this.page.waitForTimeout(500);
}

/**
* Wait for DAG list to be rendered
*/
Expand All @@ -337,4 +394,10 @@
timeout: 10_000,
});
}

private async waitForRunList(): Promise<void> {
await expect(this.page.locator('[data-testid="run-id"]').first()).toBeVisible({
timeout: 10_000,
});
}
}
Loading
Loading