diff --git a/.devcontainer/template.json b/.devcontainer/template.json
index 4e8bb6d0c..c01c95141 100644
--- a/.devcontainer/template.json
+++ b/.devcontainer/template.json
@@ -1,6 +1,8 @@
{
"name": "playwright",
- "image": "mcr.microsoft.com/playwright:v1.55.0-jammy",
+ "build": {
+ "dockerfile": "Dockerfile"
+ },
"workspaceFolder": "/workspaces/trustify-ui/e2e",
"runArgs": [
"--privileged",
@@ -27,7 +29,8 @@
"customizations": {
"vscode": {
"extensions": [
- "ms-playwright.playwright"
+ "ms-playwright.playwright",
+ "alexkrechik.cucumberautocomplete"
]
}
}
diff --git a/.env b/.env
index 0a3d759d2..64197d16b 100644
--- a/.env
+++ b/.env
@@ -7,4 +7,4 @@ PLAYWRIGHT_IMAGE=mcr.microsoft.com/playwright
PLAYWRIGHT_VERSION=v1.55.0
UBUNTU_VERSION_ALIAS=-jammy
PLAYWRIGHT_PORT=5000
-PLAYWRIGHT_HOME=/home/pwuser
\ No newline at end of file
+PLAYWRIGHT_HOME=/home/pwuser
diff --git a/e2e/tests/ui/pages/DetailsPageLayout.ts b/e2e/tests/ui/pages/DetailsPageLayout.ts
new file mode 100644
index 000000000..3101216c6
--- /dev/null
+++ b/e2e/tests/ui/pages/DetailsPageLayout.ts
@@ -0,0 +1,43 @@
+import { expect, type Page } from "@playwright/test";
+
+export class DetailsPageLayout {
+ private readonly _page: Page;
+
+ private constructor(page: Page) {
+ this._page = page;
+ }
+
+ static async build(page: Page) {
+ await expect(page.locator("nav[aria-label='Breadcrumb']")).toBeVisible();
+ return new DetailsPageLayout(page);
+ }
+
+ async selectTab(tabName: string) {
+ const tab = this._page.locator("button[role='tab']", { hasText: tabName });
+ await expect(tab).toBeVisible();
+ await tab.click();
+ }
+
+ async clickOnPageAction(actionName: string) {
+ await this._page.getByRole("button", { name: "Actions" }).click();
+ await this._page.getByRole("menuitem", { name: actionName }).click();
+ }
+
+ async verifyPageHeader(header: string) {
+ await expect(this._page.getByRole("heading")).toContainText(header);
+ }
+
+ async verifyTabIsSelected(tabName: string) {
+ await expect(
+ this._page.getByRole("tab", { name: tabName }),
+ ).toHaveAttribute("aria-selected", "true");
+ }
+
+ async verifyTabIsVisible(tabName: string) {
+ await expect(this._page.getByRole("tab", { name: tabName })).toBeVisible();
+ }
+
+ async verifyTabIsNotVisible(tabName: string) {
+ await expect(this._page.getByRole("tab", { name: tabName })).toHaveCount(0);
+ }
+}
diff --git a/e2e/tests/ui/pages/Helpers.ts b/e2e/tests/ui/pages/Helpers.ts
new file mode 100644
index 000000000..338ffacf6
--- /dev/null
+++ b/e2e/tests/ui/pages/Helpers.ts
@@ -0,0 +1,23 @@
+import { expect } from "@playwright/test";
+
+export const sortArray = (arr: string[], asc: boolean) => {
+ let sorted = [...arr].sort((a, b) =>
+ a.localeCompare(b, "en", { numeric: true }),
+ );
+ if (!asc) {
+ sorted = sorted.reverse();
+ }
+ const isSorted = arr.every((val, i) => val === sorted[i]);
+ return {
+ isSorted,
+ sorted,
+ };
+};
+
+export const expectSort = (arr: string[], asc: boolean) => {
+ const { isSorted, sorted } = sortArray(arr, asc);
+ expect(
+ isSorted,
+ `Received: ${arr.join(", ")} \nExpected: ${sorted.join(", ")}`,
+ ).toBe(true);
+};
diff --git a/e2e/tests/ui/pages/LabelsModal.ts b/e2e/tests/ui/pages/LabelsModal.ts
new file mode 100644
index 000000000..7f1a8159f
--- /dev/null
+++ b/e2e/tests/ui/pages/LabelsModal.ts
@@ -0,0 +1,42 @@
+import type { Locator, Page } from "playwright-core";
+import { expect } from "playwright/test";
+
+export class LabelsModal {
+ private _dialog: Locator;
+
+ private constructor(_page: Page, dialog: Locator) {
+ this._dialog = dialog;
+ }
+
+ static async build(page: Page) {
+ const dialog = page.getByRole("dialog");
+ await expect(dialog).toBeVisible();
+ return new LabelsModal(page, dialog);
+ }
+
+ async clickSave() {
+ await this._dialog.locator("button[aria-label='submit']").click();
+ await expect(this._dialog).not.toBeVisible();
+ }
+
+ async addLabels(labels: string[]) {
+ const inputText = this._dialog.getByPlaceholder("Add label");
+
+ for (const label of labels) {
+ await inputText.click();
+ await inputText.fill(label);
+ await inputText.press("Enter");
+ }
+ }
+
+ async removeLabels(labels: string[]) {
+ for (const label of labels) {
+ await this._dialog
+ .locator(".pf-v6-c-label-group__list-item", {
+ hasText: label,
+ })
+ .locator("button")
+ .click();
+ }
+ }
+}
diff --git a/e2e/tests/ui/pages/Navigation.ts b/e2e/tests/ui/pages/Navigation.ts
new file mode 100644
index 000000000..f9a7fd66e
--- /dev/null
+++ b/e2e/tests/ui/pages/Navigation.ts
@@ -0,0 +1,33 @@
+import type { Page } from "playwright-core";
+
+/**
+ * Used to navigate to different pages
+ */
+export class Navigation {
+ private readonly _page: Page;
+
+ private constructor(page: Page) {
+ this._page = page;
+ }
+
+ static async build(page: Page) {
+ return new Navigation(page);
+ }
+
+ async goToSidebar(
+ menu:
+ | "Dashboard"
+ | "Search"
+ | "SBOMs"
+ | "Vulnerabilities"
+ | "Packages"
+ | "Advisories"
+ | "Importers"
+ | "Upload",
+ ) {
+ // By default, we do not initialize navigation at "/"" where the Dashboard is located
+ // This should help us to save some time loading pages as the Dashboard fetches too much data
+ await this._page.goto("/upload");
+ await this._page.getByRole("link", { name: menu }).click();
+ }
+}
diff --git a/e2e/tests/ui/pages/Pagination.ts b/e2e/tests/ui/pages/Pagination.ts
new file mode 100644
index 000000000..cad53b77a
--- /dev/null
+++ b/e2e/tests/ui/pages/Pagination.ts
@@ -0,0 +1,90 @@
+import { expect, type Locator, type Page } from "@playwright/test";
+import type { Table } from "./Table";
+
+export class Pagination {
+ private readonly _page: Page;
+ _pagination: Locator;
+
+ private constructor(page: Page, pagination: Locator) {
+ this._page = page;
+ this._pagination = pagination;
+ }
+
+ static async build(page: Page, paginationId: string) {
+ const pagination = page.locator(`#${paginationId}`);
+ await expect(pagination).toBeVisible();
+ return new Pagination(page, pagination);
+ }
+
+ /**
+ * Selects Number of rows per page on the table
+ * @param perPage Number of rows
+ */
+ async selectItemsPerPage(perPage: 10 | 20 | 50 | 100) {
+ await this._pagination
+ .locator(`//button[@aria-haspopup='listbox']`)
+ .click();
+ await this._page
+ .getByRole("menuitem", { name: `${perPage} per page` })
+ .click();
+
+ await expect(this._pagination.locator("input")).toHaveValue("1");
+ }
+
+ async validatePagination() {
+ // Verify next buttons are enabled as there are more than 11 rows present
+ const nextPageButton = this._pagination.locator(
+ "button[data-action='next']",
+ );
+ await expect(nextPageButton).toBeVisible();
+ await expect(nextPageButton).not.toBeDisabled();
+
+ // Verify that previous buttons are disabled being on the first page
+ const prevPageButton = this._pagination.locator(
+ "button[data-action='previous']",
+ );
+ await expect(prevPageButton).toBeVisible();
+ await expect(prevPageButton).toBeDisabled();
+
+ // Verify that navigation button to last page is enabled
+ const lastPageButton = this._pagination.locator(
+ "button[data-action='last']",
+ );
+ await expect(lastPageButton).toBeVisible();
+ await expect(lastPageButton).not.toBeDisabled();
+
+ // Verify that navigation button to first page is disabled being on the first page
+ const firstPageButton = this._pagination.locator(
+ "button[data-action='first']",
+ );
+ await expect(firstPageButton).toBeVisible();
+ await expect(firstPageButton).toBeDisabled();
+
+ // Navigate to next page
+ await nextPageButton.click();
+
+ // Verify that previous buttons are enabled after moving to next page
+ await expect(prevPageButton).toBeVisible();
+ await expect(prevPageButton).not.toBeDisabled();
+
+ // Verify that navigation button to first page is enabled after moving to next page
+ await expect(firstPageButton).toBeVisible();
+ await expect(firstPageButton).not.toBeDisabled();
+
+ // Moving back to the first page
+ await firstPageButton.click();
+ }
+
+ async validateItemsPerPage(columnName: string, table: Table) {
+ // Verify that only 10 items are displayed
+ await this.selectItemsPerPage(10);
+ await table.validateNumberOfRows({ equal: 10 }, columnName);
+
+ // Verify that items less than or equal to 20 and greater than 10 are displayed
+ await this.selectItemsPerPage(20);
+ await table.validateNumberOfRows(
+ { greaterThan: 10, lessThan: 21 },
+ columnName,
+ );
+ }
+}
diff --git a/e2e/tests/ui/pages/Table.ts b/e2e/tests/ui/pages/Table.ts
new file mode 100644
index 000000000..d7024ee64
--- /dev/null
+++ b/e2e/tests/ui/pages/Table.ts
@@ -0,0 +1,92 @@
+import { expect, type Locator, type Page } from "@playwright/test";
+
+export class Table {
+ private readonly _page: Page;
+ _table: Locator;
+
+ private constructor(page: Page, table: Locator) {
+ this._page = page;
+ this._table = table;
+ }
+
+ /**
+ * @param page
+ * @param tableAriaLabel the unique aria-label that corresponds to the DOM element that contains the Table. E.g.
+ * @returns a new instance of a Toolbar
+ */
+ static async build(page: Page, tableAriaLabel: string) {
+ const table = page.locator(`table[aria-label="${tableAriaLabel}"]`);
+ await expect(table).toBeVisible();
+
+ const result = new Table(page, table);
+ await result.waitUntilDataIsLoaded();
+ return result;
+ }
+
+ async waitUntilDataIsLoaded() {
+ const rows = this._table.locator(
+ 'xpath=//tbody[not(@aria-label="Table loading")]',
+ );
+ await expect(rows.first()).toBeVisible();
+
+ const rowsCount = await rows.count();
+ expect(rowsCount).toBeGreaterThanOrEqual(1);
+ }
+
+ async clickSortBy(columnName: string) {
+ await this._table
+ .getByRole("button", { name: columnName, exact: true })
+ .click();
+ await this.waitUntilDataIsLoaded();
+ }
+
+ async clickAction(actionName: string, rowIndex: number) {
+ await this._table
+ .locator(`button[aria-label="Kebab toggle"]`)
+ .nth(rowIndex)
+ .click();
+
+ await this._page.getByRole("menuitem", { name: actionName }).click();
+ }
+
+ async verifyTableIsSortedBy(columnName: string, asc: boolean = true) {
+ await expect(
+ this._table.getByRole("columnheader", { name: columnName }),
+ ).toHaveAttribute("aria-sort", asc ? "ascending" : "descending");
+ }
+
+ async verifyColumnContainsText(columnName: string, expectedValue: string) {
+ await expect(
+ this._table.locator(`td[data-label="${columnName}"]`, {
+ hasText: expectedValue,
+ }),
+ ).toBeVisible();
+ }
+
+ async verifyTableHasNoData() {
+ await expect(
+ this._table.locator(`tbody[aria-label="Table empty"]`),
+ ).toBeVisible();
+ }
+
+ async validateNumberOfRows(
+ expectedRows: {
+ equal?: number;
+ greaterThan?: number;
+ lessThan?: number;
+ },
+ columnName: string,
+ ) {
+ const rows = this._table.locator(`td[data-label="${columnName}"]`);
+
+ if (expectedRows.equal) {
+ expect(await rows.count()).toBe(expectedRows.equal);
+ }
+ if (expectedRows.greaterThan) {
+ expect(await rows.count()).toBeGreaterThan(expectedRows.greaterThan);
+ }
+ if (expectedRows.lessThan) {
+ expect(await rows.count()).toBeLessThan(expectedRows.lessThan);
+ }
+ }
+}
diff --git a/e2e/tests/ui/pages/Toolbar.ts b/e2e/tests/ui/pages/Toolbar.ts
new file mode 100644
index 000000000..2a3927ae6
--- /dev/null
+++ b/e2e/tests/ui/pages/Toolbar.ts
@@ -0,0 +1,109 @@
+import { expect, type Locator, type Page } from "@playwright/test";
+
+export class Toolbar {
+ private readonly _page: Page;
+ _toolbar: Locator;
+
+ private constructor(page: Page, toolbar: Locator) {
+ this._page = page;
+ this._toolbar = toolbar;
+ }
+
+ /**
+ * @param page
+ * @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) {
+ 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: 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 applyTextFilter(filterName: string, filterValue: string) {
+ await this.selectFilter(filterName);
+
+ 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,
+ ) {
+ await this.selectFilter(filterName);
+
+ await this._toolbar
+ .locator("input[aria-label='Interval start']")
+ .fill(fromDate);
+ await this._toolbar
+ .locator("input[aria-label='Interval end']")
+ .fill(toDate);
+
+ await this.assertFilterHasLabels(filterName, [fromDate, toDate]);
+ }
+
+ async applyMultiSelectFilter(filterName: string, selections: string[]) {
+ await this.selectFilter(filterName);
+
+ for (const option of selections) {
+ const inputText = this._toolbar.locator(
+ "input[aria-label='Type to filter']",
+ );
+ await inputText.clear();
+ await inputText.fill(option);
+
+ const dropdownOption = this._page.getByRole("menuitem", { name: option });
+ await expect(dropdownOption).toBeVisible();
+ await dropdownOption.click();
+ }
+
+ await this.assertFilterHasLabels(filterName, selections);
+ }
+
+ async applyLabelsFilter(filterName: string, labels: string[]) {
+ await this.selectFilter(filterName);
+
+ for (const label of labels) {
+ await this._toolbar
+ .locator("input[aria-label='select-autocomplete-listbox']")
+ .fill(label);
+
+ const dropdownOption = this._page.getByRole("option", { name: label });
+ await expect(dropdownOption).toBeVisible();
+ await dropdownOption.click();
+ }
+
+ await this.assertFilterHasLabels(filterName, labels);
+ }
+}
diff --git a/e2e/tests/ui/pages/advisory-details/AdvisoryDetailsPage.ts b/e2e/tests/ui/pages/advisory-details/AdvisoryDetailsPage.ts
new file mode 100644
index 000000000..771c6c0d4
--- /dev/null
+++ b/e2e/tests/ui/pages/advisory-details/AdvisoryDetailsPage.ts
@@ -0,0 +1,32 @@
+import type { Page } from "@playwright/test";
+import { DetailsPageLayout } from "../DetailsPageLayout";
+import { Navigation } from "../Navigation";
+import { AdvisoryListPage } from "../advisory-list/AdvisoryListPage";
+
+export class AdvisoryDetailsPage {
+ _layout: DetailsPageLayout;
+
+ private constructor(_page: Page, layout: DetailsPageLayout) {
+ this._layout = layout;
+ }
+
+ static async build(page: Page, advisoryID: string) {
+ const navigation = await Navigation.build(page);
+ await navigation.goToSidebar("Advisories");
+
+ const listPage = await AdvisoryListPage.build(page);
+ const toolbar = await listPage.getToolbar();
+ const table = await listPage.getTable();
+
+ await toolbar.applyTextFilter("Filter text", advisoryID);
+ await table.waitUntilDataIsLoaded();
+ await table.verifyColumnContainsText("ID", advisoryID);
+
+ await page.getByRole("link", { name: advisoryID, exact: true }).click();
+
+ const layout = await DetailsPageLayout.build(page);
+ await layout.verifyPageHeader(advisoryID);
+
+ return new AdvisoryDetailsPage(page, layout);
+ }
+}
diff --git a/e2e/tests/ui/pages/advisory-details/info/info.spec.ts b/e2e/tests/ui/pages/advisory-details/info/info.spec.ts
new file mode 100644
index 000000000..fb4528169
--- /dev/null
+++ b/e2e/tests/ui/pages/advisory-details/info/info.spec.ts
@@ -0,0 +1,46 @@
+// @ts-check
+
+import { expect } from "@playwright/test";
+
+import { test } from "../../../fixtures";
+import { login } from "../../../helpers/Auth";
+import { AdvisoryDetailsPage } from "../AdvisoryDetailsPage";
+import { LabelsModal } from "../../LabelsModal";
+
+test.describe("Info Tab validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Labels", async ({ page }) => {
+ await AdvisoryDetailsPage.build(page, "CVE-2024-26308");
+
+ const labels = ["color=red", "production"];
+
+ // Open Edit Labels Modal
+ await page.getByRole("button", { name: "Edit" }).click();
+
+ let labelsModal = await LabelsModal.build(page);
+ await labelsModal.addLabels(labels);
+ await labelsModal.clickSave();
+
+ for (const label of labels) {
+ await expect(
+ page.locator(".pf-v6-c-label-group__list-item", { hasText: label }),
+ ).toHaveCount(1);
+ }
+
+ // Clean labels added previously
+ await page.getByRole("button", { name: "Edit" }).click();
+
+ labelsModal = await LabelsModal.build(page);
+ await labelsModal.removeLabels(labels);
+ await labelsModal.clickSave();
+
+ for (const label of labels) {
+ await expect(
+ page.locator(".pf-v6-c-label-group__list-item", { hasText: label }),
+ ).toHaveCount(0);
+ }
+ });
+});
diff --git a/e2e/tests/ui/pages/advisory-details/vulnerabilities/VulnerabilitiesTab.ts b/e2e/tests/ui/pages/advisory-details/vulnerabilities/VulnerabilitiesTab.ts
new file mode 100644
index 000000000..3ab068efa
--- /dev/null
+++ b/e2e/tests/ui/pages/advisory-details/vulnerabilities/VulnerabilitiesTab.ts
@@ -0,0 +1,37 @@
+import type { Page } from "@playwright/test";
+import { AdvisoryDetailsPage } from "../AdvisoryDetailsPage";
+import { Toolbar } from "../../Toolbar";
+import { Table } from "../../Table";
+import { Pagination } from "../../Pagination";
+
+export class VulnerabilitiesTab {
+ private readonly _page: Page;
+ _detailsPage: AdvisoryDetailsPage;
+
+ private constructor(page: Page, layout: AdvisoryDetailsPage) {
+ this._page = page;
+ this._detailsPage = layout;
+ }
+
+ static async build(page: Page, packageName: string) {
+ const detailsPage = await AdvisoryDetailsPage.build(page, packageName);
+ await detailsPage._layout.selectTab("Vulnerabilities");
+
+ return new VulnerabilitiesTab(page, detailsPage);
+ }
+
+ async getToolbar() {
+ return await Toolbar.build(this._page, "vulnerability toolbar");
+ }
+
+ async getTable() {
+ return await Table.build(this._page, "vulnerability table");
+ }
+
+ async getPagination(location: "top" | "bottom" = "top") {
+ return await Pagination.build(
+ this._page,
+ `vulnerability-table-pagination-${location}`,
+ );
+ }
+}
diff --git a/e2e/tests/ui/pages/advisory-details/vulnerabilities/columns.spec.ts b/e2e/tests/ui/pages/advisory-details/vulnerabilities/columns.spec.ts
new file mode 100644
index 000000000..54f7e43e1
--- /dev/null
+++ b/e2e/tests/ui/pages/advisory-details/vulnerabilities/columns.spec.ts
@@ -0,0 +1,44 @@
+// @ts-check
+
+import { expect } from "@playwright/test";
+
+import { test } from "../../../fixtures";
+import { login } from "../../../helpers/Auth";
+import { VulnerabilitiesTab } from "./VulnerabilitiesTab";
+
+test.describe("Columns validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Columns", async ({ page }) => {
+ const vulnerabilitiesTab = await VulnerabilitiesTab.build(
+ page,
+ "CVE-2024-26308",
+ );
+ const table = await vulnerabilitiesTab.getTable();
+
+ const ids = await table._table
+ .locator(`td[data-label="ID"]`)
+ .allInnerTexts();
+ const idIndex = ids.indexOf("CVE-2024-26308");
+ expect(idIndex).not.toBe(-1);
+
+ // Name
+ await expect(
+ table._table.locator(`td[data-label="ID"]`).nth(idIndex),
+ ).toContainText("CVE-2024-26308");
+
+ // Title
+ await expect(
+ table._table.locator(`td[data-label="Title"]`).nth(idIndex),
+ ).toContainText(
+ "Apache Commons Compress: OutOfMemoryError unpacking broken Pack200 file",
+ );
+
+ // CWE
+ await expect(
+ table._table.locator(`td[data-label="CWE"]`).nth(idIndex),
+ ).toContainText("CWE-770");
+ });
+});
diff --git a/e2e/tests/ui/pages/advisory-details/vulnerabilities/pagination.spec.ts b/e2e/tests/ui/pages/advisory-details/vulnerabilities/pagination.spec.ts
new file mode 100644
index 000000000..dff610544
--- /dev/null
+++ b/e2e/tests/ui/pages/advisory-details/vulnerabilities/pagination.spec.ts
@@ -0,0 +1,34 @@
+// @ts-check
+
+import { test } from "../../../fixtures";
+import { login } from "../../../helpers/Auth";
+import { VulnerabilitiesTab } from "./VulnerabilitiesTab";
+
+// Number of items less than 10, cannot tests pagination
+test.describe.skip("Pagination validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Navigation button validations", async ({ page }) => {
+ const vulnerabilitiesTab = await VulnerabilitiesTab.build(
+ page,
+ "CVE-2024-26308",
+ );
+ const pagination = await vulnerabilitiesTab.getPagination();
+
+ await pagination.validatePagination();
+ });
+
+ test("Items per page validations", async ({ page }) => {
+ const vulnerabilitiesTab = await VulnerabilitiesTab.build(
+ page,
+ "CVE-2024-26308",
+ );
+
+ const pagination = await vulnerabilitiesTab.getPagination();
+ const table = await vulnerabilitiesTab.getTable();
+
+ await pagination.validateItemsPerPage("ID", table);
+ });
+});
diff --git a/e2e/tests/ui/pages/advisory-details/vulnerabilities/sort.spec.ts b/e2e/tests/ui/pages/advisory-details/vulnerabilities/sort.spec.ts
new file mode 100644
index 000000000..ff39c15dc
--- /dev/null
+++ b/e2e/tests/ui/pages/advisory-details/vulnerabilities/sort.spec.ts
@@ -0,0 +1,30 @@
+// @ts-check
+
+import { test } from "../../../fixtures";
+import { login } from "../../../helpers/Auth";
+import { VulnerabilitiesTab } from "./VulnerabilitiesTab";
+import { expectSort } from "../../Helpers";
+
+test.describe("Sort validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Sort", async ({ page }) => {
+ const vulnerabilitiesTab = await VulnerabilitiesTab.build(
+ page,
+ "CVE-2024-26308",
+ );
+ const table = await vulnerabilitiesTab.getTable();
+
+ const columnNameSelector = table._table.locator(`td[data-label="ID"]`);
+
+ const ascList = await columnNameSelector.allInnerTexts();
+ expectSort(ascList, true);
+
+ // Reverse sorting
+ await table.clickSortBy("ID");
+ const descList = await columnNameSelector.allInnerTexts();
+ expectSort(descList, false);
+ });
+});
diff --git a/e2e/tests/ui/pages/advisory-list/AdvisoryListPage.ts b/e2e/tests/ui/pages/advisory-list/AdvisoryListPage.ts
new file mode 100644
index 000000000..54d5cf30a
--- /dev/null
+++ b/e2e/tests/ui/pages/advisory-list/AdvisoryListPage.ts
@@ -0,0 +1,35 @@
+import type { Page } from "@playwright/test";
+import { Navigation } from "../Navigation";
+import { Toolbar } from "../Toolbar";
+import { Table } from "../Table";
+import { Pagination } from "../Pagination";
+
+export class AdvisoryListPage {
+ private readonly _page: Page;
+
+ private constructor(page: Page) {
+ this._page = page;
+ }
+
+ static async build(page: Page) {
+ const navigation = await Navigation.build(page);
+ await navigation.goToSidebar("Advisories");
+
+ return new AdvisoryListPage(page);
+ }
+
+ async getToolbar() {
+ return await Toolbar.build(this._page, "advisory-toolbar");
+ }
+
+ async getTable() {
+ return await Table.build(this._page, "advisory-table");
+ }
+
+ async getPagination(top: boolean = true) {
+ return await Pagination.build(
+ this._page,
+ `advisory-table-pagination-${top ? "top" : "bottom"}`,
+ );
+ }
+}
diff --git a/e2e/tests/ui/pages/advisory-list/columns.spec.ts b/e2e/tests/ui/pages/advisory-list/columns.spec.ts
new file mode 100644
index 000000000..7d1ef125f
--- /dev/null
+++ b/e2e/tests/ui/pages/advisory-list/columns.spec.ts
@@ -0,0 +1,42 @@
+// @ts-check
+
+import { expect } from "@playwright/test";
+
+import { test } from "../../fixtures";
+import { login } from "../../helpers/Auth";
+import { AdvisoryListPage } from "./AdvisoryListPage";
+
+test.describe("Columns validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Columns", async ({ page }) => {
+ const listPage = await AdvisoryListPage.build(page);
+
+ const toolbar = await listPage.getToolbar();
+ const table = await listPage.getTable();
+
+ // Full search
+ await toolbar.applyTextFilter("Filter text", "CVE-2024-26308");
+ await table.waitUntilDataIsLoaded();
+
+ // ID
+ await table.verifyColumnContainsText("ID", "CVE-2024-26308");
+
+ // Title
+ await expect(table._table.locator(`td[data-label="Title"]`)).toContainText(
+ "Apache Commons Compress: OutOfMemoryError unpacking broken Pack200 file",
+ );
+
+ // Type
+ await expect(table._table.locator(`td[data-label="Type"]`)).toContainText(
+ "cve",
+ );
+
+ // Labels
+ await expect(table._table.locator(`td[data-label="Labels"]`)).toContainText(
+ "type=cve",
+ );
+ });
+});
diff --git a/e2e/tests/ui/pages/advisory-list/filter.spec.ts b/e2e/tests/ui/pages/advisory-list/filter.spec.ts
new file mode 100644
index 000000000..1351a884e
--- /dev/null
+++ b/e2e/tests/ui/pages/advisory-list/filter.spec.ts
@@ -0,0 +1,33 @@
+// @ts-check
+
+import { test } from "../../fixtures";
+import { login } from "../../helpers/Auth";
+import { AdvisoryListPage } from "./AdvisoryListPage";
+
+test.describe("Filter validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Filters", async ({ page }) => {
+ const listPage = await AdvisoryListPage.build(page);
+
+ const toolbar = await listPage.getToolbar();
+ const table = await listPage.getTable();
+
+ // Full search
+ await toolbar.applyTextFilter("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 table.waitUntilDataIsLoaded();
+ await table.verifyColumnContainsText("ID", "CVE-2024-26308");
+
+ // Labels filter
+ await toolbar.applyLabelsFilter("Label", ["type=cve"]);
+ await table.waitUntilDataIsLoaded();
+ await table.verifyColumnContainsText("ID", "CVE-2024-26308");
+ });
+});
diff --git a/e2e/tests/ui/pages/advisory-list/pagination.spec.ts b/e2e/tests/ui/pages/advisory-list/pagination.spec.ts
new file mode 100644
index 000000000..2fea59397
--- /dev/null
+++ b/e2e/tests/ui/pages/advisory-list/pagination.spec.ts
@@ -0,0 +1,27 @@
+// @ts-check
+
+import { test } from "../../fixtures";
+import { login } from "../../helpers/Auth";
+import { AdvisoryListPage } from "./AdvisoryListPage";
+
+test.describe("Pagination validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Navigation button validations", async ({ page }) => {
+ const listPage = await AdvisoryListPage.build(page);
+
+ const pagination = await listPage.getPagination();
+ await pagination.validatePagination();
+ });
+
+ test("Items per page validations", async ({ page }) => {
+ const listPage = await AdvisoryListPage.build(page);
+
+ const pagination = await listPage.getPagination();
+ const table = await listPage.getTable();
+
+ await pagination.validateItemsPerPage("ID", table);
+ });
+});
diff --git a/e2e/tests/ui/pages/advisory-list/sort.spec.ts b/e2e/tests/ui/pages/advisory-list/sort.spec.ts
new file mode 100644
index 000000000..570e36591
--- /dev/null
+++ b/e2e/tests/ui/pages/advisory-list/sort.spec.ts
@@ -0,0 +1,30 @@
+// @ts-check
+
+import { test } from "../../fixtures";
+import { login } from "../../helpers/Auth";
+import { expectSort } from "../Helpers";
+import { AdvisoryListPage } from "./AdvisoryListPage";
+
+test.describe("Sort validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ // TODO: enable after https://github.com/trustification/trustify/issues/1810 is fixed
+ test.skip("Sort", async ({ page }) => {
+ const listPage = await AdvisoryListPage.build(page);
+ const table = await listPage.getTable();
+
+ const columnNameSelector = table._table.locator(`td[data-label="ID"]`);
+
+ // ID Asc
+ await table.clickSortBy("ID");
+ const ascList = await columnNameSelector.allInnerTexts();
+ expectSort(ascList, true);
+
+ // ID Desc
+ await table.clickSortBy("ID");
+ const descList = await columnNameSelector.allInnerTexts();
+ expectSort(descList, false);
+ });
+});
diff --git a/e2e/tests/ui/pages/package-details/PackageDetailsPage.ts b/e2e/tests/ui/pages/package-details/PackageDetailsPage.ts
new file mode 100644
index 000000000..90a248dfc
--- /dev/null
+++ b/e2e/tests/ui/pages/package-details/PackageDetailsPage.ts
@@ -0,0 +1,32 @@
+import type { Page } from "@playwright/test";
+import { DetailsPageLayout } from "../DetailsPageLayout";
+import { Navigation } from "../Navigation";
+import { PackageListPage } from "../package-list/PackageListPage";
+
+export class PackageDetailsPage {
+ _layout: DetailsPageLayout;
+
+ private constructor(_page: Page, layout: DetailsPageLayout) {
+ this._layout = layout;
+ }
+
+ static async build(page: Page, packageName: string) {
+ const navigation = await Navigation.build(page);
+ await navigation.goToSidebar("Packages");
+
+ const listPage = await PackageListPage.build(page);
+ const toolbar = await listPage.getToolbar();
+ const table = await listPage.getTable();
+
+ await toolbar.applyTextFilter("Filter text", packageName);
+ await table.waitUntilDataIsLoaded();
+ await table.verifyColumnContainsText("Name", packageName);
+
+ await page.getByRole("link", { name: packageName, exact: true }).click();
+
+ const layout = await DetailsPageLayout.build(page);
+ await layout.verifyPageHeader(packageName);
+
+ return new PackageDetailsPage(page, layout);
+ }
+}
diff --git a/e2e/tests/ui/pages/package-details/info/info.spec.ts b/e2e/tests/ui/pages/package-details/info/info.spec.ts
new file mode 100644
index 000000000..5690703fa
--- /dev/null
+++ b/e2e/tests/ui/pages/package-details/info/info.spec.ts
@@ -0,0 +1,18 @@
+// @ts-check
+
+import { expect, test } from "../../../fixtures";
+import { login } from "../../../helpers/Auth";
+import { PackageDetailsPage } from "../PackageDetailsPage";
+
+test.describe("Info Tab validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Info", async ({ page }) => {
+ await PackageDetailsPage.build(page, "keycloak-core");
+
+ // Verify version
+ await expect(page.getByText("version: 18.0.6.redhat-00001")).toHaveCount(1);
+ });
+});
diff --git a/e2e/tests/ui/pages/package-details/sboms/SbomsTab.ts b/e2e/tests/ui/pages/package-details/sboms/SbomsTab.ts
new file mode 100644
index 000000000..cc956cbb5
--- /dev/null
+++ b/e2e/tests/ui/pages/package-details/sboms/SbomsTab.ts
@@ -0,0 +1,37 @@
+import type { Page } from "@playwright/test";
+import { PackageDetailsPage } from "../PackageDetailsPage";
+import { Toolbar } from "../../Toolbar";
+import { Table } from "../../Table";
+import { Pagination } from "../../Pagination";
+
+export class SbomsTab {
+ private readonly _page: Page;
+ _detailsPage: PackageDetailsPage;
+
+ private constructor(page: Page, layout: PackageDetailsPage) {
+ this._page = page;
+ this._detailsPage = layout;
+ }
+
+ static async build(page: Page, packageName: string) {
+ const detailsPage = await PackageDetailsPage.build(page, packageName);
+ await detailsPage._layout.selectTab("SBOMs using package");
+
+ return new SbomsTab(page, detailsPage);
+ }
+
+ async getToolbar() {
+ return await Toolbar.build(this._page, "SBOM toolbar");
+ }
+
+ async getTable() {
+ return await Table.build(this._page, "SBOM table");
+ }
+
+ async getPagination(top: boolean = true) {
+ return await Pagination.build(
+ this._page,
+ `sbom-table-pagination-${top ? "top" : "bottom"}`,
+ );
+ }
+}
diff --git a/e2e/tests/ui/pages/package-details/sboms/columns.spec.ts b/e2e/tests/ui/pages/package-details/sboms/columns.spec.ts
new file mode 100644
index 000000000..933419cbc
--- /dev/null
+++ b/e2e/tests/ui/pages/package-details/sboms/columns.spec.ts
@@ -0,0 +1,39 @@
+// @ts-check
+
+import { expect } from "@playwright/test";
+
+import { test } from "../../../fixtures";
+import { login } from "../../../helpers/Auth";
+import { SbomsTab } from "./SbomsTab";
+
+test.describe("Columns validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Columns", async ({ page }) => {
+ const sbomTab = await SbomsTab.build(page, "keycloak-core");
+ const table = await sbomTab.getTable();
+
+ const ids = await table._table
+ .locator(`td[data-label="Name"]`)
+ .allInnerTexts();
+ const idIndex = ids.indexOf("quarkus-bom");
+ expect(idIndex).not.toBe(-1);
+
+ // Name
+ await expect(
+ table._table.locator(`td[data-label="Name"]`).nth(idIndex),
+ ).toContainText("quarkus-bom");
+
+ // Version
+ await expect(
+ table._table.locator(`td[data-label="Version"]`).nth(idIndex),
+ ).toContainText("2.13.8.Final-redhat-00004");
+
+ // Supplier
+ await expect(
+ table._table.locator(`td[data-label="Supplier"]`).nth(idIndex),
+ ).toContainText("Organization: Red Hat");
+ });
+});
diff --git a/e2e/tests/ui/pages/package-details/sboms/pagination.spec.ts b/e2e/tests/ui/pages/package-details/sboms/pagination.spec.ts
new file mode 100644
index 000000000..757c2f081
--- /dev/null
+++ b/e2e/tests/ui/pages/package-details/sboms/pagination.spec.ts
@@ -0,0 +1,28 @@
+// @ts-check
+
+import { test } from "../../../fixtures";
+import { login } from "../../../helpers/Auth";
+import { SbomsTab } from "./SbomsTab";
+
+// Number of items less than 10, cannot tests pagination
+test.describe.skip("Pagination validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Navigation button validations", async ({ page }) => {
+ const sbomTab = await SbomsTab.build(page, "keycloak-core");
+ const pagination = await sbomTab.getPagination();
+
+ await pagination.validatePagination();
+ });
+
+ test("Items per page validations", async ({ page }) => {
+ const sbomTab = await SbomsTab.build(page, "keycloak-core");
+
+ const pagination = await sbomTab.getPagination();
+ const table = await sbomTab.getTable();
+
+ await pagination.validateItemsPerPage("Name", table);
+ });
+});
diff --git a/e2e/tests/ui/pages/package-details/sboms/sort.spec.ts b/e2e/tests/ui/pages/package-details/sboms/sort.spec.ts
new file mode 100644
index 000000000..835c9a6ee
--- /dev/null
+++ b/e2e/tests/ui/pages/package-details/sboms/sort.spec.ts
@@ -0,0 +1,27 @@
+// @ts-check
+
+import { test } from "../../../fixtures";
+import { login } from "../../../helpers/Auth";
+import { SbomsTab } from "./SbomsTab";
+import { expectSort } from "../../Helpers";
+
+test.describe("Sort validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Sort", async ({ page }) => {
+ const sbomTab = await SbomsTab.build(page, "keycloak-core");
+ const table = await sbomTab.getTable();
+
+ const columnNameSelector = table._table.locator(`td[data-label="Name"]`);
+
+ const ascList = await columnNameSelector.allInnerTexts();
+ expectSort(ascList, true);
+
+ // Reverse sorting
+ await table.clickSortBy("Name");
+ const descList = await columnNameSelector.allInnerTexts();
+ expectSort(descList, false);
+ });
+});
diff --git a/e2e/tests/ui/pages/package-details/vulnerabilities/VulnerabilitiesTab.ts b/e2e/tests/ui/pages/package-details/vulnerabilities/VulnerabilitiesTab.ts
new file mode 100644
index 000000000..7c41ea60c
--- /dev/null
+++ b/e2e/tests/ui/pages/package-details/vulnerabilities/VulnerabilitiesTab.ts
@@ -0,0 +1,37 @@
+import type { Page } from "@playwright/test";
+import { PackageDetailsPage } from "../PackageDetailsPage";
+import { Toolbar } from "../../Toolbar";
+import { Table } from "../../Table";
+import { Pagination } from "../../Pagination";
+
+export class VulnerabilitiesTab {
+ private readonly _page: Page;
+ _detailsPage: PackageDetailsPage;
+
+ private constructor(page: Page, layout: PackageDetailsPage) {
+ this._page = page;
+ this._detailsPage = layout;
+ }
+
+ static async build(page: Page, packageName: string) {
+ const detailsPage = await PackageDetailsPage.build(page, packageName);
+ await detailsPage._layout.selectTab("Vulnerabilities");
+
+ return new VulnerabilitiesTab(page, detailsPage);
+ }
+
+ async getToolbar() {
+ return await Toolbar.build(this._page, "vulnerability toolbar");
+ }
+
+ async getTable() {
+ return await Table.build(this._page, "vulnerability table");
+ }
+
+ async getPagination(top: boolean = true) {
+ return await Pagination.build(
+ this._page,
+ `vulnerability-table-pagination-${top ? "top" : "bottom"}`,
+ );
+ }
+}
diff --git a/e2e/tests/ui/pages/package-details/vulnerabilities/columns.spec.ts b/e2e/tests/ui/pages/package-details/vulnerabilities/columns.spec.ts
new file mode 100644
index 000000000..cb4c6deab
--- /dev/null
+++ b/e2e/tests/ui/pages/package-details/vulnerabilities/columns.spec.ts
@@ -0,0 +1,37 @@
+// @ts-check
+
+import { expect } from "@playwright/test";
+
+import { test } from "../../../fixtures";
+import { login } from "../../../helpers/Auth";
+import { VulnerabilitiesTab } from "./VulnerabilitiesTab";
+
+test.describe("Columns validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Columns", async ({ page }) => {
+ const vulnerabilitiesTab = await VulnerabilitiesTab.build(
+ page,
+ "keycloak-core",
+ );
+ const table = await vulnerabilitiesTab.getTable();
+
+ const ids = await table._table
+ .locator(`td[data-label="ID"]`)
+ .allInnerTexts();
+ const idIndex = ids.indexOf("CVE-2023-1664");
+ expect(idIndex).not.toBe(-1);
+
+ // Name
+ await expect(
+ table._table.locator(`td[data-label="ID"]`).nth(idIndex),
+ ).toContainText("CVE-2023-1664");
+
+ // Title
+ await expect(
+ table._table.locator(`td[data-label="CVSS"]`).nth(idIndex),
+ ).toContainText("Medium(6.5)");
+ });
+});
diff --git a/e2e/tests/ui/pages/package-details/vulnerabilities/pagination.spec.ts b/e2e/tests/ui/pages/package-details/vulnerabilities/pagination.spec.ts
new file mode 100644
index 000000000..e2bc25a95
--- /dev/null
+++ b/e2e/tests/ui/pages/package-details/vulnerabilities/pagination.spec.ts
@@ -0,0 +1,34 @@
+// @ts-check
+
+import { test } from "../../../fixtures";
+import { login } from "../../../helpers/Auth";
+import { VulnerabilitiesTab } from "./VulnerabilitiesTab";
+
+// Number of items less than 10, cannot tests pagination
+test.describe.skip("Pagination validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Navigation button validations", async ({ page }) => {
+ const vulnerabilitiesTab = await VulnerabilitiesTab.build(
+ page,
+ "keycloak-core",
+ );
+ const pagination = await vulnerabilitiesTab.getPagination();
+
+ await pagination.validatePagination();
+ });
+
+ test("Items per page validations", async ({ page }) => {
+ const vulnerabilitiesTab = await VulnerabilitiesTab.build(
+ page,
+ "keycloak-core",
+ );
+
+ const pagination = await vulnerabilitiesTab.getPagination();
+ const table = await vulnerabilitiesTab.getTable();
+
+ await pagination.validateItemsPerPage("ID", table);
+ });
+});
diff --git a/e2e/tests/ui/pages/package-details/vulnerabilities/sort.spec.ts b/e2e/tests/ui/pages/package-details/vulnerabilities/sort.spec.ts
new file mode 100644
index 000000000..edf653494
--- /dev/null
+++ b/e2e/tests/ui/pages/package-details/vulnerabilities/sort.spec.ts
@@ -0,0 +1,30 @@
+// @ts-check
+
+import { test } from "../../../fixtures";
+import { login } from "../../../helpers/Auth";
+import { VulnerabilitiesTab } from "./VulnerabilitiesTab";
+import { expectSort } from "../../Helpers";
+
+test.describe("Sort validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Sort", async ({ page }) => {
+ const vulnerabilitiesTab = await VulnerabilitiesTab.build(
+ page,
+ "keycloak-core",
+ );
+ const table = await vulnerabilitiesTab.getTable();
+
+ const columnNameSelector = table._table.locator(`td[data-label="ID"]`);
+
+ const ascList = await columnNameSelector.allInnerTexts();
+ expectSort(ascList, true);
+
+ // Reverse sorting
+ await table.clickSortBy("ID");
+ const descList = await columnNameSelector.allInnerTexts();
+ expectSort(descList, false);
+ });
+});
diff --git a/e2e/tests/ui/pages/package-list/PackageListPage.ts b/e2e/tests/ui/pages/package-list/PackageListPage.ts
new file mode 100644
index 000000000..705ca07e9
--- /dev/null
+++ b/e2e/tests/ui/pages/package-list/PackageListPage.ts
@@ -0,0 +1,35 @@
+import type { Page } from "@playwright/test";
+import { Navigation } from "../Navigation";
+import { Toolbar } from "../Toolbar";
+import { Table } from "../Table";
+import { Pagination } from "../Pagination";
+
+export class PackageListPage {
+ private readonly _page: Page;
+
+ private constructor(page: Page) {
+ this._page = page;
+ }
+
+ static async build(page: Page) {
+ const navigation = await Navigation.build(page);
+ await navigation.goToSidebar("Packages");
+
+ return new PackageListPage(page);
+ }
+
+ async getToolbar() {
+ return await Toolbar.build(this._page, "package-toolbar");
+ }
+
+ async getTable() {
+ return await Table.build(this._page, "Package table");
+ }
+
+ async getPagination(top: boolean = true) {
+ return await Pagination.build(
+ this._page,
+ `package-table-pagination-${top ? "top" : "bottom"}`,
+ );
+ }
+}
diff --git a/e2e/tests/ui/pages/package-list/columns.spec.ts b/e2e/tests/ui/pages/package-list/columns.spec.ts
new file mode 100644
index 000000000..12feea2de
--- /dev/null
+++ b/e2e/tests/ui/pages/package-list/columns.spec.ts
@@ -0,0 +1,60 @@
+// @ts-check
+
+import { expect } from "@playwright/test";
+
+import { test } from "../../fixtures";
+import { login } from "../../helpers/Auth";
+import { PackageListPage } from "./PackageListPage";
+
+test.describe("Columns validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Columns", async ({ page }) => {
+ const listPage = await PackageListPage.build(page);
+
+ const toolbar = await listPage.getToolbar();
+ const table = await listPage.getTable();
+
+ // Full search
+ await toolbar.applyTextFilter("Filter text", "keycloak-core");
+ await table.waitUntilDataIsLoaded();
+ await table.verifyColumnContainsText("Name", "keycloak-core");
+
+ // Namespace
+ await expect(
+ table._table.locator(`td[data-label="Namespace"]`),
+ ).toContainText("org.keycloak");
+
+ // Version
+ await expect(
+ table._table.locator(`td[data-label="Version"]`),
+ ).toContainText("18.0.6.redhat-00001");
+
+ // Type
+ await expect(table._table.locator(`td[data-label="Type"]`)).toContainText(
+ "maven",
+ );
+
+ // Qualifiers
+ await expect(
+ table._table.locator(`td[data-label="Qualifiers"]`),
+ ).toContainText("type=jar");
+ await expect(
+ table._table.locator(`td[data-label="Qualifiers"]`),
+ ).toContainText("repository_url=https://maven.repository.redhat.com/ga/");
+
+ // Vulnerabilities
+ await expect(
+ table._table
+ .locator(`td[data-label="Vulnerabilities"]`)
+ .locator("div[aria-label='total']"),
+ ).toContainText("1");
+ await expect(
+ table._table
+ .locator(`td[data-label="Vulnerabilities"]`)
+ .locator("div[aria-label='medium']"),
+ ).toContainText("1");
+ });
+});
diff --git a/e2e/tests/ui/pages/package-list/filter.spec.ts b/e2e/tests/ui/pages/package-list/filter.spec.ts
new file mode 100644
index 000000000..332f4e94a
--- /dev/null
+++ b/e2e/tests/ui/pages/package-list/filter.spec.ts
@@ -0,0 +1,33 @@
+// @ts-check
+
+import { test } from "../../fixtures";
+import { login } from "../../helpers/Auth";
+import { PackageListPage } from "./PackageListPage";
+
+test.describe("Filter validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Filters", async ({ page }) => {
+ const listPage = await PackageListPage.build(page);
+
+ const toolbar = await listPage.getToolbar();
+ const table = await listPage.getTable();
+
+ // Full search
+ await toolbar.applyTextFilter("Filter text", "keycloak-core");
+ await table.waitUntilDataIsLoaded();
+ await table.verifyColumnContainsText("Name", "keycloak-core");
+
+ // Type filter
+ await toolbar.applyMultiSelectFilter("Type", ["Maven", "RPM"]);
+ await table.waitUntilDataIsLoaded();
+ await table.verifyColumnContainsText("Name", "keycloak-core");
+
+ // Architecture
+ await toolbar.applyMultiSelectFilter("Architecture", ["S390", "No Arch"]);
+ await table.waitUntilDataIsLoaded();
+ await table.verifyTableHasNoData();
+ });
+});
diff --git a/e2e/tests/ui/pages/package-list/pagination.spec.ts b/e2e/tests/ui/pages/package-list/pagination.spec.ts
new file mode 100644
index 000000000..131689590
--- /dev/null
+++ b/e2e/tests/ui/pages/package-list/pagination.spec.ts
@@ -0,0 +1,27 @@
+// @ts-check
+
+import { test } from "../../fixtures";
+import { login } from "../../helpers/Auth";
+import { PackageListPage } from "./PackageListPage";
+
+test.describe("Pagination validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Navigation button validations", async ({ page }) => {
+ const listPage = await PackageListPage.build(page);
+ const pagination = await listPage.getPagination();
+
+ await pagination.validatePagination();
+ });
+
+ test("Items per page validations", async ({ page }) => {
+ const listPage = await PackageListPage.build(page);
+
+ const pagination = await listPage.getPagination();
+ const table = await listPage.getTable();
+
+ await pagination.validateItemsPerPage("Name", table);
+ });
+});
diff --git a/e2e/tests/ui/pages/package-list/sort.spec.ts b/e2e/tests/ui/pages/package-list/sort.spec.ts
new file mode 100644
index 000000000..c685b775d
--- /dev/null
+++ b/e2e/tests/ui/pages/package-list/sort.spec.ts
@@ -0,0 +1,28 @@
+// @ts-check
+
+import { test } from "../../fixtures";
+import { login } from "../../helpers/Auth";
+import { expectSort } from "../Helpers";
+import { PackageListPage } from "./PackageListPage";
+
+test.describe("Sort validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Sort", async ({ page }) => {
+ const listPage = await PackageListPage.build(page);
+ const table = await listPage.getTable();
+
+ const columnNameSelector = table._table.locator(`td[data-label="ID"]`);
+
+ // ID Asc
+ const ascList = await columnNameSelector.allInnerTexts();
+ expectSort(ascList, true);
+
+ // ID Desc
+ await table.clickSortBy("Name");
+ const descList = await columnNameSelector.allInnerTexts();
+ expectSort(descList, false);
+ });
+});
diff --git a/e2e/tests/ui/pages/sbom-details/SbomDetailsPage.ts b/e2e/tests/ui/pages/sbom-details/SbomDetailsPage.ts
new file mode 100644
index 000000000..029cc5769
--- /dev/null
+++ b/e2e/tests/ui/pages/sbom-details/SbomDetailsPage.ts
@@ -0,0 +1,32 @@
+import type { Page } from "@playwright/test";
+import { DetailsPageLayout } from "../DetailsPageLayout";
+import { Navigation } from "../Navigation";
+import { SbomListPage } from "../sbom-list/SbomListPage";
+
+export class SbomDetailsPage {
+ _layout: DetailsPageLayout;
+
+ private constructor(_page: Page, layout: DetailsPageLayout) {
+ this._layout = layout;
+ }
+
+ static async build(page: Page, sbomName: string) {
+ const navigation = await Navigation.build(page);
+ await navigation.goToSidebar("SBOMs");
+
+ const listPage = await SbomListPage.build(page);
+ const toolbar = await listPage.getToolbar();
+ const table = await listPage.getTable();
+
+ await toolbar.applyTextFilter("Filter text", sbomName);
+ await table.waitUntilDataIsLoaded();
+ await table.verifyColumnContainsText("Name", sbomName);
+
+ await page.getByRole("link", { name: sbomName, exact: true }).click();
+
+ const layout = await DetailsPageLayout.build(page);
+ await layout.verifyPageHeader(sbomName);
+
+ return new SbomDetailsPage(page, layout);
+ }
+}
diff --git a/e2e/tests/ui/pages/sbom-details/actions.spec.ts b/e2e/tests/ui/pages/sbom-details/actions.spec.ts
new file mode 100644
index 000000000..45961dc5c
--- /dev/null
+++ b/e2e/tests/ui/pages/sbom-details/actions.spec.ts
@@ -0,0 +1,33 @@
+// @ts-check
+
+import { expect } from "@playwright/test";
+
+import { test } from "../../fixtures";
+import { login } from "../../helpers/Auth";
+import { SbomDetailsPage } from "./SbomDetailsPage";
+
+test.describe("Action validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Actions - Download SBOM", async ({ page }) => {
+ const detailsPage = await SbomDetailsPage.build(page, "quarkus-bom");
+
+ const downloadPromise = page.waitForEvent("download");
+ await detailsPage._layout.clickOnPageAction("Download SBOM");
+ const download = await downloadPromise;
+ const filename = download.suggestedFilename();
+ expect(filename).toEqual("quarkus-bom.json");
+ });
+
+ test("Actions - Download License Report", async ({ page }) => {
+ const detailsPage = await SbomDetailsPage.build(page, "quarkus-bom");
+
+ const downloadPromise = page.waitForEvent("download");
+ await detailsPage._layout.clickOnPageAction("Download License Report");
+ const download = await downloadPromise;
+ const filename = download.suggestedFilename();
+ expect(filename).toContain("quarkus-bom");
+ });
+});
diff --git a/e2e/tests/ui/pages/sbom-details/info/info.spec.ts b/e2e/tests/ui/pages/sbom-details/info/info.spec.ts
new file mode 100644
index 000000000..372de55a7
--- /dev/null
+++ b/e2e/tests/ui/pages/sbom-details/info/info.spec.ts
@@ -0,0 +1,45 @@
+// @ts-check
+
+import { expect } from "@playwright/test";
+
+import { test } from "../../../fixtures";
+import { login } from "../../../helpers/Auth";
+import { SbomDetailsPage } from "../SbomDetailsPage";
+import { LabelsModal } from "../../LabelsModal";
+
+test.describe("Info Tab validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Labels", async ({ page }) => {
+ await SbomDetailsPage.build(page, "quarkus-bom");
+ const labels = ["color=red", "production"];
+
+ // Open Edit Labels Modal
+ await page.getByRole("button", { name: "Edit" }).click();
+
+ let labelsModal = await LabelsModal.build(page);
+ await labelsModal.addLabels(labels);
+ await labelsModal.clickSave();
+
+ for (const label of labels) {
+ await expect(
+ page.locator(".pf-v6-c-label-group__list-item", { hasText: label }),
+ ).toHaveCount(1);
+ }
+
+ // Clean labels added previously
+ await page.getByRole("button", { name: "Edit" }).click();
+
+ labelsModal = await LabelsModal.build(page);
+ await labelsModal.removeLabels(labels);
+ await labelsModal.clickSave();
+
+ for (const label of labels) {
+ await expect(
+ page.locator(".pf-v6-c-label-group__list-item", { hasText: label }),
+ ).toHaveCount(0);
+ }
+ });
+});
diff --git a/e2e/tests/ui/pages/sbom-details/packages/PackagesTab.ts b/e2e/tests/ui/pages/sbom-details/packages/PackagesTab.ts
new file mode 100644
index 000000000..26198a7bd
--- /dev/null
+++ b/e2e/tests/ui/pages/sbom-details/packages/PackagesTab.ts
@@ -0,0 +1,37 @@
+import type { Page } from "@playwright/test";
+import { SbomDetailsPage } from "../SbomDetailsPage";
+import { Toolbar } from "../../Toolbar";
+import { Table } from "../../Table";
+import { Pagination } from "../../Pagination";
+
+export class PackagesTab {
+ private readonly _page: Page;
+ _detailsPage: SbomDetailsPage;
+
+ private constructor(page: Page, layout: SbomDetailsPage) {
+ this._page = page;
+ this._detailsPage = layout;
+ }
+
+ static async build(page: Page, sbomName: string) {
+ const detailsPage = await SbomDetailsPage.build(page, sbomName);
+ await detailsPage._layout.selectTab("Packages");
+
+ return new PackagesTab(page, detailsPage);
+ }
+
+ async getToolbar() {
+ return await Toolbar.build(this._page, "Package toolbar");
+ }
+
+ async getTable() {
+ return await Table.build(this._page, "Package table");
+ }
+
+ async getPagination(top: boolean = true) {
+ return await Pagination.build(
+ this._page,
+ `package-table-pagination-${top ? "top" : "bottom"}`,
+ );
+ }
+}
diff --git a/e2e/tests/ui/pages/sbom-details/packages/columns.spec.ts b/e2e/tests/ui/pages/sbom-details/packages/columns.spec.ts
new file mode 100644
index 000000000..e2b21f3bb
--- /dev/null
+++ b/e2e/tests/ui/pages/sbom-details/packages/columns.spec.ts
@@ -0,0 +1,72 @@
+// @ts-check
+
+import { expect } from "@playwright/test";
+
+import { test } from "../../../fixtures";
+import { login } from "../../../helpers/Auth";
+import { PackagesTab } from "./PackagesTab";
+
+test.describe("Columns validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Columns", async ({ page }) => {
+ const packageTab = await PackagesTab.build(page, "quarkus-bom");
+
+ const toolbar = await packageTab.getToolbar();
+ const table = await packageTab.getTable();
+
+ // Full search
+ await toolbar.applyTextFilter("Filter text", "commons-compress");
+ await table.waitUntilDataIsLoaded();
+ await table.verifyColumnContainsText("Name", "commons-compress");
+
+ // Name
+ await expect(table._table.locator(`td[data-label="Name"]`)).toContainText(
+ "commons-compress",
+ );
+
+ // Version
+ await expect(
+ table._table.locator(`td[data-label="Version"]`),
+ ).toContainText("1.21.0.redhat-00001");
+
+ // Vulnerabilities
+ await expect(
+ table._table
+ .locator(`td[data-label="Vulnerabilities"]`)
+ .locator("div[aria-label='total']"),
+ ).toContainText("1");
+
+ // Licenses
+ await expect(
+ table._table.locator(`td[data-label="Licenses"]`),
+ ).toContainText("2 Licenses");
+
+ await table._table.locator(`td[data-label="Licenses"]`).click();
+
+ await expect(
+ table._table
+ .locator(`td[data-label="Licenses"]`)
+ .nth(1)
+ .locator("ul > li", { hasText: "Apache-2.0" }),
+ ).toBeVisible();
+ await expect(
+ table._table
+ .locator(`td[data-label="Licenses"]`)
+ .nth(1)
+ .locator("ul > li", { hasText: "NOASSERTION" }),
+ ).toBeVisible();
+
+ // PURL
+ await expect(table._table.locator(`td[data-label="PURLs"]`)).toContainText(
+ "pkg:maven/org.apache.commons/commons-compress@1.21.0.redhat-00001?repository_url=https://maven.repository.redhat.com/ga/&type=jar",
+ );
+
+ // CPE
+ await expect(table._table.locator(`td[data-label="CPEs"]`)).toContainText(
+ "0 CPEs",
+ );
+ });
+});
diff --git a/e2e/tests/ui/pages/sbom-details/packages/filter.spec.ts b/e2e/tests/ui/pages/sbom-details/packages/filter.spec.ts
new file mode 100644
index 000000000..f26ee5758
--- /dev/null
+++ b/e2e/tests/ui/pages/sbom-details/packages/filter.spec.ts
@@ -0,0 +1,31 @@
+// @ts-check
+
+import { test } from "../../../fixtures";
+import { login } from "../../../helpers/Auth";
+import { PackagesTab } from "./PackagesTab";
+
+test.describe("Filter validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Filters", async ({ page }) => {
+ const packageTab = await PackagesTab.build(page, "quarkus-bom");
+
+ const toolbar = await packageTab.getToolbar();
+ const table = await packageTab.getTable();
+
+ // Full search
+ await toolbar.applyTextFilter("Filter text", "commons-compress");
+ await table.waitUntilDataIsLoaded();
+ await table.verifyColumnContainsText("Name", "commons-compress");
+
+ // Labels filter
+ await toolbar.applyMultiSelectFilter("License", [
+ "Apache-2.0",
+ "NOASSERTION",
+ ]);
+ await table.waitUntilDataIsLoaded();
+ await table.verifyColumnContainsText("Name", "commons-compress");
+ });
+});
diff --git a/e2e/tests/ui/pages/sbom-details/packages/pagination.spec.ts b/e2e/tests/ui/pages/sbom-details/packages/pagination.spec.ts
new file mode 100644
index 000000000..fd56de05b
--- /dev/null
+++ b/e2e/tests/ui/pages/sbom-details/packages/pagination.spec.ts
@@ -0,0 +1,27 @@
+// @ts-check
+
+import { test } from "../../../fixtures";
+import { login } from "../../../helpers/Auth";
+import { PackagesTab } from "./PackagesTab";
+
+test.describe("Pagination validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Navigation button validations", async ({ page }) => {
+ const packageTab = await PackagesTab.build(page, "quarkus-bom");
+ const pagination = await packageTab.getPagination();
+
+ await pagination.validatePagination();
+ });
+
+ test("Items per page validations", async ({ page }) => {
+ const packageTab = await PackagesTab.build(page, "quarkus-bom");
+
+ const pagination = await packageTab.getPagination();
+ const table = await packageTab.getTable();
+
+ await pagination.validateItemsPerPage("Name", table);
+ });
+});
diff --git a/e2e/tests/ui/pages/sbom-details/packages/sort.spec.ts b/e2e/tests/ui/pages/sbom-details/packages/sort.spec.ts
new file mode 100644
index 000000000..d7574f164
--- /dev/null
+++ b/e2e/tests/ui/pages/sbom-details/packages/sort.spec.ts
@@ -0,0 +1,27 @@
+// @ts-check
+
+import { test } from "../../../fixtures";
+import { login } from "../../../helpers/Auth";
+import { PackagesTab } from "./PackagesTab";
+import { expectSort } from "../../Helpers";
+
+test.describe("Sort validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Sort", async ({ page }) => {
+ const packageTab = await PackagesTab.build(page, "quarkus-bom");
+ const table = await packageTab.getTable();
+
+ const columnNameSelector = table._table.locator(`td[data-label="Name"]`);
+
+ const ascList = await columnNameSelector.allInnerTexts();
+ expectSort(ascList, true);
+
+ // Reverse sorting
+ await table.clickSortBy("Name");
+ const descList = await columnNameSelector.allInnerTexts();
+ expectSort(descList, false);
+ });
+});
diff --git a/e2e/tests/ui/pages/sbom-details/vulnerabilities/VulnerabilitiesTab.ts b/e2e/tests/ui/pages/sbom-details/vulnerabilities/VulnerabilitiesTab.ts
new file mode 100644
index 000000000..c1e9a8ec4
--- /dev/null
+++ b/e2e/tests/ui/pages/sbom-details/vulnerabilities/VulnerabilitiesTab.ts
@@ -0,0 +1,37 @@
+import type { Page } from "@playwright/test";
+import { SbomDetailsPage } from "../SbomDetailsPage";
+import { Toolbar } from "../../Toolbar";
+import { Table } from "../../Table";
+import { Pagination } from "../../Pagination";
+
+export class VulnerabilitiesTab {
+ private readonly _page: Page;
+ _detailsPage: SbomDetailsPage;
+
+ private constructor(page: Page, layout: SbomDetailsPage) {
+ this._page = page;
+ this._detailsPage = layout;
+ }
+
+ static async build(page: Page, sbomName: string) {
+ const detailsPage = await SbomDetailsPage.build(page, sbomName);
+ await detailsPage._layout.selectTab("Vulnerabilities");
+
+ return new VulnerabilitiesTab(page, detailsPage);
+ }
+
+ async getToolbar() {
+ return await Toolbar.build(this._page, "Vulnerability toolbar");
+ }
+
+ async getTable() {
+ return await Table.build(this._page, "Vulnerability table");
+ }
+
+ async getPagination(top: boolean = true) {
+ return await Pagination.build(
+ this._page,
+ `vulnerability-table-pagination-${top ? "top" : "bottom"}`,
+ );
+ }
+}
diff --git a/e2e/tests/ui/pages/sbom-details/vulnerabilities/columns.spec.ts b/e2e/tests/ui/pages/sbom-details/vulnerabilities/columns.spec.ts
new file mode 100644
index 000000000..a32a4c825
--- /dev/null
+++ b/e2e/tests/ui/pages/sbom-details/vulnerabilities/columns.spec.ts
@@ -0,0 +1,71 @@
+// @ts-check
+
+import { expect } from "@playwright/test";
+
+import { test } from "../../../fixtures";
+import { login } from "../../../helpers/Auth";
+import { VulnerabilitiesTab } from "./VulnerabilitiesTab";
+
+test.describe("Columns validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Columns", async ({ page }) => {
+ const vulnerabilityTab = await VulnerabilitiesTab.build(
+ page,
+ "quarkus-bom",
+ );
+
+ const table = await vulnerabilityTab.getTable();
+
+ const ids = await table._table
+ .locator(`td[data-label="Id"]`)
+ .allInnerTexts();
+ const idIndex = ids.indexOf("CVE-2023-4853");
+ expect(idIndex).not.toBe(-1);
+
+ // Name
+ await expect(
+ table._table.locator(`td[data-label="Id"]`).nth(idIndex),
+ ).toContainText("CVE-2023-4853");
+
+ // Description
+ await expect(
+ table._table.locator(`td[data-label="Description"]`).nth(idIndex),
+ ).toContainText("quarkus: HTTP security policy bypass");
+
+ // Vulnerabilities
+ await expect(
+ table._table.locator(`td[data-label="CVSS"]`).nth(idIndex),
+ ).toContainText("High(8.1)");
+
+ // Affected dependencies
+ await expect(
+ table._table
+ .locator(`td[data-label="Affected dependencies"]`)
+ .nth(idIndex),
+ ).toContainText("3");
+
+ await table._table
+ .locator(`td[data-label="Affected dependencies"]`)
+ .nth(idIndex)
+ .click();
+
+ await expect(
+ table._table
+ .locator(`td[data-label="Affected dependencies"]`)
+ .nth(idIndex + 1),
+ ).toContainText("quarkus-undertow");
+ await expect(
+ table._table
+ .locator(`td[data-label="Affected dependencies"]`)
+ .nth(idIndex + 1),
+ ).toContainText("quarkus-keycloak-authorization");
+ await expect(
+ table._table
+ .locator(`td[data-label="Affected dependencies"]`)
+ .nth(idIndex + 1),
+ ).toContainText("quarkus-vertx-http");
+ });
+});
diff --git a/e2e/tests/ui/pages/sbom-details/vulnerabilities/donutchart.spec.ts b/e2e/tests/ui/pages/sbom-details/vulnerabilities/donutchart.spec.ts
new file mode 100644
index 000000000..4922cb46c
--- /dev/null
+++ b/e2e/tests/ui/pages/sbom-details/vulnerabilities/donutchart.spec.ts
@@ -0,0 +1,24 @@
+// @ts-check
+
+import { expect } from "@playwright/test";
+
+import { test } from "../../../fixtures";
+import { login } from "../../../helpers/Auth";
+import { VulnerabilitiesTab } from "./VulnerabilitiesTab";
+
+test.describe("DonutChart validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Vulnerabilities", async ({ page }) => {
+ await VulnerabilitiesTab.build(page, "quarkus-bom");
+
+ await expect(page.locator("#legend-labels-0")).toContainText("Critical: 0");
+ await expect(page.locator("#legend-labels-1")).toContainText("High: 1");
+ await expect(page.locator("#legend-labels-2")).toContainText("Medium: 10");
+ await expect(page.locator("#legend-labels-3")).toContainText("Low: 0");
+ await expect(page.locator("#legend-labels-4")).toContainText("None: 0");
+ await expect(page.locator("#legend-labels-5")).toContainText("Unknown: 0");
+ });
+});
diff --git a/e2e/tests/ui/pages/sbom-details/vulnerabilities/filter.spec.ts b/e2e/tests/ui/pages/sbom-details/vulnerabilities/filter.spec.ts
new file mode 100644
index 000000000..e785a06f5
--- /dev/null
+++ b/e2e/tests/ui/pages/sbom-details/vulnerabilities/filter.spec.ts
@@ -0,0 +1,32 @@
+// @ts-check
+
+import { test } from "../../../fixtures";
+import { login } from "../../../helpers/Auth";
+import { VulnerabilitiesTab } from "./VulnerabilitiesTab";
+
+// Table has no filters
+test.describe.skip("Filter validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test.skip("Filters", async ({ page }) => {
+ const vulnerabilityTab = await VulnerabilitiesTab.build(
+ page,
+ "quarkus-bom",
+ );
+
+ const toolbar = await vulnerabilityTab.getToolbar();
+ const table = await vulnerabilityTab.getTable();
+
+ // Full search
+ await toolbar.applyTextFilter("Filter text", "CVE-2023-4853");
+ await table.waitUntilDataIsLoaded();
+ await table.verifyColumnContainsText("Id", "CVE-2023-4853");
+
+ // Labels filter
+ await toolbar.applyMultiSelectFilter("Severity", ["High"]);
+ await table.waitUntilDataIsLoaded();
+ await table.verifyColumnContainsText("Id", "CVE-2023-4853");
+ });
+});
diff --git a/e2e/tests/ui/pages/sbom-details/vulnerabilities/pagination.spec.ts b/e2e/tests/ui/pages/sbom-details/vulnerabilities/pagination.spec.ts
new file mode 100644
index 000000000..3d2460a00
--- /dev/null
+++ b/e2e/tests/ui/pages/sbom-details/vulnerabilities/pagination.spec.ts
@@ -0,0 +1,30 @@
+// @ts-check
+
+import { test } from "../../../fixtures";
+import { login } from "../../../helpers/Auth";
+import { VulnerabilitiesTab } from "./VulnerabilitiesTab";
+
+test.describe("Pagination validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Navigation button validations", async ({ page }) => {
+ const vulnerabilityTab = await VulnerabilitiesTab.build(
+ page,
+ "quarkus-bom",
+ );
+ const pagination = await vulnerabilityTab.getPagination();
+
+ await pagination.validatePagination();
+ });
+
+ test("Items per page validations", async ({ page }) => {
+ const packageTab = await VulnerabilitiesTab.build(page, "quarkus-bom");
+
+ const pagination = await packageTab.getPagination();
+ const table = await packageTab.getTable();
+
+ await pagination.validateItemsPerPage("Id", table);
+ });
+});
diff --git a/e2e/tests/ui/pages/sbom-details/vulnerabilities/sort.spec.ts b/e2e/tests/ui/pages/sbom-details/vulnerabilities/sort.spec.ts
new file mode 100644
index 000000000..adee61606
--- /dev/null
+++ b/e2e/tests/ui/pages/sbom-details/vulnerabilities/sort.spec.ts
@@ -0,0 +1,30 @@
+// @ts-check
+
+import { test } from "../../../fixtures";
+import { login } from "../../../helpers/Auth";
+import { VulnerabilitiesTab } from "./VulnerabilitiesTab";
+import { expectSort } from "../../Helpers";
+
+test.describe("Sort validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Sort", async ({ page }) => {
+ const vulnerabilityTab = await VulnerabilitiesTab.build(
+ page,
+ "quarkus-bom",
+ );
+ const table = await vulnerabilityTab.getTable();
+
+ const columnNameSelector = table._table.locator(`td[data-label="Id"]`);
+
+ const ascList = await columnNameSelector.allInnerTexts();
+ expectSort(ascList, true);
+
+ // Reverse sorting
+ await table.clickSortBy("Id");
+ const descList = await columnNameSelector.allInnerTexts();
+ expectSort(descList, false);
+ });
+});
diff --git a/e2e/tests/ui/pages/sbom-list/SbomListPage.ts b/e2e/tests/ui/pages/sbom-list/SbomListPage.ts
new file mode 100644
index 000000000..fd9fd6a8c
--- /dev/null
+++ b/e2e/tests/ui/pages/sbom-list/SbomListPage.ts
@@ -0,0 +1,35 @@
+import type { Page } from "@playwright/test";
+import { Navigation } from "../Navigation";
+import { Toolbar } from "../Toolbar";
+import { Table } from "../Table";
+import { Pagination } from "../Pagination";
+
+export class SbomListPage {
+ private readonly _page: Page;
+
+ private constructor(page: Page) {
+ this._page = page;
+ }
+
+ static async build(page: Page) {
+ const navigation = await Navigation.build(page);
+ await navigation.goToSidebar("SBOMs");
+
+ return new SbomListPage(page);
+ }
+
+ async getToolbar() {
+ return await Toolbar.build(this._page, "sbom-toolbar");
+ }
+
+ async getTable() {
+ return await Table.build(this._page, "sbom-table");
+ }
+
+ async getPagination(top: boolean = true) {
+ return await Pagination.build(
+ this._page,
+ `sbom-table-pagination-${top ? "top" : "bottom"}`,
+ );
+ }
+}
diff --git a/e2e/tests/ui/pages/sbom-list/actions.spec.ts b/e2e/tests/ui/pages/sbom-list/actions.spec.ts
new file mode 100644
index 000000000..33a5c8c0a
--- /dev/null
+++ b/e2e/tests/ui/pages/sbom-list/actions.spec.ts
@@ -0,0 +1,86 @@
+// @ts-check
+
+import { expect } from "@playwright/test";
+
+import { test } from "../../fixtures";
+import { login } from "../../helpers/Auth";
+import { LabelsModal } from "../LabelsModal";
+import { SbomListPage } from "./SbomListPage";
+
+test.describe("Action validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Actions - Download SBOM", async ({ page }) => {
+ const listPage = await SbomListPage.build(page);
+ const table = await listPage.getTable();
+
+ const sbomNames = await table._table
+ .locator(`td[data-label="Name"]`)
+ .allInnerTexts();
+
+ const downloadPromise = page.waitForEvent("download");
+ await table.clickAction("Download SBOM", 0);
+ const download = await downloadPromise;
+ const filename = download.suggestedFilename();
+ expect(filename).toEqual(`${sbomNames[0]}.json`);
+ });
+
+ test("Actions - Download License Report", async ({ page }) => {
+ const listPage = await SbomListPage.build(page);
+ const table = await listPage.getTable();
+
+ const sbomNames = await table._table
+ .locator(`td[data-label="Name"]`)
+ .allInnerTexts();
+
+ const downloadPromise = page.waitForEvent("download");
+ await table.clickAction("Download License Report", 0);
+ const download = await downloadPromise;
+ const filename = download.suggestedFilename();
+ expect(filename).toContain(sbomNames[0]);
+ });
+
+ test("Actions - Edit Labels", async ({ page }) => {
+ const listPage = await SbomListPage.build(page);
+ const table = await listPage.getTable();
+
+ const labels = ["color=red", "production"];
+ await table.clickAction("Edit labels", 0);
+
+ let labelsModal = await LabelsModal.build(page);
+
+ // Add labels
+ await labelsModal.addLabels(labels);
+ await labelsModal.clickSave();
+
+ // Verify labels were added
+ await table.waitUntilDataIsLoaded();
+ for (const label of labels) {
+ await expect(
+ table._table
+ .locator(`td[data-label="Labels"]`)
+ .first()
+ .locator(".pf-v6-c-label", { hasText: label }),
+ ).toHaveCount(1);
+ }
+
+ // Clean labels added previously
+ await table.clickAction("Edit labels", 0);
+
+ labelsModal = await LabelsModal.build(page);
+ await labelsModal.removeLabels(labels);
+ await labelsModal.clickSave();
+
+ await table.waitUntilDataIsLoaded();
+ for (const label of labels) {
+ await expect(
+ table._table
+ .locator(`td[data-label="Labels"]`)
+ .first()
+ .locator(".pf-v6-c-label", { hasText: label }),
+ ).toHaveCount(0);
+ }
+ });
+});
diff --git a/e2e/tests/ui/pages/sbom-list/columns.spec.ts b/e2e/tests/ui/pages/sbom-list/columns.spec.ts
new file mode 100644
index 000000000..7fe5780ad
--- /dev/null
+++ b/e2e/tests/ui/pages/sbom-list/columns.spec.ts
@@ -0,0 +1,54 @@
+// @ts-check
+
+import { expect } from "@playwright/test";
+
+import { test } from "../../fixtures";
+import { login } from "../../helpers/Auth";
+import { SbomListPage } from "./SbomListPage";
+
+test.describe("Columns validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Vulnerabilities", async ({ page }) => {
+ const listPage = await SbomListPage.build(page);
+
+ const toolbar = await listPage.getToolbar();
+ const table = await listPage.getTable();
+
+ // Full search
+ await toolbar.applyTextFilter("Filter text", "quarkus-bom");
+ await table.waitUntilDataIsLoaded();
+ await table.verifyColumnContainsText("Name", "quarkus-bom");
+
+ // Total Vulnerabilities
+ await expect(
+ table._table
+ .locator(`td[data-label="Vulnerabilities"]`)
+ .locator("div[aria-label='total']", { hasText: "11" }),
+ ).toHaveCount(1);
+
+ // Severities
+ const expectedVulnerabilities = [
+ {
+ severity: "high",
+ count: 1,
+ },
+ {
+ severity: "medium",
+ count: 10,
+ },
+ ];
+
+ for (const expectedVulnerability of expectedVulnerabilities) {
+ await expect(
+ table._table
+ .locator(`td[data-label="Vulnerabilities"]`)
+ .locator(`div[aria-label="${expectedVulnerability.severity}"]`, {
+ hasText: expectedVulnerability.count.toString(),
+ }),
+ ).toHaveCount(1);
+ }
+ });
+});
diff --git a/e2e/tests/ui/pages/sbom-list/filter.spec.ts b/e2e/tests/ui/pages/sbom-list/filter.spec.ts
new file mode 100644
index 000000000..7fa0c4564
--- /dev/null
+++ b/e2e/tests/ui/pages/sbom-list/filter.spec.ts
@@ -0,0 +1,37 @@
+// @ts-check
+
+import { test } from "../../fixtures";
+import { login } from "../../helpers/Auth";
+import { SbomListPage } from "./SbomListPage";
+
+test.describe("Filter validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Filters", async ({ page }) => {
+ const listPage = await SbomListPage.build(page);
+
+ const toolbar = await listPage.getToolbar();
+ const table = await listPage.getTable();
+
+ // Full search
+ await toolbar.applyTextFilter("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 table.waitUntilDataIsLoaded();
+ await table.verifyColumnContainsText("Name", "quarkus-bom");
+
+ // Labels filter
+ await toolbar.applyLabelsFilter("Label", ["type=spdx"]);
+ await table.waitUntilDataIsLoaded();
+ await table.verifyColumnContainsText("Name", "quarkus-bom");
+ });
+});
diff --git a/e2e/tests/ui/pages/sbom-list/pagination.spec.ts b/e2e/tests/ui/pages/sbom-list/pagination.spec.ts
new file mode 100644
index 000000000..be9174b0e
--- /dev/null
+++ b/e2e/tests/ui/pages/sbom-list/pagination.spec.ts
@@ -0,0 +1,27 @@
+// @ts-check
+
+import { test } from "../../fixtures";
+import { login } from "../../helpers/Auth";
+import { SbomListPage } from "./SbomListPage";
+
+test.describe("Pagination validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Navigation button validations", async ({ page }) => {
+ const listPage = await SbomListPage.build(page);
+ const pagination = await listPage.getPagination();
+
+ await pagination.validatePagination();
+ });
+
+ test("Items per page validations", async ({ page }) => {
+ const listPage = await SbomListPage.build(page);
+
+ const pagination = await listPage.getPagination();
+ const table = await listPage.getTable();
+
+ await pagination.validateItemsPerPage("Name", table);
+ });
+});
diff --git a/e2e/tests/ui/pages/sbom-list/sort.spec.ts b/e2e/tests/ui/pages/sbom-list/sort.spec.ts
new file mode 100644
index 000000000..555b7c92a
--- /dev/null
+++ b/e2e/tests/ui/pages/sbom-list/sort.spec.ts
@@ -0,0 +1,27 @@
+// @ts-check
+
+import { test } from "../../fixtures";
+import { login } from "../../helpers/Auth";
+import { expectSort } from "../Helpers";
+import { SbomListPage } from "./SbomListPage";
+
+test.describe("Sort validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Sort", async ({ page }) => {
+ const listPage = await SbomListPage.build(page);
+ const table = await listPage.getTable();
+
+ const columnNameSelector = table._table.locator(`td[data-label="Name"]`);
+
+ const ascList = await columnNameSelector.allInnerTexts();
+ expectSort(ascList, true);
+
+ // Reverse sorting
+ await table.clickSortBy("Name");
+ const desList = await columnNameSelector.allInnerTexts();
+ expectSort(desList, false);
+ });
+});
diff --git a/e2e/tests/ui/pages/vulnerability-details/VulnerabilityDetailsPage.ts b/e2e/tests/ui/pages/vulnerability-details/VulnerabilityDetailsPage.ts
new file mode 100644
index 000000000..dd30ec034
--- /dev/null
+++ b/e2e/tests/ui/pages/vulnerability-details/VulnerabilityDetailsPage.ts
@@ -0,0 +1,34 @@
+import type { Page } from "@playwright/test";
+import { DetailsPageLayout } from "../DetailsPageLayout";
+import { Navigation } from "../Navigation";
+import { VulnerabilityListPage } from "../vulnerability-list/VulnerabilityListPage";
+
+export class VulnerabilityDetailsPage {
+ _layout: DetailsPageLayout;
+
+ private constructor(_page: Page, layout: DetailsPageLayout) {
+ this._layout = layout;
+ }
+
+ static async build(page: Page, vulnerabilityID: string) {
+ const navigation = await Navigation.build(page);
+ await navigation.goToSidebar("Vulnerabilities");
+
+ const listPage = await VulnerabilityListPage.build(page);
+ const toolbar = await listPage.getToolbar();
+ const table = await listPage.getTable();
+
+ await toolbar.applyTextFilter("Filter text", vulnerabilityID);
+ await table.waitUntilDataIsLoaded();
+ await table.verifyColumnContainsText("ID", vulnerabilityID);
+
+ await page
+ .getByRole("link", { name: vulnerabilityID, exact: true })
+ .click();
+
+ const layout = await DetailsPageLayout.build(page);
+ await layout.verifyPageHeader(vulnerabilityID);
+
+ return new VulnerabilityDetailsPage(page, layout);
+ }
+}
diff --git a/e2e/tests/ui/pages/vulnerability-details/advisories/AdvisoriesTab.ts b/e2e/tests/ui/pages/vulnerability-details/advisories/AdvisoriesTab.ts
new file mode 100644
index 000000000..597625c7a
--- /dev/null
+++ b/e2e/tests/ui/pages/vulnerability-details/advisories/AdvisoriesTab.ts
@@ -0,0 +1,40 @@
+import type { Page } from "@playwright/test";
+import { VulnerabilityDetailsPage } from "../VulnerabilityDetailsPage";
+import { Toolbar } from "../../Toolbar";
+import { Table } from "../../Table";
+import { Pagination } from "../../Pagination";
+
+export class AdvisoriesTab {
+ private readonly _page: Page;
+ _detailsPage: VulnerabilityDetailsPage;
+
+ private constructor(page: Page, layout: VulnerabilityDetailsPage) {
+ this._page = page;
+ this._detailsPage = layout;
+ }
+
+ static async build(page: Page, vulnerabilityID: string) {
+ const detailsPage = await VulnerabilityDetailsPage.build(
+ page,
+ vulnerabilityID,
+ );
+ await detailsPage._layout.selectTab("Related Advisories");
+
+ return new AdvisoriesTab(page, detailsPage);
+ }
+
+ async getToolbar() {
+ return await Toolbar.build(this._page, "Advisory toolbar");
+ }
+
+ async getTable() {
+ return await Table.build(this._page, "Advisory table");
+ }
+
+ async getPagination(top: boolean = true) {
+ return await Pagination.build(
+ this._page,
+ `advisory-table-pagination-${top ? "top" : "bottom"}`,
+ );
+ }
+}
diff --git a/e2e/tests/ui/pages/vulnerability-details/advisories/columns.spec.ts b/e2e/tests/ui/pages/vulnerability-details/advisories/columns.spec.ts
new file mode 100644
index 000000000..9ee0b564a
--- /dev/null
+++ b/e2e/tests/ui/pages/vulnerability-details/advisories/columns.spec.ts
@@ -0,0 +1,49 @@
+// @ts-check
+
+import { expect } from "@playwright/test";
+
+import { test } from "../../../fixtures";
+import { login } from "../../../helpers/Auth";
+import { AdvisoriesTab } from "./AdvisoriesTab";
+
+test.describe("Columns validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Columns", async ({ page }) => {
+ const advisoriesTab = await AdvisoriesTab.build(page, "CVE-2023-2976");
+ const table = await advisoriesTab.getTable();
+
+ const types = await table._table
+ .locator(`td[data-label="Type"]`)
+ .allInnerTexts();
+ const rowIndex = types.indexOf("csaf");
+ expect(rowIndex).not.toBe(-1);
+
+ // Name
+ await expect(
+ table._table.locator(`td[data-label="ID"]`).nth(rowIndex),
+ ).toContainText("CVE-2023-2976");
+
+ // Title
+ await expect(
+ table._table.locator(`td[data-label="Title"]`).nth(rowIndex),
+ ).toContainText("guava: insecure temporary directory creation");
+
+ // Type
+ await expect(
+ table._table.locator(`td[data-label="Type"]`).nth(rowIndex),
+ ).toContainText("csaf");
+
+ // Revision
+ await expect(
+ table._table.locator(`td[data-label="Revision"]`).nth(rowIndex),
+ ).toContainText("Nov 14, 2023");
+
+ // Vulnerabilities
+ await expect(
+ table._table.locator(`td[data-label="Vulnerabilities"]`).nth(rowIndex),
+ ).toContainText("1");
+ });
+});
diff --git a/e2e/tests/ui/pages/vulnerability-details/advisories/pagination.spec.ts b/e2e/tests/ui/pages/vulnerability-details/advisories/pagination.spec.ts
new file mode 100644
index 000000000..6ebd78a94
--- /dev/null
+++ b/e2e/tests/ui/pages/vulnerability-details/advisories/pagination.spec.ts
@@ -0,0 +1,28 @@
+// @ts-check
+
+import { test } from "../../../fixtures";
+import { login } from "../../../helpers/Auth";
+import { AdvisoriesTab } from "./AdvisoriesTab";
+
+// Number of items less than 10, cannot tests pagination
+test.describe.skip("Pagination validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Navigation button validations", async ({ page }) => {
+ const advisoriesTab = await AdvisoriesTab.build(page, "CVE-2023-2976");
+ const pagination = await advisoriesTab.getPagination();
+
+ await pagination.validatePagination();
+ });
+
+ test("Items per page validations", async ({ page }) => {
+ const sbomTab = await AdvisoriesTab.build(page, "CVE-2023-2976");
+
+ const pagination = await sbomTab.getPagination();
+ const table = await sbomTab.getTable();
+
+ await pagination.validateItemsPerPage("ID", table);
+ });
+});
diff --git a/e2e/tests/ui/pages/vulnerability-details/advisories/sort.spec.ts b/e2e/tests/ui/pages/vulnerability-details/advisories/sort.spec.ts
new file mode 100644
index 000000000..1538614e0
--- /dev/null
+++ b/e2e/tests/ui/pages/vulnerability-details/advisories/sort.spec.ts
@@ -0,0 +1,27 @@
+// @ts-check
+
+import { test } from "../../../fixtures";
+import { login } from "../../../helpers/Auth";
+import { AdvisoriesTab } from "./AdvisoriesTab";
+import { expectSort } from "../../Helpers";
+
+test.describe("Sort validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Sort", async ({ page }) => {
+ const advisoryTab = await AdvisoriesTab.build(page, "CVE-2023-2976");
+ const table = await advisoryTab.getTable();
+
+ const columnNameSelector = table._table.locator(`td[data-label="ID"]`);
+
+ const ascList = await columnNameSelector.allInnerTexts();
+ expectSort(ascList, true);
+
+ // Reverse sorting
+ await table.clickSortBy("ID");
+ const descList = await columnNameSelector.allInnerTexts();
+ expectSort(descList, false);
+ });
+});
diff --git a/e2e/tests/ui/pages/vulnerability-details/info/info.spec.ts b/e2e/tests/ui/pages/vulnerability-details/info/info.spec.ts
new file mode 100644
index 000000000..b1773b317
--- /dev/null
+++ b/e2e/tests/ui/pages/vulnerability-details/info/info.spec.ts
@@ -0,0 +1,16 @@
+// @ts-check
+
+import { test } from "../../../fixtures";
+import { login } from "../../../helpers/Auth";
+import { VulnerabilityDetailsPage } from "../VulnerabilityDetailsPage";
+
+test.describe("Info Tab validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Info", async ({ page }) => {
+ await VulnerabilityDetailsPage.build(page, "CVE-2023-2976");
+ // Verify
+ });
+});
diff --git a/e2e/tests/ui/pages/vulnerability-details/sboms/SbomsTab.ts b/e2e/tests/ui/pages/vulnerability-details/sboms/SbomsTab.ts
new file mode 100644
index 000000000..8f21c6465
--- /dev/null
+++ b/e2e/tests/ui/pages/vulnerability-details/sboms/SbomsTab.ts
@@ -0,0 +1,40 @@
+import type { Page } from "@playwright/test";
+import { VulnerabilityDetailsPage } from "../VulnerabilityDetailsPage";
+import { Toolbar } from "../../Toolbar";
+import { Table } from "../../Table";
+import { Pagination } from "../../Pagination";
+
+export class SbomsTab {
+ private readonly _page: Page;
+ _detailsPage: VulnerabilityDetailsPage;
+
+ private constructor(page: Page, layout: VulnerabilityDetailsPage) {
+ this._page = page;
+ this._detailsPage = layout;
+ }
+
+ static async build(page: Page, vulnerabilityID: string) {
+ const detailsPage = await VulnerabilityDetailsPage.build(
+ page,
+ vulnerabilityID,
+ );
+ await detailsPage._layout.selectTab("Related SBOMs");
+
+ return new SbomsTab(page, detailsPage);
+ }
+
+ async getToolbar() {
+ return await Toolbar.build(this._page, "Sbom toolbar");
+ }
+
+ async getTable() {
+ return await Table.build(this._page, "Sbom table");
+ }
+
+ async getPagination(top: boolean = true) {
+ return await Pagination.build(
+ this._page,
+ `sbom-table-pagination-${top ? "top" : "bottom"}`,
+ );
+ }
+}
diff --git a/e2e/tests/ui/pages/vulnerability-details/sboms/columns.spec.ts b/e2e/tests/ui/pages/vulnerability-details/sboms/columns.spec.ts
new file mode 100644
index 000000000..65d7400d6
--- /dev/null
+++ b/e2e/tests/ui/pages/vulnerability-details/sboms/columns.spec.ts
@@ -0,0 +1,69 @@
+// @ts-check
+
+import { expect } from "@playwright/test";
+
+import { test } from "../../../fixtures";
+import { login } from "../../../helpers/Auth";
+import { SbomsTab } from "./SbomsTab";
+
+test.describe("Columns validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Columns", async ({ page }) => {
+ const sbomTab = await SbomsTab.build(page, "CVE-2023-2976");
+ const table = await sbomTab.getTable();
+
+ const ids = await table._table
+ .locator(`td[data-label="Name"]`)
+ .allInnerTexts();
+ const idIndex = ids.indexOf("quarkus-bom");
+ expect(idIndex).not.toBe(-1);
+
+ // Name
+ await expect(
+ table._table.locator(`td[data-label="Name"]`).nth(idIndex),
+ ).toContainText("quarkus-bom");
+
+ // Version
+ await expect(
+ table._table.locator(`td[data-label="Version"]`).nth(idIndex),
+ ).toContainText("2.13.8.Final-redhat-00004");
+
+ // Status
+ await expect(
+ table._table.locator(`td[data-label="Status"]`).nth(idIndex),
+ ).toContainText("Affected");
+
+ // Dependencies
+ await expect(
+ table._table.locator(`td[data-label="Dependencies"]`).nth(idIndex),
+ ).toContainText("1");
+
+ await table._table
+ .locator(`td[data-label="Dependencies"]`)
+ .nth(idIndex)
+ .click();
+
+ await expect(
+ table._table.locator(`td[data-label="Dependencies"]`).nth(idIndex + 1),
+ ).toContainText("com.google.guava");
+ await expect(
+ table._table
+ .locator(`td[data-label="Dependencies"]`)
+ .nth(idIndex + 1)
+ .getByRole("link", { name: "guava" }),
+ ).toHaveCount(1);
+
+ // Status
+ await expect(
+ table._table.locator(`td[data-label="Supplier"]`).nth(idIndex),
+ ).toContainText("Organization: Red Hat");
+
+ // Status
+ await expect(
+ table._table.locator(`td[data-label="Created on"]`).nth(idIndex),
+ ).toContainText("Nov 22, 2023");
+ });
+});
diff --git a/e2e/tests/ui/pages/vulnerability-details/sboms/pagination.spec.ts b/e2e/tests/ui/pages/vulnerability-details/sboms/pagination.spec.ts
new file mode 100644
index 000000000..83a988907
--- /dev/null
+++ b/e2e/tests/ui/pages/vulnerability-details/sboms/pagination.spec.ts
@@ -0,0 +1,28 @@
+// @ts-check
+
+import { test } from "../../../fixtures";
+import { login } from "../../../helpers/Auth";
+import { SbomsTab } from "./SbomsTab";
+
+// Number of items less than 10, cannot tests pagination
+test.describe.skip("Pagination validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Navigation button validations", async ({ page }) => {
+ const sbomTab = await SbomsTab.build(page, "CVE-2023-2976");
+ const pagination = await sbomTab.getPagination();
+
+ await pagination.validatePagination();
+ });
+
+ test("Items per page validations", async ({ page }) => {
+ const sbomTab = await SbomsTab.build(page, "CVE-2023-2976");
+
+ const pagination = await sbomTab.getPagination();
+ const table = await sbomTab.getTable();
+
+ await pagination.validateItemsPerPage("Name", table);
+ });
+});
diff --git a/e2e/tests/ui/pages/vulnerability-details/sboms/sort.spec.ts b/e2e/tests/ui/pages/vulnerability-details/sboms/sort.spec.ts
new file mode 100644
index 000000000..041b09e9c
--- /dev/null
+++ b/e2e/tests/ui/pages/vulnerability-details/sboms/sort.spec.ts
@@ -0,0 +1,27 @@
+// @ts-check
+
+import { test } from "../../../fixtures";
+import { login } from "../../../helpers/Auth";
+import { SbomsTab } from "./SbomsTab";
+import { expectSort } from "../../Helpers";
+
+test.describe("Sort validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Sort", async ({ page }) => {
+ const sbomTab = await SbomsTab.build(page, "CVE-2023-2976");
+ const table = await sbomTab.getTable();
+
+ const columnNameSelector = table._table.locator(`td[data-label="Name"]`);
+
+ const ascList = await columnNameSelector.allInnerTexts();
+ expectSort(ascList, true);
+
+ // Reverse sorting
+ await table.clickSortBy("Name");
+ const descList = await columnNameSelector.allInnerTexts();
+ expectSort(descList, false);
+ });
+});
diff --git a/e2e/tests/ui/pages/vulnerability-list/VulnerabilityListPage.ts b/e2e/tests/ui/pages/vulnerability-list/VulnerabilityListPage.ts
new file mode 100644
index 000000000..b994f08f2
--- /dev/null
+++ b/e2e/tests/ui/pages/vulnerability-list/VulnerabilityListPage.ts
@@ -0,0 +1,35 @@
+import type { Page } from "@playwright/test";
+import { Navigation } from "../Navigation";
+import { Toolbar } from "../Toolbar";
+import { Table } from "../Table";
+import { Pagination } from "../Pagination";
+
+export class VulnerabilityListPage {
+ private readonly _page: Page;
+
+ private constructor(page: Page) {
+ this._page = page;
+ }
+
+ static async build(page: Page) {
+ const navigation = await Navigation.build(page);
+ await navigation.goToSidebar("Vulnerabilities");
+
+ return new VulnerabilityListPage(page);
+ }
+
+ async getToolbar() {
+ return await Toolbar.build(this._page, "vulnerability-toolbar");
+ }
+
+ async getTable() {
+ return await Table.build(this._page, "Vulnerability table");
+ }
+
+ async getPagination(top: boolean = true) {
+ return await Pagination.build(
+ this._page,
+ `vulnerability-table-pagination-${top ? "top" : "bottom"}`,
+ );
+ }
+}
diff --git a/e2e/tests/ui/pages/vulnerability-list/columns.spec.ts b/e2e/tests/ui/pages/vulnerability-list/columns.spec.ts
new file mode 100644
index 000000000..081e1395a
--- /dev/null
+++ b/e2e/tests/ui/pages/vulnerability-list/columns.spec.ts
@@ -0,0 +1,30 @@
+// @ts-check
+
+import { expect } from "@playwright/test";
+
+import { test } from "../../fixtures";
+import { login } from "../../helpers/Auth";
+import { VulnerabilityListPage } from "./VulnerabilityListPage";
+
+test.describe("Columns validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Impacted SBOMs", async ({ page }) => {
+ const listPage = await VulnerabilityListPage.build(page);
+
+ const toolbar = await listPage.getToolbar();
+ const table = await listPage.getTable();
+
+ // Full search
+ await toolbar.applyTextFilter("Filter text", "CVE-2024-26308");
+ await table.waitUntilDataIsLoaded();
+ await table.verifyColumnContainsText("ID", "CVE-2024-26308");
+
+ // Impacted SBOMs
+ await expect(
+ table._table.locator(`td[data-label="Impacted SBOMs"]`),
+ ).toContainText("1");
+ });
+});
diff --git a/e2e/tests/ui/pages/vulnerability-list/filter.spec.ts b/e2e/tests/ui/pages/vulnerability-list/filter.spec.ts
new file mode 100644
index 000000000..a9a2c8e12
--- /dev/null
+++ b/e2e/tests/ui/pages/vulnerability-list/filter.spec.ts
@@ -0,0 +1,37 @@
+// @ts-check
+
+import { test } from "../../fixtures";
+import { login } from "../../helpers/Auth";
+import { VulnerabilityListPage } from "./VulnerabilityListPage";
+
+test.describe("Filter validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Filters", async ({ page }) => {
+ const listPage = await VulnerabilityListPage.build(page);
+
+ const toolbar = await listPage.getToolbar();
+ const table = await listPage.getTable();
+
+ // Full search
+ await toolbar.applyTextFilter("Filter text", "CVE-2024-26308");
+ await table.waitUntilDataIsLoaded();
+ await table.verifyColumnContainsText("ID", "CVE-2024-26308");
+
+ // Severity filter
+ await toolbar.applyMultiSelectFilter("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 table.waitUntilDataIsLoaded();
+ await table.verifyColumnContainsText("ID", "CVE-2024-26308");
+ });
+});
diff --git a/e2e/tests/ui/pages/vulnerability-list/pagination.spec.ts b/e2e/tests/ui/pages/vulnerability-list/pagination.spec.ts
new file mode 100644
index 000000000..802ad1fe1
--- /dev/null
+++ b/e2e/tests/ui/pages/vulnerability-list/pagination.spec.ts
@@ -0,0 +1,27 @@
+// @ts-check
+
+import { test } from "../../fixtures";
+import { login } from "../../helpers/Auth";
+import { VulnerabilityListPage } from "./VulnerabilityListPage";
+
+test.describe("Pagination validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Navigation button validations", async ({ page }) => {
+ const listPage = await VulnerabilityListPage.build(page);
+ const pagination = await listPage.getPagination();
+
+ await pagination.validatePagination();
+ });
+
+ test("Items per page validations", async ({ page }) => {
+ const listPage = await VulnerabilityListPage.build(page);
+
+ const pagination = await listPage.getPagination();
+ const table = await listPage.getTable();
+
+ await pagination.validateItemsPerPage("ID", table);
+ });
+});
diff --git a/e2e/tests/ui/pages/vulnerability-list/sort.spec.ts b/e2e/tests/ui/pages/vulnerability-list/sort.spec.ts
new file mode 100644
index 000000000..b2d4b5d29
--- /dev/null
+++ b/e2e/tests/ui/pages/vulnerability-list/sort.spec.ts
@@ -0,0 +1,30 @@
+// @ts-check
+
+import { test } from "../../fixtures";
+import { login } from "../../helpers/Auth";
+import { expectSort } from "../Helpers";
+import { VulnerabilityListPage } from "./VulnerabilityListPage";
+
+test.describe("Sort validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ // TODO enable after https://github.com/trustification/trustify/issues/1811 is fixed
+ test.skip("Sort", async ({ page }) => {
+ const listPage = await VulnerabilityListPage.build(page);
+ const table = await listPage.getTable();
+
+ const columnNameSelector = table._table.locator(`td[data-label="ID"]`);
+
+ // ID Asc
+ await table.clickSortBy("ID");
+ const ascList = await columnNameSelector.allInnerTexts();
+ expectSort(ascList, true);
+
+ // ID Desc
+ await table.clickSortBy("ID");
+ const desList = await columnNameSelector.allInnerTexts();
+ expectSort(desList, false);
+ });
+});