Skip to content
Open
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
117 changes: 117 additions & 0 deletions airflow-core/src/airflow/ui/tests/e2e/pages/ProvidersPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { expect, type Locator, type Page } from "@playwright/test";

import { BasePage } from "./BasePage";

export class ProvidersPage extends BasePage {
public readonly heading: Locator;
public readonly paginationNextButton: Locator;
public readonly paginationPrevButton: Locator;
public readonly rows: Locator;
public readonly table: Locator;

public constructor(page: Page) {
super(page);

this.heading = page.getByRole("heading", { name: /^providers$/i });
this.table = page.getByTestId("table-list");
this.rows = this.table.locator("tbody tr").filter({
has: page.locator("td"),
});
this.paginationNextButton = page.locator('[data-testid="next"]');
this.paginationPrevButton = page.locator('[data-testid="prev"]');
}

/**
* Click next page button
*/
public async clickNextPage(): Promise<void> {
const initialProviderNames = await this.providerNames();

await this.paginationNextButton.click();

await expect.poll(() => this.providerNames(), { timeout: 10_000 }).not.toEqual(initialProviderNames);
await this.waitForTableData();
}

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

await this.paginationPrevButton.click();

await expect.poll(() => this.providerNames(), { timeout: 10_000 }).not.toEqual(initialProviderNames);
await this.waitForTableData();
}

public async getRowCount(): Promise<number> {
return this.rows.count();
}

public async getRowDetails(index: number) {
const row = this.rows.nth(index);
const cells = row.locator("td");

const pkg = await cells.nth(0).locator("a").textContent();
const ver = await cells.nth(1).textContent();
const desc = await cells.nth(2).textContent();

return {
description: (desc ?? "").trim(),
packageName: (pkg ?? "").trim(),
version: (ver ?? "").trim(),
};
}

public async navigate(): Promise<void> {
await this.navigateTo("/providers");
}

public async providerNames(): Promise<Array<string>> {
return this.rows.locator("td a").allTextContents();
}

public async waitForLoad(): Promise<void> {
await this.table.waitFor({ state: "visible", timeout: 30_000 });
await this.waitForTableData();
}

private async waitForTableData(): Promise<void> {
// Wait for actual data links to appear (not skeleton loaders)
await this.page.waitForFunction(
() => {
const table = document.querySelector('[data-testid="table-list"]');

if (!table) {
return false;
}

// Check for actual links in tbody (real data, not skeleton)
const links = table.querySelectorAll("tbody tr td a");

return links.length > 0;
},
undefined,
{ timeout: 30_000 },
);
}
}
121 changes: 121 additions & 0 deletions airflow-core/src/airflow/ui/tests/e2e/specs/providers.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*!
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { test, expect } from "@playwright/test";

import { ProvidersPage } from "../pages/ProvidersPage";

test.describe("Providers Page", () => {
let providers: ProvidersPage;

test.beforeEach(async ({ page }) => {
providers = new ProvidersPage(page);
await providers.navigate();
await providers.waitForLoad();
});

test("verify providers page heading", async () => {
await expect(providers.heading).toBeVisible();
});

test("Verify Providers page is accessible via Admin menu", async ({ page }) => {
await page.goto("/");

await page.getByRole("button", { name: /^admin$/i }).click();

// Click Providers
const providersItem = page.getByRole("menuitem", { name: /^providers$/i });

await expect(providersItem).toBeVisible();
await providersItem.click();

await providers.waitForLoad();
// Assert Providers page loaded
await expect(providers.heading).toBeVisible();
expect(await providers.getRowCount()).toBeGreaterThan(0);
});

test("Verify the providers list displays", async () => {
await expect(providers.table).toBeVisible();
});

test("Verify package name, version, and description are not blank", async () => {
const count = await providers.getRowCount();

expect(count).toBeGreaterThan(0);

for (let i = 0; i < 2; i++) {
const { description, packageName, version } = await providers.getRowDetails(i);

expect(packageName).not.toEqual("");
expect(version).not.toEqual("");
expect(description).not.toEqual("");
}
});

test("verify providers pagination", async () => {
const limit = 5;

await providers.navigateTo(`/providers?offset=0&limit=${limit}`);
await providers.waitForLoad();

const rows = await providers.getRowCount();

expect(rows).toBeGreaterThan(0);

const initialProviderNames = await providers.providerNames();

expect(initialProviderNames.length).toBeGreaterThan(0);

await expect(providers.paginationNextButton).toBeVisible();
await expect(providers.paginationPrevButton).toBeVisible();

await providers.paginationNextButton.click();
await providers.waitForLoad();

await providers.page.waitForURL((url) => {
const u = new URL(url);
const offset = u.searchParams.get("offset");

return offset !== null && offset !== "0";
});

const rowsPage2 = await providers.getRowCount();

expect(rowsPage2).toBeGreaterThan(0);

const ProviderNamesAfterNext = await providers.providerNames();

expect(ProviderNamesAfterNext.length).toBeGreaterThan(0);
expect(ProviderNamesAfterNext).not.toEqual(initialProviderNames);

await providers.paginationPrevButton.click();
await providers.waitForLoad();

await providers.page.waitForURL((url) => {
const u = new URL(url);
const offset = u.searchParams.get("offset");

return offset === "0" || offset === null;
});

const rowsBack = await providers.getRowCount();

expect(rowsBack).toBeGreaterThan(0);
});
});
Loading