Skip to content

Commit 2ca87da

Browse files
Add E2E tests to verify DAG runs tab functionality #59541 (#61234)
1 parent 1c41180 commit 2ca87da

File tree

4 files changed

+421
-0
lines changed

4 files changed

+421
-0
lines changed

airflow-core/src/airflow/ui/src/components/MarkAs/Run/MarkRunAsButton.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ const MarkRunAsButton = ({ dagRun, isHotkeyEnabled = false }: Props) => {
7171
type: translate("dagRun_one"),
7272
})}
7373
colorPalette="brand"
74+
data-testid="mark-run-as-button"
7475
size="md"
7576
variant="ghost"
7677
>

airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGButton.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export const TriggerDAGButton = ({
9595
<Button
9696
aria-label={translate("triggerDag.title")}
9797
colorPalette="brand"
98+
data-testid="trigger-dag-button"
9899
size="md"
99100
variant={variant}
100101
>
@@ -132,6 +133,7 @@ export const TriggerDAGButton = ({
132133
<Button
133134
aria-label={translate("triggerDag.title")}
134135
colorPalette="brand"
136+
data-testid="trigger-dag-button"
135137
onClick={handleNormalTrigger}
136138
size="md"
137139
variant={variant}
@@ -143,6 +145,7 @@ export const TriggerDAGButton = ({
143145
<IconButton
144146
aria-label={translate("triggerDag.title")}
145147
colorPalette="brand"
148+
data-testid="trigger-dag-button"
146149
onClick={onOpen}
147150
size="md"
148151
variant={variant}
Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
/*!
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
import { expect, type Locator, type Page } from "@playwright/test";
20+
import { BasePage } from "tests/e2e/pages/BasePage";
21+
22+
export class DagRunsTabPage extends BasePage {
23+
public readonly markRunAsButton: Locator;
24+
public readonly nextPageButton: Locator;
25+
public readonly prevPageButton: Locator;
26+
public readonly runsTable: Locator;
27+
public readonly tableRows: Locator;
28+
public readonly triggerButton: Locator;
29+
30+
private currentDagId?: string;
31+
private currentLimit?: number;
32+
33+
public constructor(page: Page) {
34+
super(page);
35+
this.markRunAsButton = page.locator('[data-testid="mark-run-as-button"]').first();
36+
this.nextPageButton = page.locator('[data-testid="next"]');
37+
this.prevPageButton = page.locator('[data-testid="prev"]');
38+
this.runsTable = page.locator('[data-testid="table-list"]');
39+
this.tableRows = this.runsTable.locator("tbody tr");
40+
this.triggerButton = page.locator('[data-testid="trigger-dag-button"]');
41+
}
42+
43+
public async clickNextPage(): Promise<void> {
44+
await this.waitForRunsTableToLoad();
45+
const firstRunLink = this.tableRows.first().locator("a[href*='/runs/']").first();
46+
47+
await expect(firstRunLink).toBeVisible();
48+
const firstRunId = await firstRunLink.textContent();
49+
50+
if (firstRunId === null || firstRunId === "") {
51+
throw new Error("Could not get first run ID before pagination");
52+
}
53+
54+
await this.nextPageButton.click();
55+
await expect(this.tableRows.first()).not.toContainText(firstRunId, { timeout: 10_000 });
56+
await this.ensureUrlParams();
57+
}
58+
59+
public async clickPrevPage(): Promise<void> {
60+
await this.waitForRunsTableToLoad();
61+
const firstRunLink = this.tableRows.first().locator("a[href*='/runs/']").first();
62+
63+
await expect(firstRunLink).toBeVisible();
64+
const firstRunId = await firstRunLink.textContent();
65+
66+
if (firstRunId === null || firstRunId === "") {
67+
throw new Error("Could not get first run ID before pagination");
68+
}
69+
70+
await this.prevPageButton.click();
71+
await expect(this.tableRows.first()).not.toContainText(firstRunId, { timeout: 10_000 });
72+
await this.ensureUrlParams();
73+
}
74+
75+
public async clickRunAndVerifyDetails(): Promise<void> {
76+
const firstRunLink = this.tableRows.first().locator("a[href*='/runs/']").first();
77+
78+
await expect(firstRunLink).toBeVisible({ timeout: 10_000 });
79+
await firstRunLink.click();
80+
await this.page.waitForURL(/.*\/dags\/.*\/runs\/[^/]+$/, { timeout: 15_000 });
81+
await expect(this.markRunAsButton).toBeVisible({ timeout: 10_000 });
82+
}
83+
84+
public async clickRunsTab(): Promise<void> {
85+
const runsTab = this.page.locator('a[href$="/runs"]');
86+
87+
await expect(runsTab).toBeVisible({ timeout: 10_000 });
88+
await runsTab.click();
89+
await this.page.waitForURL(/.*\/dags\/[^/]+\/runs/, { timeout: 15_000 });
90+
await this.waitForRunsTableToLoad();
91+
}
92+
93+
public async clickRunsTabWithPageSize(dagId: string, pageSize: number): Promise<void> {
94+
this.currentDagId = dagId;
95+
this.currentLimit = pageSize;
96+
97+
await this.navigateTo(`/dags/${dagId}/runs?offset=0&limit=${pageSize}`);
98+
await this.page.waitForURL(/.*\/dags\/[^/]+\/runs.*offset=0&limit=/, {
99+
timeout: 15_000,
100+
});
101+
await this.waitForRunsTableToLoad();
102+
}
103+
104+
public async filterByState(state: string): Promise<void> {
105+
const currentUrl = new URL(this.page.url());
106+
107+
currentUrl.searchParams.set("state", state.toLowerCase());
108+
await this.navigateTo(currentUrl.pathname + currentUrl.search);
109+
await this.page.waitForURL(/.*state=.*/, { timeout: 15_000 });
110+
await this.waitForRunsTableToLoad();
111+
}
112+
113+
public async getRowCount(): Promise<number> {
114+
await this.waitForRunsTableToLoad();
115+
116+
return this.tableRows.count();
117+
}
118+
119+
public async markRunAs(state: "failed" | "success"): Promise<void> {
120+
const stateBadge = this.page.locator('[data-testid="state-badge"]').first();
121+
122+
await expect(stateBadge).toBeVisible({ timeout: 10_000 });
123+
const currentState = await stateBadge.textContent();
124+
125+
if (currentState?.toLowerCase().includes(state)) {
126+
return;
127+
}
128+
129+
await expect(this.markRunAsButton).toBeVisible({ timeout: 10_000 });
130+
await this.markRunAsButton.click();
131+
132+
const stateOption = this.page.locator(`[data-testid="mark-run-as-${state}"]`);
133+
134+
await expect(stateOption).toBeVisible({ timeout: 5000 });
135+
await stateOption.click();
136+
137+
const confirmButton = this.page.getByRole("button", { name: "Confirm" });
138+
139+
await expect(confirmButton).toBeVisible({ timeout: 5000 });
140+
141+
const responsePromise = this.page.waitForResponse(
142+
(response) => response.url().includes("dagRuns") && response.request().method() === "PATCH",
143+
{ timeout: 10_000 },
144+
);
145+
146+
await confirmButton.click();
147+
await responsePromise;
148+
149+
await expect(confirmButton).toBeHidden({ timeout: 10_000 });
150+
}
151+
152+
public async navigateToDag(dagId: string): Promise<void> {
153+
await this.navigateTo(`/dags/${dagId}`);
154+
await this.page.waitForURL(`**/dags/${dagId}**`, { timeout: 15_000 });
155+
await expect(this.triggerButton).toBeVisible({ timeout: 10_000 });
156+
}
157+
158+
public async navigateToRunDetails(dagId: string, runId: string): Promise<void> {
159+
await this.navigateTo(`/dags/${dagId}/runs/${runId}`);
160+
await this.page.waitForURL(`**/dags/${dagId}/runs/${runId}**`, { timeout: 15_000 });
161+
await expect(this.markRunAsButton).toBeVisible({ timeout: 15_000 });
162+
}
163+
164+
public async searchByRunIdPattern(pattern: string): Promise<void> {
165+
const currentUrl = new URL(this.page.url());
166+
167+
currentUrl.searchParams.set("run_id_pattern", pattern);
168+
await this.navigateTo(currentUrl.pathname + currentUrl.search);
169+
await this.page.waitForURL(/.*run_id_pattern=.*/, { timeout: 15_000 });
170+
await this.waitForRunsTableToLoad();
171+
}
172+
173+
public async triggerDagRun(): Promise<string | undefined> {
174+
await expect(this.triggerButton).toBeVisible({ timeout: 10_000 });
175+
await this.triggerButton.click();
176+
177+
const dialog = this.page.getByRole("dialog");
178+
179+
await expect(dialog).toBeVisible({ timeout: 8000 });
180+
181+
const confirmButton = dialog.getByRole("button", { name: "Trigger" });
182+
183+
await expect(confirmButton).toBeVisible({ timeout: 5000 });
184+
185+
const responsePromise = this.page.waitForResponse(
186+
(response) => {
187+
const url = response.url();
188+
const method = response.request().method();
189+
190+
return method === "POST" && url.includes("dagRuns") && !url.includes("hitlDetails");
191+
},
192+
{ timeout: 15_000 },
193+
);
194+
195+
await confirmButton.click();
196+
197+
const apiResponse = await responsePromise;
198+
199+
const responseBody = await apiResponse.text();
200+
const responseJson = JSON.parse(responseBody) as { dag_run_id?: string };
201+
202+
return responseJson.dag_run_id;
203+
}
204+
205+
public async verifyFilteredByState(expectedState: string): Promise<void> {
206+
await this.waitForRunsTableToLoad();
207+
208+
const rows = this.tableRows;
209+
210+
await expect(rows).not.toHaveCount(0);
211+
212+
const rowCount = await rows.count();
213+
214+
for (let i = 0; i < Math.min(rowCount, 5); i++) {
215+
const stateBadge = rows.nth(i).locator('[data-testid="state-badge"]');
216+
217+
await expect(stateBadge).toBeVisible();
218+
await expect(stateBadge).toContainText(expectedState, { ignoreCase: true });
219+
}
220+
}
221+
222+
public async verifyRunDetailsDisplay(): Promise<void> {
223+
const firstRow = this.tableRows.first();
224+
225+
await expect(firstRow).toBeVisible({ timeout: 10_000 });
226+
227+
const runIdLink = firstRow.locator("a[href*='/runs/']").first();
228+
229+
await expect(runIdLink).toBeVisible();
230+
await expect(runIdLink).not.toBeEmpty();
231+
232+
const stateBadge = firstRow.locator('[data-testid="state-badge"]');
233+
234+
await expect(stateBadge).toBeVisible();
235+
236+
const timeElements = firstRow.locator("time");
237+
238+
await expect(timeElements.first()).toBeVisible();
239+
}
240+
241+
public async verifyRunsExist(): Promise<void> {
242+
const runLinks = this.runsTable.locator("a[href*='/runs/']");
243+
244+
await expect(runLinks.first()).toBeVisible({ timeout: 30_000 });
245+
await expect(runLinks).not.toHaveCount(0);
246+
}
247+
248+
public async verifySearchResults(pattern: string): Promise<void> {
249+
await this.waitForRunsTableToLoad();
250+
251+
const rows = this.tableRows;
252+
253+
await expect(rows).not.toHaveCount(0);
254+
255+
const count = await rows.count();
256+
257+
for (let i = 0; i < Math.min(count, 5); i++) {
258+
const runIdLink = rows.nth(i).locator("a[href*='/runs/']").first();
259+
260+
await expect(runIdLink).toContainText(pattern, { ignoreCase: true });
261+
}
262+
}
263+
264+
public async waitForRunsTableToLoad(): Promise<void> {
265+
await expect(this.runsTable).toBeVisible({ timeout: 10_000 });
266+
267+
const dataLink = this.runsTable.locator("a[href*='/runs/']").first();
268+
const noDataMessage = this.page.getByText(/no.*dag.*runs.*found/i);
269+
270+
await expect(dataLink.or(noDataMessage)).toBeVisible({ timeout: 30_000 });
271+
}
272+
273+
private async ensureUrlParams(): Promise<void> {
274+
if (this.currentLimit === undefined || this.currentDagId === undefined) {
275+
return;
276+
}
277+
278+
const currentUrl = this.page.url();
279+
const url = new URL(currentUrl);
280+
const hasLimit = url.searchParams.has("limit");
281+
const hasOffset = url.searchParams.has("offset");
282+
283+
if (hasLimit && !hasOffset) {
284+
url.searchParams.set("offset", "0");
285+
await this.navigateTo(url.pathname + url.search);
286+
await this.waitForRunsTableToLoad();
287+
} else if (!hasLimit && !hasOffset) {
288+
url.searchParams.set("offset", "0");
289+
url.searchParams.set("limit", String(this.currentLimit));
290+
await this.navigateTo(url.pathname + url.search);
291+
await this.waitForRunsTableToLoad();
292+
}
293+
}
294+
}

0 commit comments

Comments
 (0)