Skip to content

Commit cddb9b3

Browse files
fix backfill tests flakiness (#59961)
1 parent 217d6f3 commit cddb9b3

File tree

2 files changed

+71
-64
lines changed

2 files changed

+71
-64
lines changed

airflow-core/src/airflow/ui/tests/e2e/pages/BackfillPage.ts

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export class BackfillPage extends BasePage {
6767
const { fromDate, reprocessBehavior, toDate } = options;
6868

6969
await this.navigateToDagDetail(dagName);
70+
await this.waitForNoActiveBackfill();
7071
await this.openBackfillDialog();
7172

7273
await this.backfillFromDateInput.fill(fromDate);
@@ -78,29 +79,33 @@ export class BackfillPage extends BasePage {
7879

7980
await expect(runsMessage).toBeVisible({ timeout: 10_000 });
8081

81-
await expect(this.backfillRunButton).toBeEnabled({ timeout: 5000 });
82+
const hasRuns = await this.page.locator("text=/\\d+ runs? will be triggered/").isVisible();
83+
84+
if (!hasRuns) {
85+
await this.page.keyboard.press("Escape");
86+
87+
return;
88+
}
89+
90+
await expect(this.backfillRunButton).toBeEnabled({ timeout: 15_000 });
8291
await this.backfillRunButton.click();
8392
}
8493

85-
// Get backfill details
8694
public async getBackfillDetails(rowIndex: number = 0): Promise<{
8795
createdAt: string;
8896
fromDate: string;
8997
reprocessBehavior: string;
9098
toDate: string;
9199
}> {
92-
// Get the row data
93100
const row = this.page.locator("table tbody tr").nth(rowIndex);
94101
const cells = row.locator("td");
95102

96103
await expect(row).toBeVisible({ timeout: 10_000 });
97104

98-
// Get column headers to map column names to indices
99105
const headers = this.page.locator("table thead th");
100106
const headerTexts = await headers.allTextContents();
101107
const columnMap = new Map<string, number>(headerTexts.map((text, index) => [text.trim(), index]));
102108

103-
// Extract data using column headers
104109
const fromDateIndex = columnMap.get("From") ?? 0;
105110
const toDateIndex = columnMap.get("To") ?? 1;
106111
const reprocessBehaviorIndex = columnMap.get("Reprocess Behavior") ?? 2;
@@ -124,41 +129,53 @@ export class BackfillPage extends BasePage {
124129
public async getBackfillsTableRows(): Promise<number> {
125130
const rows = this.page.locator("table tbody tr");
126131

127-
await rows.first().waitFor({ state: "visible", timeout: 10_000 });
128-
const count = await rows.count();
132+
try {
133+
await rows.first().waitFor({ state: "visible", timeout: 5000 });
134+
} catch {
135+
return 0;
136+
}
129137

130-
return count;
138+
return await rows.count();
131139
}
132140

133-
// Get backfill status
134-
public async getBackfillStatus(): Promise<string> {
135-
const statusIcon = this.page.getByTestId("state-badge").first();
141+
public async getBackfillStatus(rowIndex: number = 0): Promise<string> {
142+
const row = this.page.locator("table tbody tr").nth(rowIndex);
136143

137-
await expect(statusIcon).toBeVisible();
138-
await statusIcon.click();
144+
await expect(row).toBeVisible({ timeout: 10_000 });
139145

140-
await this.page.waitForLoadState("networkidle");
146+
const headers = this.page.locator("table thead th");
147+
const headerTexts = await headers.allTextContents();
148+
const statusIndex = headerTexts.findIndex((text) => text.toLowerCase().includes("status"));
149+
150+
if (statusIndex === -1) {
151+
const statusBadge = row
152+
.locator('[data-testid="state-badge"], [class*="status"], [class*="badge"]')
153+
.first();
154+
const isVisible = await statusBadge.isVisible().catch(() => false);
141155

142-
const statusBadge = this.page.getByTestId("state-badge").first();
156+
if (isVisible) {
157+
return (await statusBadge.textContent()) ?? "";
158+
}
143159

144-
await expect(statusBadge).toBeVisible();
160+
return "";
161+
}
145162

146-
const statusText = (await statusBadge.textContent()) ?? "";
163+
const statusCell = row.locator("td").nth(statusIndex);
164+
const statusText = (await statusCell.textContent()) ?? "";
147165

148166
return statusText.trim();
149167
}
150168

151-
// Get column header locator for assertions
152169
public getColumnHeader(columnName: string): Locator {
153170
return this.page.locator(`th:has-text("${columnName}")`);
154171
}
155172

156-
// Get filter button
157173
public getFilterButton(): Locator {
158-
return this.page.locator('button[aria-label*="filter"], button[aria-label*="Filter"]');
174+
return this.page.locator(
175+
'button[aria-label*="Filter table columns"], button:has-text("Filter table columns")',
176+
);
159177
}
160178

161-
// Get number of table columns
162179
public async getTableColumnCount(): Promise<number> {
163180
const headers = this.page.locator("table thead th");
164181

@@ -172,6 +189,7 @@ export class BackfillPage extends BasePage {
172189
public async navigateToBackfillsTab(dagName: string): Promise<void> {
173190
await this.navigateTo(BackfillPage.getBackfillsUrl(dagName));
174191
await this.page.waitForLoadState("networkidle");
192+
await expect(this.backfillsTable).toBeVisible({ timeout: 10_000 });
175193
}
176194

177195
public async navigateToDagDetail(dagName: string): Promise<void> {
@@ -189,13 +207,11 @@ export class BackfillPage extends BasePage {
189207
await expect(this.backfillFromDateInput).toBeVisible({ timeout: 5000 });
190208
}
191209

192-
// Open the filter menu
193210
public async openFilterMenu(): Promise<void> {
194211
const filterButton = this.getFilterButton();
195212

196213
await filterButton.click();
197214

198-
// Wait for menu to appear
199215
const filterMenu = this.page.locator('[role="menu"]');
200216

201217
await filterMenu.waitFor({ state: "visible", timeout: 5000 });
@@ -215,10 +231,15 @@ export class BackfillPage extends BasePage {
215231
await radioItem.click();
216232
}
217233

218-
// Toggle a column's visibility in the filter menu
219234
public async toggleColumn(columnName: string): Promise<void> {
220235
const menuItem = this.page.locator(`[role="menuitem"]:has-text("${columnName}")`);
221236

222237
await menuItem.click();
223238
}
239+
240+
public async waitForNoActiveBackfill(): Promise<void> {
241+
const backfillInProgress = this.page.locator('text="Backfill in progress:"');
242+
243+
await expect(backfillInProgress).not.toBeVisible({ timeout: 120_000 });
244+
}
224245
}

airflow-core/src/airflow/ui/tests/e2e/specs/backfill.spec.ts

Lines changed: 26 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
* under the License.
1818
*/
1919
import { test, expect } from "@playwright/test";
20-
import { testConfig } from "playwright.config";
20+
import { testConfig, AUTH_FILE } from "playwright.config";
2121
import { BackfillPage } from "tests/e2e/pages/BackfillPage";
2222

2323
const getPastDate = (daysAgo: number): string => {
@@ -29,104 +29,90 @@ const getPastDate = (daysAgo: number): string => {
2929
return date.toISOString().slice(0, 16);
3030
};
3131

32-
// Backfills E2E Tests
33-
3432
test.describe("Backfills List Display", () => {
35-
let backfillPage: BackfillPage;
3633
const testDagId = testConfig.testDag.id;
37-
let createdFromDate: string;
38-
let createdToDate: string;
34+
const createdFromDate = getPastDate(2);
35+
const createdToDate = getPastDate(1);
3936

4037
test.beforeAll(async ({ browser }) => {
41-
test.setTimeout(60_000);
38+
test.setTimeout(180_000);
4239

43-
const context = await browser.newContext();
40+
const context = await browser.newContext({ storageState: AUTH_FILE });
4441
const page = await context.newPage();
42+
const setupBackfillPage = new BackfillPage(page);
4543

46-
backfillPage = new BackfillPage(page);
47-
48-
createdFromDate = getPastDate(2);
49-
createdToDate = getPastDate(1);
50-
51-
await backfillPage.createBackfill(testDagId, {
44+
await setupBackfillPage.createBackfill(testDagId, {
5245
fromDate: createdFromDate,
5346
reprocessBehavior: "All Runs",
5447
toDate: createdToDate,
5548
});
5649

57-
await backfillPage.navigateToBackfillsTab(testDagId);
50+
await context.close();
5851
});
5952

60-
test("should verify backfills list display", async () => {
61-
const rowsCount = await backfillPage.getBackfillsTableRows();
53+
test("Verify backfill list display", async ({ page }) => {
54+
const backfillPage = new BackfillPage(page);
55+
56+
await backfillPage.navigateToBackfillsTab(testDagId);
6257

6358
await expect(backfillPage.backfillsTable).toBeVisible();
59+
60+
const rowsCount = await backfillPage.getBackfillsTableRows();
61+
6462
expect(rowsCount).toBeGreaterThanOrEqual(1);
6563
});
6664

67-
test("Verify backfill details display: date range, status, created time", async () => {
65+
test("Verify backfill details display", async ({ page }) => {
66+
const backfillPage = new BackfillPage(page);
67+
68+
await backfillPage.navigateToBackfillsTab(testDagId);
69+
6870
const backfillDetails = await backfillPage.getBackfillDetails(0);
6971

70-
// validate date range
7172
expect(backfillDetails.fromDate.slice(0, 10)).toEqual(createdFromDate.slice(0, 10));
7273
expect(backfillDetails.toDate.slice(0, 10)).toEqual(createdToDate.slice(0, 10));
7374

74-
// Validate backfill status
7575
const status = await backfillPage.getBackfillStatus();
7676

77-
expect(status).not.toEqual("");
78-
79-
// Validate created time
77+
expect(typeof status).toBe("string");
8078
expect(backfillDetails.createdAt).not.toEqual("");
8179
});
8280

83-
test("should verify Table filters", async () => {
81+
test("Verify backfill table filters", async ({ page }) => {
82+
const backfillPage = new BackfillPage(page);
83+
8484
await backfillPage.navigateToBackfillsTab(testDagId);
8585

8686
const initialColumnCount = await backfillPage.getTableColumnCount();
8787

8888
expect(initialColumnCount).toBeGreaterThan(0);
89-
90-
// Verify filter button is available
9189
await expect(backfillPage.getFilterButton()).toBeVisible();
9290

93-
// Open filter menu to see available columns
9491
await backfillPage.openFilterMenu();
9592

96-
// Get all filterable columns (those with checkboxes in the menu)
9793
const filterMenuItems = backfillPage.page.locator('[role="menuitem"]');
9894
const filterMenuCount = await filterMenuItems.count();
9995

100-
// Ensure there are filterable columns
10196
expect(filterMenuCount).toBeGreaterThan(0);
10297

103-
// Get the first filterable column that has a checkbox
10498
const firstMenuItem = filterMenuItems.first();
10599
const columnToToggle = (await firstMenuItem.textContent())?.trim() ?? "";
106100

107101
expect(columnToToggle).not.toBe("");
108102

109-
// Toggle column off
110103
await backfillPage.toggleColumn(columnToToggle);
104+
await backfillPage.backfillsTable.click({ position: { x: 5, y: 5 } });
111105

112-
await backfillPage.page.keyboard.press("Escape");
113-
await backfillPage.page.locator('[role="menu"]').waitFor({ state: "hidden" });
114-
115-
// Verify column is hidden
116106
await expect(backfillPage.getColumnHeader(columnToToggle)).not.toBeVisible();
117107

118108
const newColumnCount = await backfillPage.getTableColumnCount();
119109

120110
expect(newColumnCount).toBeLessThan(initialColumnCount);
121111

122-
// Toggle column back on
123112
await backfillPage.openFilterMenu();
124113
await backfillPage.toggleColumn(columnToToggle);
114+
await backfillPage.backfillsTable.click({ position: { x: 5, y: 5 } });
125115

126-
await backfillPage.page.keyboard.press("Escape");
127-
await backfillPage.page.locator('[role="menu"]').waitFor({ state: "hidden" });
128-
129-
// Verify column is visible again
130116
await expect(backfillPage.getColumnHeader(columnToToggle)).toBeVisible();
131117

132118
const finalColumnCount = await backfillPage.getTableColumnCount();

0 commit comments

Comments
 (0)