From 93c811d2efc46810d171237af8f216849389d0ee Mon Sep 17 00:00:00 2001 From: Carlos Feria <2582866+carlosthe19916@users.noreply.github.com> Date: Mon, 3 Nov 2025 11:12:20 -0500 Subject: [PATCH 1/5] feat: add typesafe to e2e tests Signed-off-by: Carlos Feria <2582866+carlosthe19916@users.noreply.github.com> --- e2e/tests/ui/pages/Toolbar.ts | 28 +++++++++++++------ .../ui/pages/package-list/PackageListPage.ts | 4 ++- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/e2e/tests/ui/pages/Toolbar.ts b/e2e/tests/ui/pages/Toolbar.ts index 44e3b40a4..36722595c 100644 --- a/e2e/tests/ui/pages/Toolbar.ts +++ b/e2e/tests/ui/pages/Toolbar.ts @@ -1,10 +1,17 @@ import { expect, type Locator, type Page } from "@playwright/test"; -export class Toolbar { +type TFilterValue = string | number | Date; + +export class Toolbar< + TFilter extends Record, +> { private readonly _page: Page; _toolbar: Locator; - private constructor(page: Page, toolbar: Locator) { + private constructor( + page: Page, + toolbar: Locator, + ) { this._page = page; this._toolbar = toolbar; } @@ -14,17 +21,20 @@ export class Toolbar { * @param toolbarAriaLabel the unique aria-label that corresponds to the DOM element that contains the Toolbar. E.g.
* @returns a new instance of a Toolbar */ - static async build(page: Page, toolbarAriaLabel: string) { + static async build>( + page: Page, + toolbarAriaLabel: string, + ) { const toolbar = page.locator(`[aria-label="${toolbarAriaLabel}"]`); await expect(toolbar).toBeVisible(); - return new Toolbar(page, toolbar); + return new Toolbar(page, toolbar); } /** * Selects the main filter to be applied * @param filterName the name of the filter as rendered in the UI */ - async selectFilter(filterName: string) { + async selectFilter>(filterName: TFilterName) { await this._toolbar .locator(".pf-m-toggle-group button.pf-v6-c-menu-toggle") .click(); @@ -47,7 +57,7 @@ export class Toolbar { } } - async applyTextFilter(filterName: string, filterValue: string) { + async applyTextFilter(filterName: TFilterName, filterValue: string) { await this.selectFilter(filterName); await this._toolbar.getByRole("textbox").fill(filterValue); @@ -57,7 +67,7 @@ export class Toolbar { } async applyDateRangeFilter( - filterName: string, + filterName: TFilterName, fromDate: string, toDate: string, ) { @@ -73,7 +83,7 @@ export class Toolbar { await this.assertFilterHasLabels(filterName, [fromDate, toDate]); } - async applyMultiSelectFilter(filterName: string, selections: string[]) { + async applyMultiSelectFilter(filterName: TFilterName, selections: string[]) { await this.selectFilter(filterName); for (const option of selections) { @@ -94,7 +104,7 @@ export class Toolbar { await this.assertFilterHasLabels(filterName, selections); } - async applyLabelsFilter(filterName: string, labels: string[]) { + async applyLabelsFilter(filterName: TFilterName, labels: string[]) { await this.selectFilter(filterName); for (const label of labels) { diff --git a/e2e/tests/ui/pages/package-list/PackageListPage.ts b/e2e/tests/ui/pages/package-list/PackageListPage.ts index 705ca07e9..5b4510b0a 100644 --- a/e2e/tests/ui/pages/package-list/PackageListPage.ts +++ b/e2e/tests/ui/pages/package-list/PackageListPage.ts @@ -19,7 +19,9 @@ export class PackageListPage { } async getToolbar() { - return await Toolbar.build(this._page, "package-toolbar"); + const t = await Toolbar.build<{name: string, surname: number}>(this._page, "package-toolbar"); + t.someMethod("name"); + return await Toolbar.build<{name: string, surname: number}>(this._page, "package-toolbar"); } async getTable() { From 6c4260de3db41bf90274b917f2914f7be19efebe Mon Sep 17 00:00:00 2001 From: Carlos Feria <2582866+carlosthe19916@users.noreply.github.com> Date: Tue, 4 Nov 2025 20:18:38 +0000 Subject: [PATCH 2/5] fix: e2e tests make toolbar typesafe --- e2e/tests/common/utils.ts | 3 + e2e/tests/ui/pages/Toolbar.ts | 165 ++++++++++++------ .../advisory-details/AdvisoryDetailsPage.ts | 2 +- .../pages/advisory-list/AdvisoryListPage.ts | 6 +- .../ui/pages/advisory-list/columns.spec.ts | 2 +- .../ui/pages/advisory-list/filter.spec.ts | 8 +- .../package-details/PackageDetailsPage.ts | 2 +- .../ui/pages/package-list/PackageListPage.ts | 9 +- .../ui/pages/package-list/columns.spec.ts | 2 +- .../ui/pages/package-list/filter.spec.ts | 6 +- .../ui/pages/sbom-details/SbomDetailsPage.ts | 2 +- .../sbom-details/packages/PackagesTab.ts | 5 +- .../sbom-details/packages/columns.spec.ts | 2 +- .../sbom-details/packages/filter.spec.ts | 7 +- .../vulnerabilities/filter.spec.ts | 4 +- e2e/tests/ui/pages/sbom-list/SbomListPage.ts | 7 +- e2e/tests/ui/pages/sbom-list/columns.spec.ts | 2 +- e2e/tests/ui/pages/sbom-list/filter.spec.ts | 12 +- .../VulnerabilityDetailsPage.ts | 2 +- .../advisories/AdvisoriesTab.ts | 4 +- .../VulnerabilityListPage.ts | 6 +- .../pages/vulnerability-list/columns.spec.ts | 2 +- .../pages/vulnerability-list/filter.spec.ts | 12 +- 23 files changed, 176 insertions(+), 96 deletions(-) create mode 100644 e2e/tests/common/utils.ts diff --git a/e2e/tests/common/utils.ts b/e2e/tests/common/utils.ts new file mode 100644 index 000000000..9058c870a --- /dev/null +++ b/e2e/tests/common/utils.ts @@ -0,0 +1,3 @@ +export const typedEntries = (obj: T) => { + return Object.entries(obj) as [Extract, T[keyof T]][]; +}; diff --git a/e2e/tests/ui/pages/Toolbar.ts b/e2e/tests/ui/pages/Toolbar.ts index 36722595c..dc15e7e7d 100644 --- a/e2e/tests/ui/pages/Toolbar.ts +++ b/e2e/tests/ui/pages/Toolbar.ts @@ -1,91 +1,124 @@ import { expect, type Locator, type Page } from "@playwright/test"; -type TFilterValue = string | number | Date; +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> = { + [K in keyof TFilter]: FilterValueTypeMap[TFilter[K]]; +}; + +function isStringFilter>( + type: T[K], + value: unknown, +): value is string { + return type === "string"; +} + +function isDateRangeFilter>( + type: T[K], + value: unknown, +): value is TDateRange { + return type === "dateRange"; +} + +function isMultiSelectFilter< + K extends string, + T extends Record, +>(type: T[K], value: unknown): value is TMultiValue { + return type === "multiSelect"; +} + +function isTypeaheadFilter>( + type: T[K], + value: unknown, +): value is TMultiValue { + return type === "typeahead"; +} -export class Toolbar< - TFilter extends Record, -> { +export class Toolbar> { 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.
+ * @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, + filters: TFilter = {} as TFilter, ) { const toolbar = page.locator(`[aria-label="${toolbarAriaLabel}"]`); await expect(toolbar).toBeVisible(); - return new Toolbar(page, toolbar); - } - - /** - * Selects the main filter to be applied - * @param filterName the name of the filter as rendered in the UI - */ - 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(); + return new Toolbar(page, toolbar, filters); } - 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>) { + for (const filterName of Object.keys(filters) as Array< + Extract + >) { + const filterValue = filters[filterName]; + 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: TFilterName, filterValue: string) { - await this.selectFilter(filterName); - + private async applyTextFilter< + TFilterName extends Extract, + >(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: TFilterName, - fromDate: string, - toDate: string, - ) { - await this.selectFilter(filterName); - + private async applyDateRangeFilter< + TFilterName extends Extract, + >(filterName: TFilterName, dateRange: TDateRange) { 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: TFilterName, selections: string[]) { - await this.selectFilter(filterName); - + private async applyMultiSelectFilter< + TFilterName extends Extract, + >(filterName: TFilterName, selections: string[]) { for (const option of selections) { const inputText = this._toolbar.locator( "input[aria-label='Type to filter']", @@ -104,9 +137,9 @@ export class Toolbar< await this.assertFilterHasLabels(filterName, selections); } - async applyLabelsFilter(filterName: TFilterName, labels: string[]) { - await this.selectFilter(filterName); - + private async applyTypeaheadFilter< + TFilterName extends Extract, + >(filterName: TFilterName, labels: string[]) { for (const label of labels) { await this._toolbar .locator("input[aria-label='select-autocomplete-listbox']") @@ -119,4 +152,32 @@ 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< + TFilterName extends Extract, + >(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< + TFilterName extends Extract, + >(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(); + } + } } diff --git a/e2e/tests/ui/pages/advisory-details/AdvisoryDetailsPage.ts b/e2e/tests/ui/pages/advisory-details/AdvisoryDetailsPage.ts index 771c6c0d4..e1ea3da38 100644 --- a/e2e/tests/ui/pages/advisory-details/AdvisoryDetailsPage.ts +++ b/e2e/tests/ui/pages/advisory-details/AdvisoryDetailsPage.ts @@ -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); diff --git a/e2e/tests/ui/pages/advisory-list/AdvisoryListPage.ts b/e2e/tests/ui/pages/advisory-list/AdvisoryListPage.ts index 54d5cf30a..e2c35df01 100644 --- a/e2e/tests/ui/pages/advisory-list/AdvisoryListPage.ts +++ b/e2e/tests/ui/pages/advisory-list/AdvisoryListPage.ts @@ -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() { diff --git a/e2e/tests/ui/pages/advisory-list/columns.spec.ts b/e2e/tests/ui/pages/advisory-list/columns.spec.ts index 7d1ef125f..0a7ce74cb 100644 --- a/e2e/tests/ui/pages/advisory-list/columns.spec.ts +++ b/e2e/tests/ui/pages/advisory-list/columns.spec.ts @@ -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 diff --git a/e2e/tests/ui/pages/advisory-list/filter.spec.ts b/e2e/tests/ui/pages/advisory-list/filter.spec.ts index 1351a884e..368ba184d 100644 --- a/e2e/tests/ui/pages/advisory-list/filter.spec.ts +++ b/e2e/tests/ui/pages/advisory-list/filter.spec.ts @@ -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"); }); diff --git a/e2e/tests/ui/pages/package-details/PackageDetailsPage.ts b/e2e/tests/ui/pages/package-details/PackageDetailsPage.ts index 130207c1d..6b4e487f6 100644 --- a/e2e/tests/ui/pages/package-details/PackageDetailsPage.ts +++ b/e2e/tests/ui/pages/package-details/PackageDetailsPage.ts @@ -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); diff --git a/e2e/tests/ui/pages/package-list/PackageListPage.ts b/e2e/tests/ui/pages/package-list/PackageListPage.ts index 5b4510b0a..293f2584b 100644 --- a/e2e/tests/ui/pages/package-list/PackageListPage.ts +++ b/e2e/tests/ui/pages/package-list/PackageListPage.ts @@ -19,9 +19,12 @@ export class PackageListPage { } async getToolbar() { - const t = await Toolbar.build<{name: string, surname: number}>(this._page, "package-toolbar"); - t.someMethod("name"); - return await Toolbar.build<{name: string, surname: number}>(this._page, "package-toolbar"); + return await Toolbar.build(this._page, "package-toolbar", { + "Filter text": "string", + Type: "multiSelect", + Architecture: "multiSelect", + License: "multiSelect", + }); } async getTable() { diff --git a/e2e/tests/ui/pages/package-list/columns.spec.ts b/e2e/tests/ui/pages/package-list/columns.spec.ts index a0928fca6..0ade4e003 100644 --- a/e2e/tests/ui/pages/package-list/columns.spec.ts +++ b/e2e/tests/ui/pages/package-list/columns.spec.ts @@ -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", diff --git a/e2e/tests/ui/pages/package-list/filter.spec.ts b/e2e/tests/ui/pages/package-list/filter.spec.ts index 6e7e9d4c2..9ba2c6e69 100644 --- a/e2e/tests/ui/pages/package-list/filter.spec.ts +++ b/e2e/tests/ui/pages/package-list/filter.spec.ts @@ -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", @@ -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", @@ -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(); }); diff --git a/e2e/tests/ui/pages/sbom-details/SbomDetailsPage.ts b/e2e/tests/ui/pages/sbom-details/SbomDetailsPage.ts index 029cc5769..5043ba874 100644 --- a/e2e/tests/ui/pages/sbom-details/SbomDetailsPage.ts +++ b/e2e/tests/ui/pages/sbom-details/SbomDetailsPage.ts @@ -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); diff --git a/e2e/tests/ui/pages/sbom-details/packages/PackagesTab.ts b/e2e/tests/ui/pages/sbom-details/packages/PackagesTab.ts index 26198a7bd..f2ddd1cc1 100644 --- a/e2e/tests/ui/pages/sbom-details/packages/PackagesTab.ts +++ b/e2e/tests/ui/pages/sbom-details/packages/PackagesTab.ts @@ -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() { diff --git a/e2e/tests/ui/pages/sbom-details/packages/columns.spec.ts b/e2e/tests/ui/pages/sbom-details/packages/columns.spec.ts index e2b21f3bb..215a3c043 100644 --- a/e2e/tests/ui/pages/sbom-details/packages/columns.spec.ts +++ b/e2e/tests/ui/pages/sbom-details/packages/columns.spec.ts @@ -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"); diff --git a/e2e/tests/ui/pages/sbom-details/packages/filter.spec.ts b/e2e/tests/ui/pages/sbom-details/packages/filter.spec.ts index f26ee5758..6695d9429 100644 --- a/e2e/tests/ui/pages/sbom-details/packages/filter.spec.ts +++ b/e2e/tests/ui/pages/sbom-details/packages/filter.spec.ts @@ -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"); }); diff --git a/e2e/tests/ui/pages/sbom-details/vulnerabilities/filter.spec.ts b/e2e/tests/ui/pages/sbom-details/vulnerabilities/filter.spec.ts index e785a06f5..c6e41fd1e 100644 --- a/e2e/tests/ui/pages/sbom-details/vulnerabilities/filter.spec.ts +++ b/e2e/tests/ui/pages/sbom-details/vulnerabilities/filter.spec.ts @@ -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"); }); diff --git a/e2e/tests/ui/pages/sbom-list/SbomListPage.ts b/e2e/tests/ui/pages/sbom-list/SbomListPage.ts index fd9fd6a8c..4886d4d42 100644 --- a/e2e/tests/ui/pages/sbom-list/SbomListPage.ts +++ b/e2e/tests/ui/pages/sbom-list/SbomListPage.ts @@ -19,7 +19,12 @@ export class SbomListPage { } async getToolbar() { - return await Toolbar.build(this._page, "sbom-toolbar"); + return await Toolbar.build(this._page, "sbom-toolbar", { + "Filter text": "string", + "Created on": "dateRange", + Label: "typeahead", + License: "multiSelect", + }); } async getTable() { diff --git a/e2e/tests/ui/pages/sbom-list/columns.spec.ts b/e2e/tests/ui/pages/sbom-list/columns.spec.ts index b32174205..0686c3968 100644 --- a/e2e/tests/ui/pages/sbom-list/columns.spec.ts +++ b/e2e/tests/ui/pages/sbom-list/columns.spec.ts @@ -18,7 +18,7 @@ test.describe("Columns validations", { tag: "@tier1" }, () => { const table = await listPage.getTable(); // Full search - await toolbar.applyTextFilter("Filter text", "quarkus-bom"); + await toolbar.applyFilter({ "Filter text": "quarkus-bom" }); await table.waitUntilDataIsLoaded(); await table.verifyColumnContainsText("Name", "quarkus-bom"); diff --git a/e2e/tests/ui/pages/sbom-list/filter.spec.ts b/e2e/tests/ui/pages/sbom-list/filter.spec.ts index 7fa0c4564..6e3fbf5cc 100644 --- a/e2e/tests/ui/pages/sbom-list/filter.spec.ts +++ b/e2e/tests/ui/pages/sbom-list/filter.spec.ts @@ -16,21 +16,19 @@ test.describe("Filter validations", { tag: "@tier1" }, () => { const table = await listPage.getTable(); // Full search - await toolbar.applyTextFilter("Filter text", "quarkus"); + await toolbar.applyFilter({ "Filter text": "quarkus" }); await table.waitUntilDataIsLoaded(); await table.verifyColumnContainsText("Name", "quarkus-bom"); // Date filter - await toolbar.applyDateRangeFilter( - "Created on", - "11/21/2023", - "11/23/2023", - ); + await toolbar.applyFilter({ + "Created on": { from: "11/21/2023", to: "11/23/2023" }, + }); await table.waitUntilDataIsLoaded(); await table.verifyColumnContainsText("Name", "quarkus-bom"); // Labels filter - await toolbar.applyLabelsFilter("Label", ["type=spdx"]); + await toolbar.applyFilter({ Label: ["type=spdx"] }); await table.waitUntilDataIsLoaded(); await table.verifyColumnContainsText("Name", "quarkus-bom"); }); diff --git a/e2e/tests/ui/pages/vulnerability-details/VulnerabilityDetailsPage.ts b/e2e/tests/ui/pages/vulnerability-details/VulnerabilityDetailsPage.ts index dd30ec034..52b11d1a5 100644 --- a/e2e/tests/ui/pages/vulnerability-details/VulnerabilityDetailsPage.ts +++ b/e2e/tests/ui/pages/vulnerability-details/VulnerabilityDetailsPage.ts @@ -18,7 +18,7 @@ export class VulnerabilityDetailsPage { const toolbar = await listPage.getToolbar(); const table = await listPage.getTable(); - await toolbar.applyTextFilter("Filter text", vulnerabilityID); + await toolbar.applyFilter({ "Filter text": vulnerabilityID }); await table.waitUntilDataIsLoaded(); await table.verifyColumnContainsText("ID", vulnerabilityID); diff --git a/e2e/tests/ui/pages/vulnerability-details/advisories/AdvisoriesTab.ts b/e2e/tests/ui/pages/vulnerability-details/advisories/AdvisoriesTab.ts index 597625c7a..c3fd379e0 100644 --- a/e2e/tests/ui/pages/vulnerability-details/advisories/AdvisoriesTab.ts +++ b/e2e/tests/ui/pages/vulnerability-details/advisories/AdvisoriesTab.ts @@ -24,7 +24,9 @@ export class AdvisoriesTab { } async getToolbar() { - return await Toolbar.build(this._page, "Advisory toolbar"); + return await Toolbar.build(this._page, "Advisory toolbar", { + "Filter text": "string", + }); } async getTable() { diff --git a/e2e/tests/ui/pages/vulnerability-list/VulnerabilityListPage.ts b/e2e/tests/ui/pages/vulnerability-list/VulnerabilityListPage.ts index b994f08f2..394904a50 100644 --- a/e2e/tests/ui/pages/vulnerability-list/VulnerabilityListPage.ts +++ b/e2e/tests/ui/pages/vulnerability-list/VulnerabilityListPage.ts @@ -19,7 +19,11 @@ export class VulnerabilityListPage { } async getToolbar() { - return await Toolbar.build(this._page, "vulnerability-toolbar"); + return await Toolbar.build(this._page, "vulnerability-toolbar", { + "Filter text": "string", + CVSS: "multiSelect", + "Created on": "dateRange", + }); } async getTable() { diff --git a/e2e/tests/ui/pages/vulnerability-list/columns.spec.ts b/e2e/tests/ui/pages/vulnerability-list/columns.spec.ts index 081e1395a..495cbb13b 100644 --- a/e2e/tests/ui/pages/vulnerability-list/columns.spec.ts +++ b/e2e/tests/ui/pages/vulnerability-list/columns.spec.ts @@ -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(); await table.verifyColumnContainsText("ID", "CVE-2024-26308"); diff --git a/e2e/tests/ui/pages/vulnerability-list/filter.spec.ts b/e2e/tests/ui/pages/vulnerability-list/filter.spec.ts index a9a2c8e12..b72fa5cc5 100644 --- a/e2e/tests/ui/pages/vulnerability-list/filter.spec.ts +++ b/e2e/tests/ui/pages/vulnerability-list/filter.spec.ts @@ -16,21 +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"); // Severity filter - await toolbar.applyMultiSelectFilter("CVSS", ["Unknown", "Medium"]); + await toolbar.applyFilter({ CVSS: ["Unknown", "Medium"] }); await table.waitUntilDataIsLoaded(); await table.verifyColumnContainsText("ID", "CVE-2024-26308"); // Date filter - await toolbar.applyDateRangeFilter( - "Created on", - "02/18/2024", - "02/20/2024", - ); + await toolbar.applyFilter({ + "Created on": { from: "02/18/2024", to: "02/20/2024" }, + }); await table.waitUntilDataIsLoaded(); await table.verifyColumnContainsText("ID", "CVE-2024-26308"); }); From dc3c00972f5373dc36e42a197218f845c3598152 Mon Sep 17 00:00:00 2001 From: Carlos Feria <2582866+carlosthe19916@users.noreply.github.com> Date: Tue, 4 Nov 2025 20:23:08 +0000 Subject: [PATCH 3/5] fix: remove unused file --- e2e/tests/common/utils.ts | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 e2e/tests/common/utils.ts diff --git a/e2e/tests/common/utils.ts b/e2e/tests/common/utils.ts deleted file mode 100644 index 9058c870a..000000000 --- a/e2e/tests/common/utils.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const typedEntries = (obj: T) => { - return Object.entries(obj) as [Extract, T[keyof T]][]; -}; From 1a063a93a711df9cd3d950658a30cc27abfc44bb Mon Sep 17 00:00:00 2001 From: Carlos Feria <2582866+carlosthe19916@users.noreply.github.com> Date: Tue, 4 Nov 2025 20:47:48 +0000 Subject: [PATCH 4/5] fix: add validation of undefined values --- e2e/tests/ui/pages/Toolbar.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/e2e/tests/ui/pages/Toolbar.ts b/e2e/tests/ui/pages/Toolbar.ts index dc15e7e7d..1e4d10dfe 100644 --- a/e2e/tests/ui/pages/Toolbar.ts +++ b/e2e/tests/ui/pages/Toolbar.ts @@ -76,6 +76,8 @@ export class Toolbar> { Extract >) { const filterValue = filters[filterName]; + if (!filterValue) continue; + const filterType = this._filters[filterName]; await this.selectFilter(filterName); From a51a889d4fcba77c646fe6f881d5aac5716716e8 Mon Sep 17 00:00:00 2001 From: Carlos Feria <2582866+carlosthe19916@users.noreply.github.com> Date: Sun, 9 Nov 2025 11:42:40 -0500 Subject: [PATCH 5/5] fix: simplify TFilterName definition Signed-off-by: Carlos Feria <2582866+carlosthe19916@users.noreply.github.com> --- e2e/tests/ui/pages/Toolbar.ts | 45 ++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/e2e/tests/ui/pages/Toolbar.ts b/e2e/tests/ui/pages/Toolbar.ts index 1e4d10dfe..52c721120 100644 --- a/e2e/tests/ui/pages/Toolbar.ts +++ b/e2e/tests/ui/pages/Toolbar.ts @@ -44,7 +44,10 @@ function isTypeaheadFilter>( return type === "typeahead"; } -export class Toolbar> { +export class Toolbar< + TFilter extends Record, + TFilterName extends Extract, +> { private readonly _page: Page; _toolbar: Locator; private readonly _filters: TFilter; @@ -72,9 +75,7 @@ export class Toolbar> { } async applyFilter(filters: Partial>) { - for (const filterName of Object.keys(filters) as Array< - Extract - >) { + for (const filterName of Object.keys(filters) as Array) { const filterValue = filters[filterName]; if (!filterValue) continue; @@ -93,18 +94,17 @@ export class Toolbar> { } } - private async applyTextFilter< - TFilterName extends Extract, - >(filterName: TFilterName, filterValue: string) { + 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); } - private async applyDateRangeFilter< - TFilterName extends Extract, - >(filterName: TFilterName, dateRange: TDateRange) { + private async applyDateRangeFilter( + filterName: TFilterName, + dateRange: TDateRange, + ) { await this._toolbar .locator("input[aria-label='Interval start']") .fill(dateRange.from); @@ -118,9 +118,10 @@ export class Toolbar> { ]); } - private async applyMultiSelectFilter< - TFilterName extends Extract, - >(filterName: TFilterName, selections: string[]) { + private async applyMultiSelectFilter( + filterName: TFilterName, + selections: string[], + ) { for (const option of selections) { const inputText = this._toolbar.locator( "input[aria-label='Type to filter']", @@ -139,9 +140,10 @@ export class Toolbar> { await this.assertFilterHasLabels(filterName, selections); } - private async applyTypeaheadFilter< - TFilterName extends Extract, - >(filterName: TFilterName, labels: string[]) { + private async applyTypeaheadFilter( + filterName: TFilterName, + labels: string[], + ) { for (const label of labels) { await this._toolbar .locator("input[aria-label='select-autocomplete-listbox']") @@ -159,18 +161,17 @@ export class Toolbar> { * Selects the main filter to be applied * @param filterName the name of the filter as rendered in the UI */ - private async selectFilter< - TFilterName extends Extract, - >(filterName: TFilterName) { + 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< - TFilterName extends Extract, - >(filterName: TFilterName, filterValue: string | string[]) { + private async assertFilterHasLabels( + filterName: TFilterName, + filterValue: string | string[], + ) { await expect( this._toolbar.locator(".pf-m-label-group", { hasText: filterName }), ).toBeVisible();