diff --git a/package.json b/package.json
index 79d9094..0cc5b83 100644
--- a/package.json
+++ b/package.json
@@ -6,11 +6,13 @@
"scripts": {
"openapi": "openapi-ts -f ./config/openapi-ts.config.ts",
"codegen": "npx playwright codegen http://localhost:8080/",
- "test": "npx bddgen && npx playwright test --project='chromium'",
- "test:api": "npx bddgen && npx playwright test --project='api'",
- "test:ui": "npx bddgen && npx playwright test --project='chromium'",
- "test:ui:trace": "npx bddgen && npx playwright test --trace on",
+ "test": "npx bddgen && npx playwright test --project='api' --project='chromium' --project='playwright'",
+ "test:api": "npx playwright test --project='api'",
+ "test:ui": "npx bddgen && npx playwright test --project='chromium' --project='playwright'",
+ "test:ui:trace": "npx bddgen && npx playwright test --project='chromium' --project='playwright' --trace on",
"test:ui:host": "npx bddgen && npx playwright test --ui-host 127.0.0.1",
+ "test:playwright": "npx playwright test --project='playwright'",
+ "test:playwright:trace": "npx playwright test --project='playwright' --trace on",
"format:check": "prettier --check './**/*.{ts,js,json}'",
"format:fix": "prettier --write './**/*.{ts,js,json}'"
},
diff --git a/playwright.config.ts b/playwright.config.ts
index 8aae734..942bb0c 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -72,6 +72,16 @@ export default defineConfig({
dependencies: ["setup-ui-data"],
},
+ {
+ name: "playwright",
+ testDir: "./tests/ui/pages",
+ testMatch: "*.spec.ts",
+ use: {
+ ...devices["Desktop Chrome"],
+ ...DESKTOP_CONFIG,
+ },
+ },
+
{
name: "setup-ui-data",
testDir: "./tests/ui/dependencies",
diff --git a/tests/ui/helpers/Auth.ts b/tests/ui/helpers/Auth.ts
index 1ccfe43..b6787a2 100644
--- a/tests/ui/helpers/Auth.ts
+++ b/tests/ui/helpers/Auth.ts
@@ -1,4 +1,4 @@
-import { Page } from "@playwright/test";
+import { expect, Page } from "@playwright/test";
export const login = async (page: Page) => {
let shouldLogin = process.env.TRUSTIFY_AUTH_ENABLED;
@@ -7,12 +7,12 @@ export const login = async (page: Page) => {
let userName = process.env.TRUSTIFY_AUTH_USER ?? "admin";
let userPassword = process.env.TRUSTIFY_AUTH_PASSWORD ?? "admin";
- await page.goto("/");
+ await page.goto("/upload");
await page.fill('input[name="username"]:visible', userName);
await page.fill('input[name="password"]:visible', userPassword);
await page.keyboard.press("Enter");
- await page.waitForSelector("text=Dashboard"); // Ensure login was successful
+ await expect(page.getByRole("heading", { name: "Upload" })).toHaveCount(1); // Ensure login was successful
}
};
diff --git a/tests/ui/helpers/ToolbarTable.ts b/tests/ui/helpers/ToolbarTable.ts
index 49e51b3..3369691 100644
--- a/tests/ui/helpers/ToolbarTable.ts
+++ b/tests/ui/helpers/ToolbarTable.ts
@@ -1,6 +1,6 @@
import { expect, Page } from "@playwright/test";
export class ToolbarTable {
- private _page: Page;
+ private readonly _page: Page;
private _tableName: string;
constructor(page: Page, tableName: string) {
diff --git a/tests/ui/pages/DetailsPageLayout.ts b/tests/ui/pages/DetailsPageLayout.ts
new file mode 100644
index 0000000..ad344ae
--- /dev/null
+++ b/tests/ui/pages/DetailsPageLayout.ts
@@ -0,0 +1,43 @@
+import { expect, 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/tests/ui/pages/Helpers.ts b/tests/ui/pages/Helpers.ts
new file mode 100644
index 0000000..bd7b4b1
--- /dev/null
+++ b/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/tests/ui/pages/LabelsModal.ts b/tests/ui/pages/LabelsModal.ts
new file mode 100644
index 0000000..fd8b3c6
--- /dev/null
+++ b/tests/ui/pages/LabelsModal.ts
@@ -0,0 +1,44 @@
+import { Locator, Page } from "playwright-core";
+import { expect } from "playwright/test";
+
+export class LabelsModal {
+ private readonly _page: Page;
+ private _dialog: Locator;
+
+ private constructor(page: Page, dialog: Locator) {
+ this._page = page;
+ 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/tests/ui/pages/Navigation.ts b/tests/ui/pages/Navigation.ts
new file mode 100644
index 0000000..cedd817
--- /dev/null
+++ b/tests/ui/pages/Navigation.ts
@@ -0,0 +1,33 @@
+import { 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/tests/ui/pages/Pagination.ts b/tests/ui/pages/Pagination.ts
new file mode 100644
index 0000000..bdcdfd4
--- /dev/null
+++ b/tests/ui/pages/Pagination.ts
@@ -0,0 +1,90 @@
+import { expect, Locator, Page } from "@playwright/test";
+import { 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: number) {
+ 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 fistPageButton = this._pagination.locator(
+ "button[data-action='first']"
+ );
+ await expect(fistPageButton).toBeVisible();
+ await expect(fistPageButton).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(fistPageButton).toBeVisible();
+ await expect(fistPageButton).not.toBeDisabled();
+
+ // Moving back to the first page
+ await fistPageButton.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/tests/ui/pages/Table.ts b/tests/ui/pages/Table.ts
new file mode 100644
index 0000000..d70e750
--- /dev/null
+++ b/tests/ui/pages/Table.ts
@@ -0,0 +1,92 @@
+import { expect, Locator, 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/tests/ui/pages/Toolbar.ts b/tests/ui/pages/Toolbar.ts
new file mode 100644
index 0000000..865e84c
--- /dev/null
+++ b/tests/ui/pages/Toolbar.ts
@@ -0,0 +1,109 @@
+import { expect, Locator, 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/tests/ui/pages/advisory-details/AdvisoryDetailsPage.ts b/tests/ui/pages/advisory-details/AdvisoryDetailsPage.ts
new file mode 100644
index 0000000..e935e74
--- /dev/null
+++ b/tests/ui/pages/advisory-details/AdvisoryDetailsPage.ts
@@ -0,0 +1,34 @@
+import { expect, Page } from "@playwright/test";
+import { DetailsPageLayout } from "../DetailsPageLayout";
+import { Navigation } from "../Navigation";
+import { AdvisoryListPage } from "../advisory-list/AdvisoryListPage";
+
+export class AdvisoryDetailsPage {
+ private readonly _page: Page;
+ _layout: DetailsPageLayout;
+
+ private constructor(page: Page, layout: DetailsPageLayout) {
+ this._page = page;
+ 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/tests/ui/pages/advisory-details/info/info.spec.ts b/tests/ui/pages/advisory-details/info/info.spec.ts
new file mode 100644
index 0000000..c1ad9eb
--- /dev/null
+++ b/tests/ui/pages/advisory-details/info/info.spec.ts
@@ -0,0 +1,45 @@
+// @ts-check
+
+import { expect, test } from "@playwright/test";
+
+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/tests/ui/pages/advisory-details/vulnerabilities/VulnerabilitiesTab.ts b/tests/ui/pages/advisory-details/vulnerabilities/VulnerabilitiesTab.ts
new file mode 100644
index 0000000..fa5bb6e
--- /dev/null
+++ b/tests/ui/pages/advisory-details/vulnerabilities/VulnerabilitiesTab.ts
@@ -0,0 +1,37 @@
+import { 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(top: boolean = true) {
+ return await Pagination.build(
+ this._page,
+ `vulnerability-table-pagination-${top ? "top" : "bottom"}`
+ );
+ }
+}
diff --git a/tests/ui/pages/advisory-details/vulnerabilities/columns.spec.ts b/tests/ui/pages/advisory-details/vulnerabilities/columns.spec.ts
new file mode 100644
index 0000000..a198f84
--- /dev/null
+++ b/tests/ui/pages/advisory-details/vulnerabilities/columns.spec.ts
@@ -0,0 +1,43 @@
+// @ts-check
+
+import { expect, test } from "@playwright/test";
+
+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/tests/ui/pages/advisory-details/vulnerabilities/filter.spec.ts b/tests/ui/pages/advisory-details/vulnerabilities/filter.spec.ts
new file mode 100644
index 0000000..5d17778
--- /dev/null
+++ b/tests/ui/pages/advisory-details/vulnerabilities/filter.spec.ts
@@ -0,0 +1,17 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+import { login } from "../../../helpers/Auth";
+import { VulnerabilitiesTab } from "./VulnerabilitiesTab";
+
+// Tables does not have filters
+test.describe.skip("Filter validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Filters", async ({ page }) => {
+ await VulnerabilitiesTab.build(page, "CVE-2024-26308");
+ });
+});
diff --git a/tests/ui/pages/advisory-details/vulnerabilities/pagination.spec.ts b/tests/ui/pages/advisory-details/vulnerabilities/pagination.spec.ts
new file mode 100644
index 0000000..a3a5194
--- /dev/null
+++ b/tests/ui/pages/advisory-details/vulnerabilities/pagination.spec.ts
@@ -0,0 +1,35 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+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/tests/ui/pages/advisory-details/vulnerabilities/sort.spec.ts b/tests/ui/pages/advisory-details/vulnerabilities/sort.spec.ts
new file mode 100644
index 0000000..92c12f0
--- /dev/null
+++ b/tests/ui/pages/advisory-details/vulnerabilities/sort.spec.ts
@@ -0,0 +1,31 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+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/tests/ui/pages/advisory-list/AdvisoryListPage.ts b/tests/ui/pages/advisory-list/AdvisoryListPage.ts
new file mode 100644
index 0000000..ffd721a
--- /dev/null
+++ b/tests/ui/pages/advisory-list/AdvisoryListPage.ts
@@ -0,0 +1,35 @@
+import { 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/tests/ui/pages/advisory-list/columns.spec.ts b/tests/ui/pages/advisory-list/columns.spec.ts
new file mode 100644
index 0000000..b72aa7a
--- /dev/null
+++ b/tests/ui/pages/advisory-list/columns.spec.ts
@@ -0,0 +1,41 @@
+// @ts-check
+
+import { expect, test } from "@playwright/test";
+
+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/tests/ui/pages/advisory-list/filter.spec.ts b/tests/ui/pages/advisory-list/filter.spec.ts
new file mode 100644
index 0000000..566ac96
--- /dev/null
+++ b/tests/ui/pages/advisory-list/filter.spec.ts
@@ -0,0 +1,34 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+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", "08/01/2024", "08/03/2024");
+ 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/tests/ui/pages/advisory-list/pagination.spec.ts b/tests/ui/pages/advisory-list/pagination.spec.ts
new file mode 100644
index 0000000..18f6fb9
--- /dev/null
+++ b/tests/ui/pages/advisory-list/pagination.spec.ts
@@ -0,0 +1,28 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+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/tests/ui/pages/advisory-list/sort.spec.ts b/tests/ui/pages/advisory-list/sort.spec.ts
new file mode 100644
index 0000000..300fd57
--- /dev/null
+++ b/tests/ui/pages/advisory-list/sort.spec.ts
@@ -0,0 +1,31 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+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/tests/ui/pages/package-details/PackageDetailsPage.ts b/tests/ui/pages/package-details/PackageDetailsPage.ts
new file mode 100644
index 0000000..6716797
--- /dev/null
+++ b/tests/ui/pages/package-details/PackageDetailsPage.ts
@@ -0,0 +1,35 @@
+import { expect, Page } from "@playwright/test";
+import { DetailsPageLayout } from "../DetailsPageLayout";
+import { Navigation } from "../Navigation";
+import { VulnerabilityListPage } from "../vulnerability-list/VulnerabilityListPage";
+import { PackageListPage } from "../package-list/PackageListPage";
+
+export class PackageDetailsPage {
+ private readonly _page: Page;
+ _layout: DetailsPageLayout;
+
+ private constructor(page: Page, layout: DetailsPageLayout) {
+ this._page = page;
+ 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/tests/ui/pages/package-details/info/info.spec.ts b/tests/ui/pages/package-details/info/info.spec.ts
new file mode 100644
index 0000000..408f9f6
--- /dev/null
+++ b/tests/ui/pages/package-details/info/info.spec.ts
@@ -0,0 +1,17 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+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
+ });
+});
diff --git a/tests/ui/pages/package-details/sboms/SbomsTab.ts b/tests/ui/pages/package-details/sboms/SbomsTab.ts
new file mode 100644
index 0000000..34aee5c
--- /dev/null
+++ b/tests/ui/pages/package-details/sboms/SbomsTab.ts
@@ -0,0 +1,37 @@
+import { 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/tests/ui/pages/package-details/sboms/columns.spec.ts b/tests/ui/pages/package-details/sboms/columns.spec.ts
new file mode 100644
index 0000000..004287c
--- /dev/null
+++ b/tests/ui/pages/package-details/sboms/columns.spec.ts
@@ -0,0 +1,38 @@
+// @ts-check
+
+import { expect, test } from "@playwright/test";
+
+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/tests/ui/pages/package-details/sboms/filter.spec.ts b/tests/ui/pages/package-details/sboms/filter.spec.ts
new file mode 100644
index 0000000..315657b
--- /dev/null
+++ b/tests/ui/pages/package-details/sboms/filter.spec.ts
@@ -0,0 +1,17 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+import { login } from "../../../helpers/Auth";
+import { SbomsTab } from "./SbomsTab";
+
+// Table does not have filters
+test.describe.skip("Filter validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Filters", async ({ page }) => {
+ await SbomsTab.build(page, "keycloak-core");
+ });
+});
diff --git a/tests/ui/pages/package-details/sboms/pagination.spec.ts b/tests/ui/pages/package-details/sboms/pagination.spec.ts
new file mode 100644
index 0000000..5db8d09
--- /dev/null
+++ b/tests/ui/pages/package-details/sboms/pagination.spec.ts
@@ -0,0 +1,29 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+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/tests/ui/pages/package-details/sboms/sort.spec.ts b/tests/ui/pages/package-details/sboms/sort.spec.ts
new file mode 100644
index 0000000..13db16a
--- /dev/null
+++ b/tests/ui/pages/package-details/sboms/sort.spec.ts
@@ -0,0 +1,28 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+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/tests/ui/pages/package-details/vulnerabilities/VulnerabilitiesTab.ts b/tests/ui/pages/package-details/vulnerabilities/VulnerabilitiesTab.ts
new file mode 100644
index 0000000..5c69462
--- /dev/null
+++ b/tests/ui/pages/package-details/vulnerabilities/VulnerabilitiesTab.ts
@@ -0,0 +1,37 @@
+import { 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/tests/ui/pages/package-details/vulnerabilities/columns.spec.ts b/tests/ui/pages/package-details/vulnerabilities/columns.spec.ts
new file mode 100644
index 0000000..75f349c
--- /dev/null
+++ b/tests/ui/pages/package-details/vulnerabilities/columns.spec.ts
@@ -0,0 +1,36 @@
+// @ts-check
+
+import { expect, test } from "@playwright/test";
+
+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/tests/ui/pages/package-details/vulnerabilities/filter.spec.ts b/tests/ui/pages/package-details/vulnerabilities/filter.spec.ts
new file mode 100644
index 0000000..8874412
--- /dev/null
+++ b/tests/ui/pages/package-details/vulnerabilities/filter.spec.ts
@@ -0,0 +1,17 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+import { login } from "../../../helpers/Auth";
+import { VulnerabilitiesTab } from "./VulnerabilitiesTab";
+
+// Table does not have filters
+test.describe.skip("Filter validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Filters", async ({ page }) => {
+ await VulnerabilitiesTab.build(page, "keycloak-core");
+ });
+});
diff --git a/tests/ui/pages/package-details/vulnerabilities/pagination.spec.ts b/tests/ui/pages/package-details/vulnerabilities/pagination.spec.ts
new file mode 100644
index 0000000..a9b0ab4
--- /dev/null
+++ b/tests/ui/pages/package-details/vulnerabilities/pagination.spec.ts
@@ -0,0 +1,35 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+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/tests/ui/pages/package-details/vulnerabilities/sort.spec.ts b/tests/ui/pages/package-details/vulnerabilities/sort.spec.ts
new file mode 100644
index 0000000..da23a18
--- /dev/null
+++ b/tests/ui/pages/package-details/vulnerabilities/sort.spec.ts
@@ -0,0 +1,31 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+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/tests/ui/pages/package-list/PackageListPage.ts b/tests/ui/pages/package-list/PackageListPage.ts
new file mode 100644
index 0000000..c5374d7
--- /dev/null
+++ b/tests/ui/pages/package-list/PackageListPage.ts
@@ -0,0 +1,35 @@
+import { 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/tests/ui/pages/package-list/columns.spec.ts b/tests/ui/pages/package-list/columns.spec.ts
new file mode 100644
index 0000000..04008a4
--- /dev/null
+++ b/tests/ui/pages/package-list/columns.spec.ts
@@ -0,0 +1,59 @@
+// @ts-check
+
+import { expect, test } from "@playwright/test";
+
+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/tests/ui/pages/package-list/filter.spec.ts b/tests/ui/pages/package-list/filter.spec.ts
new file mode 100644
index 0000000..e8a711f
--- /dev/null
+++ b/tests/ui/pages/package-list/filter.spec.ts
@@ -0,0 +1,34 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+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/tests/ui/pages/package-list/pagination.spec.ts b/tests/ui/pages/package-list/pagination.spec.ts
new file mode 100644
index 0000000..979b5e9
--- /dev/null
+++ b/tests/ui/pages/package-list/pagination.spec.ts
@@ -0,0 +1,28 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+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/tests/ui/pages/package-list/sort.spec.ts b/tests/ui/pages/package-list/sort.spec.ts
new file mode 100644
index 0000000..49bf7f7
--- /dev/null
+++ b/tests/ui/pages/package-list/sort.spec.ts
@@ -0,0 +1,29 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+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/tests/ui/pages/sbom-details/SbomDetailsPage.ts b/tests/ui/pages/sbom-details/SbomDetailsPage.ts
new file mode 100644
index 0000000..f17bff9
--- /dev/null
+++ b/tests/ui/pages/sbom-details/SbomDetailsPage.ts
@@ -0,0 +1,34 @@
+import { expect, Page } from "@playwright/test";
+import { DetailsPageLayout } from "../DetailsPageLayout";
+import { Navigation } from "../Navigation";
+import { SbomListPage } from "../sbom-list/SbomListPage";
+
+export class SbomDetailsPage {
+ private readonly _page: Page;
+ _layout: DetailsPageLayout;
+
+ private constructor(page: Page, layout: DetailsPageLayout) {
+ this._page = page;
+ 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/tests/ui/pages/sbom-details/actions.spec.ts b/tests/ui/pages/sbom-details/actions.spec.ts
new file mode 100644
index 0000000..6ce85cd
--- /dev/null
+++ b/tests/ui/pages/sbom-details/actions.spec.ts
@@ -0,0 +1,32 @@
+// @ts-check
+
+import { expect, test } from "@playwright/test";
+
+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/tests/ui/pages/sbom-details/info/info.spec.ts b/tests/ui/pages/sbom-details/info/info.spec.ts
new file mode 100644
index 0000000..0406670
--- /dev/null
+++ b/tests/ui/pages/sbom-details/info/info.spec.ts
@@ -0,0 +1,44 @@
+// @ts-check
+
+import { expect, test } from "@playwright/test";
+
+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/tests/ui/pages/sbom-details/packages/PackagesTab.ts b/tests/ui/pages/sbom-details/packages/PackagesTab.ts
new file mode 100644
index 0000000..41ec7fb
--- /dev/null
+++ b/tests/ui/pages/sbom-details/packages/PackagesTab.ts
@@ -0,0 +1,37 @@
+import { 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/tests/ui/pages/sbom-details/packages/columns.spec.ts b/tests/ui/pages/sbom-details/packages/columns.spec.ts
new file mode 100644
index 0000000..6c6f4dc
--- /dev/null
+++ b/tests/ui/pages/sbom-details/packages/columns.spec.ts
@@ -0,0 +1,65 @@
+// @ts-check
+
+import { expect, test } from "@playwright/test";
+
+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)
+ ).toContainText("Apache-2.0");
+ await expect(
+ table._table.locator(`td[data-label="Licenses"]`).nth(1)
+ ).toContainText("NOASSERTION");
+
+ // 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/tests/ui/pages/sbom-details/packages/filter.spec.ts b/tests/ui/pages/sbom-details/packages/filter.spec.ts
new file mode 100644
index 0000000..643d99e
--- /dev/null
+++ b/tests/ui/pages/sbom-details/packages/filter.spec.ts
@@ -0,0 +1,32 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+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/tests/ui/pages/sbom-details/packages/pagination.spec.ts b/tests/ui/pages/sbom-details/packages/pagination.spec.ts
new file mode 100644
index 0000000..54de917
--- /dev/null
+++ b/tests/ui/pages/sbom-details/packages/pagination.spec.ts
@@ -0,0 +1,28 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+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/tests/ui/pages/sbom-details/packages/sort.spec.ts b/tests/ui/pages/sbom-details/packages/sort.spec.ts
new file mode 100644
index 0000000..1ef167f
--- /dev/null
+++ b/tests/ui/pages/sbom-details/packages/sort.spec.ts
@@ -0,0 +1,28 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+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/tests/ui/pages/sbom-details/vulnerabilities/VulnerabilitiesTab.ts b/tests/ui/pages/sbom-details/vulnerabilities/VulnerabilitiesTab.ts
new file mode 100644
index 0000000..197223b
--- /dev/null
+++ b/tests/ui/pages/sbom-details/vulnerabilities/VulnerabilitiesTab.ts
@@ -0,0 +1,37 @@
+import { 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/tests/ui/pages/sbom-details/vulnerabilities/columns.spec.ts b/tests/ui/pages/sbom-details/vulnerabilities/columns.spec.ts
new file mode 100644
index 0000000..328e3a8
--- /dev/null
+++ b/tests/ui/pages/sbom-details/vulnerabilities/columns.spec.ts
@@ -0,0 +1,70 @@
+// @ts-check
+
+import { expect, test } from "@playwright/test";
+
+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/tests/ui/pages/sbom-details/vulnerabilities/donutchart.spec.ts b/tests/ui/pages/sbom-details/vulnerabilities/donutchart.spec.ts
new file mode 100644
index 0000000..e4a2c0f
--- /dev/null
+++ b/tests/ui/pages/sbom-details/vulnerabilities/donutchart.spec.ts
@@ -0,0 +1,23 @@
+// @ts-check
+
+import { expect, test } from "@playwright/test";
+
+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: 9");
+ await expect(page.locator("#legend-labels-3")).toContainText("Low: 0");
+ await expect(page.locator("#legend-labels-4")).toContainText("None: 1");
+ await expect(page.locator("#legend-labels-5")).toContainText("Unknown: 0");
+ });
+});
diff --git a/tests/ui/pages/sbom-details/vulnerabilities/filter.spec.ts b/tests/ui/pages/sbom-details/vulnerabilities/filter.spec.ts
new file mode 100644
index 0000000..25ae770
--- /dev/null
+++ b/tests/ui/pages/sbom-details/vulnerabilities/filter.spec.ts
@@ -0,0 +1,33 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+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/tests/ui/pages/sbom-details/vulnerabilities/pagination.spec.ts b/tests/ui/pages/sbom-details/vulnerabilities/pagination.spec.ts
new file mode 100644
index 0000000..aa636f5
--- /dev/null
+++ b/tests/ui/pages/sbom-details/vulnerabilities/pagination.spec.ts
@@ -0,0 +1,31 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+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/tests/ui/pages/sbom-details/vulnerabilities/sort.spec.ts b/tests/ui/pages/sbom-details/vulnerabilities/sort.spec.ts
new file mode 100644
index 0000000..304d621
--- /dev/null
+++ b/tests/ui/pages/sbom-details/vulnerabilities/sort.spec.ts
@@ -0,0 +1,31 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+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/tests/ui/pages/sbom-list/SbomListPage.ts b/tests/ui/pages/sbom-list/SbomListPage.ts
new file mode 100644
index 0000000..c85a548
--- /dev/null
+++ b/tests/ui/pages/sbom-list/SbomListPage.ts
@@ -0,0 +1,35 @@
+import { 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/tests/ui/pages/sbom-list/actions.spec.ts b/tests/ui/pages/sbom-list/actions.spec.ts
new file mode 100644
index 0000000..845c6a8
--- /dev/null
+++ b/tests/ui/pages/sbom-list/actions.spec.ts
@@ -0,0 +1,85 @@
+// @ts-check
+
+import { expect, test } from "@playwright/test";
+
+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/tests/ui/pages/sbom-list/columns.spec.ts b/tests/ui/pages/sbom-list/columns.spec.ts
new file mode 100644
index 0000000..3b30946
--- /dev/null
+++ b/tests/ui/pages/sbom-list/columns.spec.ts
@@ -0,0 +1,57 @@
+// @ts-check
+
+import { expect, test } from "@playwright/test";
+
+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: 9,
+ },
+ {
+ severity: "none",
+ count: 1,
+ },
+ ];
+
+ 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/tests/ui/pages/sbom-list/filter.spec.ts b/tests/ui/pages/sbom-list/filter.spec.ts
new file mode 100644
index 0000000..cd11a9a
--- /dev/null
+++ b/tests/ui/pages/sbom-list/filter.spec.ts
@@ -0,0 +1,38 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+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/tests/ui/pages/sbom-list/pagination.spec.ts b/tests/ui/pages/sbom-list/pagination.spec.ts
new file mode 100644
index 0000000..7a3b270
--- /dev/null
+++ b/tests/ui/pages/sbom-list/pagination.spec.ts
@@ -0,0 +1,28 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+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/tests/ui/pages/sbom-list/sort.spec.ts b/tests/ui/pages/sbom-list/sort.spec.ts
new file mode 100644
index 0000000..540eb93
--- /dev/null
+++ b/tests/ui/pages/sbom-list/sort.spec.ts
@@ -0,0 +1,28 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+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/tests/ui/pages/vulnerability-details/VulnerabilityDetailsPage.ts b/tests/ui/pages/vulnerability-details/VulnerabilityDetailsPage.ts
new file mode 100644
index 0000000..dd707e2
--- /dev/null
+++ b/tests/ui/pages/vulnerability-details/VulnerabilityDetailsPage.ts
@@ -0,0 +1,36 @@
+import { expect, Page } from "@playwright/test";
+import { DetailsPageLayout } from "../DetailsPageLayout";
+import { Navigation } from "../Navigation";
+import { VulnerabilityListPage } from "../vulnerability-list/VulnerabilityListPage";
+
+export class VulnerabilityDetailsPage {
+ private readonly _page: Page;
+ _layout: DetailsPageLayout;
+
+ private constructor(page: Page, layout: DetailsPageLayout) {
+ this._page = page;
+ 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/tests/ui/pages/vulnerability-details/advisories/AdvisoriesTab.ts b/tests/ui/pages/vulnerability-details/advisories/AdvisoriesTab.ts
new file mode 100644
index 0000000..9d60e31
--- /dev/null
+++ b/tests/ui/pages/vulnerability-details/advisories/AdvisoriesTab.ts
@@ -0,0 +1,40 @@
+import { 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/tests/ui/pages/vulnerability-details/advisories/columns.spec.ts b/tests/ui/pages/vulnerability-details/advisories/columns.spec.ts
new file mode 100644
index 0000000..3033218
--- /dev/null
+++ b/tests/ui/pages/vulnerability-details/advisories/columns.spec.ts
@@ -0,0 +1,48 @@
+// @ts-check
+
+import { expect, test } from "@playwright/test";
+
+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 ids = await table._table
+ .locator(`td[data-label="ID"]`)
+ .allInnerTexts();
+ const idIndex = ids.indexOf("CVE-2023-2976");
+ expect(idIndex).not.toBe(-1);
+
+ // Name
+ await expect(
+ table._table.locator(`td[data-label="ID"]`).nth(idIndex)
+ ).toContainText("CVE-2023-2976");
+
+ // Title
+ await expect(
+ table._table.locator(`td[data-label="Title"]`).nth(idIndex)
+ ).toContainText("guava: insecure temporary directory creation");
+
+ // Type
+ await expect(
+ table._table.locator(`td[data-label="Type"]`).nth(idIndex)
+ ).toContainText("csaf");
+
+ // Revision
+ await expect(
+ table._table.locator(`td[data-label="Revision"]`).nth(idIndex)
+ ).toContainText("Nov 14, 2023");
+
+ // Vulnerabilities
+ await expect(
+ table._table.locator(`td[data-label="Vulnerabilities"]`).nth(idIndex)
+ ).toContainText("1");
+ });
+});
diff --git a/tests/ui/pages/vulnerability-details/advisories/filter.spec.ts b/tests/ui/pages/vulnerability-details/advisories/filter.spec.ts
new file mode 100644
index 0000000..219afeb
--- /dev/null
+++ b/tests/ui/pages/vulnerability-details/advisories/filter.spec.ts
@@ -0,0 +1,17 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+import { login } from "../../../helpers/Auth";
+import { AdvisoriesTab } from "./AdvisoriesTab";
+
+// Table does not have filters
+test.describe.skip("Filter validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Filters", async ({ page }) => {
+ await AdvisoriesTab.build(page, "CVE-2023-2976");
+ });
+});
diff --git a/tests/ui/pages/vulnerability-details/advisories/pagination.spec.ts b/tests/ui/pages/vulnerability-details/advisories/pagination.spec.ts
new file mode 100644
index 0000000..6f44686
--- /dev/null
+++ b/tests/ui/pages/vulnerability-details/advisories/pagination.spec.ts
@@ -0,0 +1,29 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+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/tests/ui/pages/vulnerability-details/advisories/sort.spec.ts b/tests/ui/pages/vulnerability-details/advisories/sort.spec.ts
new file mode 100644
index 0000000..dd6739c
--- /dev/null
+++ b/tests/ui/pages/vulnerability-details/advisories/sort.spec.ts
@@ -0,0 +1,28 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+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/tests/ui/pages/vulnerability-details/info/info.spec.ts b/tests/ui/pages/vulnerability-details/info/info.spec.ts
new file mode 100644
index 0000000..2c12e8b
--- /dev/null
+++ b/tests/ui/pages/vulnerability-details/info/info.spec.ts
@@ -0,0 +1,17 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+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/tests/ui/pages/vulnerability-details/sboms/SbomsTab.ts b/tests/ui/pages/vulnerability-details/sboms/SbomsTab.ts
new file mode 100644
index 0000000..c118fc9
--- /dev/null
+++ b/tests/ui/pages/vulnerability-details/sboms/SbomsTab.ts
@@ -0,0 +1,40 @@
+import { 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/tests/ui/pages/vulnerability-details/sboms/columns.spec.ts b/tests/ui/pages/vulnerability-details/sboms/columns.spec.ts
new file mode 100644
index 0000000..a0a8100
--- /dev/null
+++ b/tests/ui/pages/vulnerability-details/sboms/columns.spec.ts
@@ -0,0 +1,68 @@
+// @ts-check
+
+import { expect, test } from "@playwright/test";
+
+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/tests/ui/pages/vulnerability-details/sboms/filter.spec.ts b/tests/ui/pages/vulnerability-details/sboms/filter.spec.ts
new file mode 100644
index 0000000..8237876
--- /dev/null
+++ b/tests/ui/pages/vulnerability-details/sboms/filter.spec.ts
@@ -0,0 +1,17 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+import { login } from "../../../helpers/Auth";
+import { SbomsTab } from "./SbomsTab";
+
+// Table does not have filters
+test.describe.skip("Filter validations", { tag: "@tier1" }, () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test("Filters", async ({ page }) => {
+ await SbomsTab.build(page, "CVE-2023-2976");
+ });
+});
diff --git a/tests/ui/pages/vulnerability-details/sboms/pagination.spec.ts b/tests/ui/pages/vulnerability-details/sboms/pagination.spec.ts
new file mode 100644
index 0000000..0abdb61
--- /dev/null
+++ b/tests/ui/pages/vulnerability-details/sboms/pagination.spec.ts
@@ -0,0 +1,29 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+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/tests/ui/pages/vulnerability-details/sboms/sort.spec.ts b/tests/ui/pages/vulnerability-details/sboms/sort.spec.ts
new file mode 100644
index 0000000..6afbda7
--- /dev/null
+++ b/tests/ui/pages/vulnerability-details/sboms/sort.spec.ts
@@ -0,0 +1,28 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+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/tests/ui/pages/vulnerability-list/VulnerabilityListPage.ts b/tests/ui/pages/vulnerability-list/VulnerabilityListPage.ts
new file mode 100644
index 0000000..453626b
--- /dev/null
+++ b/tests/ui/pages/vulnerability-list/VulnerabilityListPage.ts
@@ -0,0 +1,35 @@
+import { 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/tests/ui/pages/vulnerability-list/columns.spec.ts b/tests/ui/pages/vulnerability-list/columns.spec.ts
new file mode 100644
index 0000000..9cc9421
--- /dev/null
+++ b/tests/ui/pages/vulnerability-list/columns.spec.ts
@@ -0,0 +1,29 @@
+// @ts-check
+
+import { expect, test } from "@playwright/test";
+
+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/tests/ui/pages/vulnerability-list/filter.spec.ts b/tests/ui/pages/vulnerability-list/filter.spec.ts
new file mode 100644
index 0000000..104322a
--- /dev/null
+++ b/tests/ui/pages/vulnerability-list/filter.spec.ts
@@ -0,0 +1,38 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+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", "None"]);
+ 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/tests/ui/pages/vulnerability-list/pagination.spec.ts b/tests/ui/pages/vulnerability-list/pagination.spec.ts
new file mode 100644
index 0000000..1cfb2fc
--- /dev/null
+++ b/tests/ui/pages/vulnerability-list/pagination.spec.ts
@@ -0,0 +1,28 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+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/tests/ui/pages/vulnerability-list/sort.spec.ts b/tests/ui/pages/vulnerability-list/sort.spec.ts
new file mode 100644
index 0000000..75d2a2c
--- /dev/null
+++ b/tests/ui/pages/vulnerability-list/sort.spec.ts
@@ -0,0 +1,31 @@
+// @ts-check
+
+import { test } from "@playwright/test";
+
+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);
+ });
+});