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
166 changes: 120 additions & 46 deletions e2e/tests/ui/pages/Toolbar.ts
Original file line number Diff line number Diff line change
@@ -1,81 +1,127 @@
import { expect, type Locator, type Page } from "@playwright/test";

export class Toolbar {
type TFilterValue = "string" | "dateRange" | "multiSelect" | "typeahead";

type TDateRange = { from: string; to: string };
type TMultiValue = string[];

type FilterValueTypeMap = {
string: string;
dateRange: TDateRange;
multiSelect: TMultiValue;
typeahead: TMultiValue;
};

type FilterValueType<TFilter extends Record<string, TFilterValue>> = {
[K in keyof TFilter]: FilterValueTypeMap[TFilter[K]];
};

function isStringFilter<K extends string, T extends Record<K, TFilterValue>>(
type: T[K],
value: unknown,
): value is string {
return type === "string";
}

function isDateRangeFilter<K extends string, T extends Record<K, TFilterValue>>(
type: T[K],
value: unknown,
): value is TDateRange {
return type === "dateRange";
}

function isMultiSelectFilter<
K extends string,
T extends Record<K, TFilterValue>,
>(type: T[K], value: unknown): value is TMultiValue {
return type === "multiSelect";
}

function isTypeaheadFilter<K extends string, T extends Record<K, TFilterValue>>(
type: T[K],
value: unknown,
): value is TMultiValue {
return type === "typeahead";
}

export class Toolbar<
TFilter extends Record<string, TFilterValue>,
TFilterName extends Extract<keyof TFilter, string>,
> {
private readonly _page: Page;
_toolbar: Locator;
private readonly _filters: TFilter;

private constructor(page: Page, toolbar: Locator) {
private constructor(page: Page, toolbar: Locator, filters: TFilter) {
this._page = page;
this._toolbar = toolbar;
this._filters = filters;
}

/**
* @param page
* @param toolbarAriaLabel the unique aria-label that corresponds to the DOM element that contains the Toolbar. E.g. <div aria-label="identifier"></div>
* @param filters a key value object that represents the filters available for the toolbar
* @returns a new instance of a Toolbar
*/
static async build(page: Page, toolbarAriaLabel: string) {
static async build<TFilter extends Record<string, TFilterValue>>(
page: Page,
toolbarAriaLabel: string,
filters: TFilter = {} as TFilter,
) {
const toolbar = page.locator(`[aria-label="${toolbarAriaLabel}"]`);
await expect(toolbar).toBeVisible();
return new Toolbar(page, toolbar);
return new Toolbar(page, toolbar, filters);
}

/**
* Selects the main filter to be applied
* @param filterName the name of the filter as rendered in the UI
*/
async selectFilter(filterName: string) {
await this._toolbar
.locator(".pf-m-toggle-group button.pf-v6-c-menu-toggle")
.click();
await this._page.getByRole("menuitem", { name: filterName }).click();
}

private async assertFilterHasLabels(
filterName: string,
filterValue: string | string[],
) {
await expect(
this._toolbar.locator(".pf-m-label-group", { hasText: filterName }),
).toBeVisible();

const labels = Array.isArray(filterValue) ? filterValue : [filterValue];
for (const label of labels) {
await expect(
this._toolbar.locator(".pf-m-label-group", { hasText: label }),
).toBeVisible();
async applyFilter(filters: Partial<FilterValueType<TFilter>>) {
for (const filterName of Object.keys(filters) as Array<TFilterName>) {
const filterValue = filters[filterName];
if (!filterValue) continue;

const filterType = this._filters[filterName];

await this.selectFilter(filterName);
if (isStringFilter(filterType, filterValue)) {
await this.applyTextFilter(filterName, filterValue);
} else if (isDateRangeFilter(filterType, filterValue)) {
await this.applyDateRangeFilter(filterName, filterValue);
} else if (isMultiSelectFilter(filterType, filterValue)) {
await this.applyMultiSelectFilter(filterName, filterValue);
} else if (isTypeaheadFilter(filterType, filterValue)) {
await this.applyTypeaheadFilter(filterName, filterValue);
}
}
}

async applyTextFilter(filterName: string, filterValue: string) {
await this.selectFilter(filterName);

private async applyTextFilter(filterName: TFilterName, filterValue: string) {
await this._toolbar.getByRole("textbox").fill(filterValue);
await this._page.keyboard.press("Enter");

await this.assertFilterHasLabels(filterName, filterValue);
}

async applyDateRangeFilter(
filterName: string,
fromDate: string,
toDate: string,
private async applyDateRangeFilter(
filterName: TFilterName,
dateRange: TDateRange,
) {
await this.selectFilter(filterName);

await this._toolbar
.locator("input[aria-label='Interval start']")
.fill(fromDate);
.fill(dateRange.from);
await this._toolbar
.locator("input[aria-label='Interval end']")
.fill(toDate);
.fill(dateRange.to);

await this.assertFilterHasLabels(filterName, [fromDate, toDate]);
await this.assertFilterHasLabels(filterName, [
dateRange.from,
dateRange.to,
]);
}

async applyMultiSelectFilter(filterName: string, selections: string[]) {
await this.selectFilter(filterName);

private async applyMultiSelectFilter(
filterName: TFilterName,
selections: string[],
) {
for (const option of selections) {
const inputText = this._toolbar.locator(
"input[aria-label='Type to filter']",
Expand All @@ -94,9 +140,10 @@ export class Toolbar {
await this.assertFilterHasLabels(filterName, selections);
}

async applyLabelsFilter(filterName: string, labels: string[]) {
await this.selectFilter(filterName);

private async applyTypeaheadFilter(
filterName: TFilterName,
labels: string[],
) {
for (const label of labels) {
await this._toolbar
.locator("input[aria-label='select-autocomplete-listbox']")
Expand All @@ -109,4 +156,31 @@ export class Toolbar {

await this.assertFilterHasLabels(filterName, labels);
}

/**
* Selects the main filter to be applied
* @param filterName the name of the filter as rendered in the UI
*/
private async selectFilter(filterName: TFilterName) {
await this._toolbar
.locator(".pf-m-toggle-group button.pf-v6-c-menu-toggle")
.click();
await this._page.getByRole("menuitem", { name: filterName }).click();
}

private async assertFilterHasLabels(
filterName: TFilterName,
filterValue: string | string[],
) {
await expect(
this._toolbar.locator(".pf-m-label-group", { hasText: filterName }),
).toBeVisible();

const labels = Array.isArray(filterValue) ? filterValue : [filterValue];
for (const label of labels) {
await expect(
this._toolbar.locator(".pf-m-label-group", { hasText: label }),
).toBeVisible();
}
}
}
2 changes: 1 addition & 1 deletion e2e/tests/ui/pages/advisory-details/AdvisoryDetailsPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class AdvisoryDetailsPage {
const toolbar = await listPage.getToolbar();
const table = await listPage.getTable();

await toolbar.applyTextFilter("Filter text", advisoryID);
await toolbar.applyFilter({ "Filter text": advisoryID });
await table.waitUntilDataIsLoaded();
await table.verifyColumnContainsText("ID", advisoryID);

Expand Down
6 changes: 5 additions & 1 deletion e2e/tests/ui/pages/advisory-list/AdvisoryListPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ export class AdvisoryListPage {
}

async getToolbar() {
return await Toolbar.build(this._page, "advisory-toolbar");
return await Toolbar.build(this._page, "advisory-toolbar", {
"Filter text": "string",
Revision: "dateRange",
Label: "typeahead",
});
}

async getTable() {
Expand Down
2 changes: 1 addition & 1 deletion e2e/tests/ui/pages/advisory-list/columns.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ test.describe("Columns validations", { tag: "@tier1" }, () => {
const table = await listPage.getTable();

// Full search
await toolbar.applyTextFilter("Filter text", "CVE-2024-26308");
await toolbar.applyFilter({ "Filter text": "CVE-2024-26308" });
await table.waitUntilDataIsLoaded();

// ID
Expand Down
8 changes: 5 additions & 3 deletions e2e/tests/ui/pages/advisory-list/filter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,19 @@ test.describe("Filter validations", { tag: "@tier1" }, () => {
const table = await listPage.getTable();

// Full search
await toolbar.applyTextFilter("Filter text", "CVE-2024-26308");
await toolbar.applyFilter({ "Filter text": "CVE-2024-26308" });
await table.waitUntilDataIsLoaded();
await table.verifyColumnContainsText("ID", "CVE-2024-26308");

// Date filter
await toolbar.applyDateRangeFilter("Revision", "03/26/2025", "03/28/2025");
await toolbar.applyFilter({
Revision: { from: "03/26/2025", to: "03/28/2025" },
});
await table.waitUntilDataIsLoaded();
await table.verifyColumnContainsText("ID", "CVE-2024-26308");

// Labels filter
await toolbar.applyLabelsFilter("Label", ["type=cve"]);
await toolbar.applyFilter({ Label: ["type=cve"] });
await table.waitUntilDataIsLoaded();
await table.verifyColumnContainsText("ID", "CVE-2024-26308");
});
Expand Down
2 changes: 1 addition & 1 deletion e2e/tests/ui/pages/package-details/PackageDetailsPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class PackageDetailsPage {
const toolbar = await listPage.getToolbar();
const table = await listPage.getTable();

await toolbar.applyTextFilter("Filter text", packageDetail.Name);
await toolbar.applyFilter({ "Filter text": packageDetail.Name });
await table.waitUntilDataIsLoaded();
// Get rows matching the package name
const matchingRows = table.getRowsByCellValue(packageDetail);
Expand Down
7 changes: 6 additions & 1 deletion e2e/tests/ui/pages/package-list/PackageListPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ export class PackageListPage {
}

async getToolbar() {
return await Toolbar.build(this._page, "package-toolbar");
return await Toolbar.build(this._page, "package-toolbar", {
"Filter text": "string",
Type: "multiSelect",
Architecture: "multiSelect",
License: "multiSelect",
});
}

async getTable() {
Expand Down
2 changes: 1 addition & 1 deletion e2e/tests/ui/pages/package-list/columns.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ test.describe("Columns validations", { tag: "@tier1" }, () => {
const table = await listPage.getTable();

// Full search
await toolbar.applyTextFilter("Filter text", "keycloak-core");
await toolbar.applyFilter({ "Filter text": "keycloak-core" });
await table.waitUntilDataIsLoaded();
const tableRow = table.getRowsByCellValue({
Name: "keycloak-core",
Expand Down
6 changes: 3 additions & 3 deletions e2e/tests/ui/pages/package-list/filter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ test.describe("Filter validations", { tag: "@tier1" }, () => {
const table = await listPage.getTable();

// Full search
await toolbar.applyTextFilter("Filter text", "keycloak-core");
await toolbar.applyFilter({ "Filter text": "keycloak-core" });
await table.waitUntilDataIsLoaded();
let tableRow = table.getRowsByCellValue({
Name: "keycloak-core",
Expand All @@ -26,7 +26,7 @@ test.describe("Filter validations", { tag: "@tier1" }, () => {
await expect(await tableRow.count()).toBeGreaterThan(0);

// Type filter
await toolbar.applyMultiSelectFilter("Type", ["Maven", "RPM"]);
await toolbar.applyFilter({ Type: ["Maven", "RPM"] });
await table.waitUntilDataIsLoaded();
tableRow = table.getRowsByCellValue({
Name: "keycloak-core",
Expand All @@ -35,7 +35,7 @@ test.describe("Filter validations", { tag: "@tier1" }, () => {
await expect(await tableRow.count()).toBeGreaterThan(0);

// Architecture
await toolbar.applyMultiSelectFilter("Architecture", ["S390", "No Arch"]);
await toolbar.applyFilter({ Architecture: ["S390", "No Arch"] });
await table.waitUntilDataIsLoaded();
await table.verifyTableHasNoData();
});
Expand Down
2 changes: 1 addition & 1 deletion e2e/tests/ui/pages/sbom-details/SbomDetailsPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export class SbomDetailsPage {
const toolbar = await listPage.getToolbar();
const table = await listPage.getTable();

await toolbar.applyTextFilter("Filter text", sbomName);
await toolbar.applyFilter({ "Filter text": sbomName });
await table.waitUntilDataIsLoaded();
await table.verifyColumnContainsText("Name", sbomName);

Expand Down
5 changes: 4 additions & 1 deletion e2e/tests/ui/pages/sbom-details/packages/PackagesTab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ export class PackagesTab {
}

async getToolbar() {
return await Toolbar.build(this._page, "Package toolbar");
return await Toolbar.build(this._page, "Package toolbar", {
"Filter text": "string",
License: "multiSelect",
});
}

async getTable() {
Expand Down
2 changes: 1 addition & 1 deletion e2e/tests/ui/pages/sbom-details/packages/columns.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ test.describe("Columns validations", { tag: "@tier1" }, () => {
const table = await packageTab.getTable();

// Full search
await toolbar.applyTextFilter("Filter text", "commons-compress");
await toolbar.applyFilter({ "Filter text": "commons-compress" });
await table.waitUntilDataIsLoaded();
await table.verifyColumnContainsText("Name", "commons-compress");

Expand Down
7 changes: 2 additions & 5 deletions e2e/tests/ui/pages/sbom-details/packages/filter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,12 @@ test.describe("Filter validations", { tag: "@tier1" }, () => {
const table = await packageTab.getTable();

// Full search
await toolbar.applyTextFilter("Filter text", "commons-compress");
await toolbar.applyFilter({ "Filter text": "commons-compress" });
await table.waitUntilDataIsLoaded();
await table.verifyColumnContainsText("Name", "commons-compress");

// Labels filter
await toolbar.applyMultiSelectFilter("License", [
"Apache-2.0",
"NOASSERTION",
]);
await toolbar.applyFilter({ License: ["Apache-2.0", "NOASSERTION"] });
await table.waitUntilDataIsLoaded();
await table.verifyColumnContainsText("Name", "commons-compress");
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ test.describe.skip("Filter validations", { tag: "@tier1" }, () => {
const table = await vulnerabilityTab.getTable();

// Full search
await toolbar.applyTextFilter("Filter text", "CVE-2023-4853");
await toolbar.applyFilter({ "Filter text": "CVE-2023-4853" });
await table.waitUntilDataIsLoaded();
await table.verifyColumnContainsText("Id", "CVE-2023-4853");

// Labels filter
await toolbar.applyMultiSelectFilter("Severity", ["High"]);
await toolbar.applyFilter({ Severity: ["High"] });
await table.waitUntilDataIsLoaded();
await table.verifyColumnContainsText("Id", "CVE-2023-4853");
});
Expand Down
Loading