Repository: apache/airflow
File: airflow-core/src/airflow/ui/tests/e2e/pages/BackfillPage.ts (TypeScript)

Code before:
    const filterMenu = this.page.locator('[role="menu"]');

    await filterMenu.waitFor({ state: "visible", timeout: 5000 });
  }

  public async selectReprocessBehavior(behavior: ReprocessBehavior): Promise<void> {
    const behaviorLabels: Record<ReprocessBehavior, string> = {
      "All Runs": "All Runs",
      "Missing and Errored Runs": "Missing and Errored Runs",
      "Missing Runs": "Missing Runs",
    };

    const label = behaviorLabels[behavior];
    const radioItem = this.page.locator(`label:has-text("${label}")`).first();

    await radioItem.waitFor({ state: "visible", timeout: 5000 });
    await radioItem.click();
  }

  public async toggleColumn(columnName: string): Promise<void> {
    const menuItem = this.page.locator(`[role="menuitem"]:has-text("${columnName}")`);

    await menuItem.click();
  }

  public async waitForBackfillCompletion(timeout: number = 30_000): Promise<void> {
    await expect(this.backfillBanner).toBeHidden({ timeout });
  }

  public async waitForNoActiveBackfill(): Promise<void> {
    const backfillInProgress = this.page.locator('text="Backfill in progress:"');

    await expect(backfillInProgress).not.toBeVisible({ timeout: 300_000 });
  }

  public async waitForTableDataLoaded(): Promise<void> {
    const firstCell = this.page.locator("table tbody tr:first-child td:first-child");

    await expect(firstCell).toBeVisible({ timeout: 30_000 });
    await expect(async () => {
      const text = await firstCell.textContent();

      if (text === null || text.trim() === "") {
        throw new Error("Table data still loading");
      }
    }).toPass({ timeout: 30_000 });
  }
}


Review comment (refactor):
You can rebase and remove this method. We already have `waitForNoActiveBackfill` in main

Code after:
  public getColumnHeader(columnName: string): Locator {
    return this.page.locator(`th:has-text("${columnName}")`);
  }

  public async getColumnIndexMap(): Promise<Map<string, number>> {
    const headers = this.page.locator("table thead th");

    await headers.first().waitFor({ state: "visible", timeout: 10_000 });
    const headerTexts = await headers.allTextContents();

    return new Map(headerTexts.map((text, index) => [text.trim(), index]));
  }

  public getFilterButton(): Locator {
    return this.page.locator(
      'button[aria-label*="Filter table columns"], button:has-text("Filter table columns")',
    );
  }

  public async getTableColumnCount(): Promise<number> {
    const headers = this.page.locator("table thead th");

    return await headers.count();
  }

  public async isBackfillPaused(): Promise<boolean> {
    await expect(this.pauseButton).toBeVisible({ timeout: 10_000 });
    const ariaLabel = await this.pauseButton.getAttribute("aria-label");

    return Boolean(ariaLabel?.toLowerCase().includes("unpause"));
  }

  public async navigateToBackfillsTab(dagName: string): Promise<void> {
    await this.navigateTo(BackfillPage.getBackfillsUrl(dagName));
    await expect(this.backfillsTable).toBeVisible({ timeout: 20_000 });
  }

  public async navigateToDagDetail(dagName: string): Promise<void> {
    await this.navigateTo(BackfillPage.getDagDetailUrl(dagName));
    await expect(this.triggerButton).toBeVisible({ timeout: 30_000 });
  }

  public async openBackfillDialog(): Promise<void> {
    await expect(this.triggerButton).toBeVisible({ timeout: 20_000 });
    await this.triggerButton.click();

    await expect(this.backfillModeRadio).toBeVisible({ timeout: 20_000 });
    await this.backfillModeRadio.click();

    await expect(this.backfillFromDateInput).toBeVisible({ timeout: 20_000 });
  }