Skip to content
This repository was archived by the owner on Sep 11, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}'"
},
Expand Down
10 changes: 10 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 3 additions & 3 deletions tests/ui/helpers/Auth.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
}
};
2 changes: 1 addition & 1 deletion tests/ui/helpers/ToolbarTable.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
43 changes: 43 additions & 0 deletions tests/ui/pages/DetailsPageLayout.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
23 changes: 23 additions & 0 deletions tests/ui/pages/Helpers.ts
Original file line number Diff line number Diff line change
@@ -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);
};
44 changes: 44 additions & 0 deletions tests/ui/pages/LabelsModal.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
33 changes: 33 additions & 0 deletions tests/ui/pages/Navigation.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}
90 changes: 90 additions & 0 deletions tests/ui/pages/Pagination.ts
Original file line number Diff line number Diff line change
@@ -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
);
}
}
92 changes: 92 additions & 0 deletions tests/ui/pages/Table.ts
Original file line number Diff line number Diff line change
@@ -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. <table aria-label="identifier"></table>
* @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);
}
}
}
Loading
Loading