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

Code before:
    const valueOption = this.page.locator(`[role="option"]:has-text("${filterValue}")`).first();

    await expect(valueOption).toBeVisible({ timeout: 5000 });
    await valueOption.click();

    await this.page.waitForLoadState("networkidle");
  }

  /**
   * Navigate to Task Instances page and wait for data to load
   */
  public async navigate(): Promise<void> {
    await this.navigateTo(TaskInstancesPage.taskInstancesUrl);
    await this.page.waitForURL(/.*task_instances/, { timeout: 15_000 });
    await this.taskInstancesTable.waitFor({ state: "visible", timeout: 10_000 });

    const dataLink = this.taskInstancesTable.locator("a[href*='/dags/']").first();
    const noDataMessage = this.page.locator('text="No Task Instances found"');

    await expect(dataLink.or(noDataMessage)).toBeVisible({ timeout: 30_000 });
  }

  /**
   * Verify pagination works correctly with offset and limit parameters
   */
  public async verifyPagination(limit: number): Promise<void> {
    await this.navigateTo(`${TaskInstancesPage.taskInstancesUrl}?offset=0&limit=${limit}`);
    await this.page.waitForURL(/.*task_instances/, { timeout: 10_000 });
    await this.taskInstancesTable.waitFor({ state: "visible", timeout: 10_000 });

    const rows = this.taskInstancesTable.locator('tbody tr:not(.no-data), div[role="row"]:not(:first-child)');
    const rowCount = await rows.count();

    expect(rowCount).toBeGreaterThan(0);

    const paginationInfo = this.page.locator("text=/\\d+\\s*-\\s*\\d+\\s*of\\s*\\d+/i");

    if ((await paginationInfo.count()) > 0) {
      await expect(paginationInfo.first()).toBeVisible();

      const paginationText = (await paginationInfo.first().textContent()) ?? "";

      expect(paginationText).toBeTruthy();

      const regex = /(\d+)\s*-\s*(\d+)\s*of\s*(\d+)/i;
      const match = regex.exec(paginationText);

      if (match?.[1] !== undefined && match[2] !== undefined && match[3] !== undefined) {
        const start = parseInt(match[1], 10);
        const end = parseInt(match[2], 10);
        const total = parseInt(match[3], 10);

Review comment (suggestion):
current implementation parses pagination text with regex which is complex and fragile. DAG Runs [PR](https://github.com/apache/airflow/issues/59309) uses a simpler approach

Code after:
    await expect
      .poll(() => this.getTaskInstanceIds(), { timeout: 10_000 })
      .not.toEqual(initialTaskInstanceIds);
    await this.waitForTaskInstanceList();
  }

  /**
   * Get all task instance identifiers from the current page
   */
  public async getTaskInstanceIds(): Promise<Array<string>> {
    await this.waitForTaskInstanceList();
    const taskLinks = this.taskInstancesTable.locator("a[href*='/dags/']");
    const texts = await taskLinks.allTextContents();

    return texts.map((text) => text.trim()).filter((text) => text !== "");
  }

  /**
   * Navigate to Task Instances page and wait for data to load
   */
  public async navigate(): Promise<void> {
    await this.navigateTo(TaskInstancesPage.taskInstancesUrl);
    await this.page.waitForURL(/.*task_instances/, { timeout: 15_000 });
    await this.taskInstancesTable.waitFor({ state: "visible", timeout: 10_000 });

    const dataLink = this.taskInstancesTable.locator("a[href*='/dags/']").first();
    const noDataMessage = this.page.locator('text="No Task Instances found"');

    await expect(dataLink.or(noDataMessage)).toBeVisible({ timeout: 30_000 });
  }

  /**
   * Verify state filtering via URL parameters
   */
  public async verifyStateFiltering(expectedState: string): Promise<void> {
    await this.navigateTo(`${TaskInstancesPage.taskInstancesUrl}?task_state=${expectedState.toLowerCase()}`);
    await this.page.waitForURL(/.*task_state=.*/, { timeout: 15_000 });
    await this.page.waitForLoadState("networkidle");

    const dataLink = this.taskInstancesTable.locator("a[href*='/dags/']").first();

    await expect(dataLink).toBeVisible({ timeout: 30_000 });
    await expect(this.taskInstancesTable).toBeVisible();

    const rowsAfterFilter = this.taskInstancesTable.locator(
      'tbody tr:not(.no-data), div[role="row"]:not(:first-child)',
    );
    const noDataMessage = this.page.locator("text=/No.*found/i, text=/No.*results/i, text=/Empty/i");
    const stateBadges = this.taskInstancesTable.locator('[class*="badge"], [class*="Badge"]');

    await expect(stateBadges.first().or(noDataMessage.first())).toBeVisible({ timeout: 30_000 });