Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
172a29b
feat: add playwright tests that do not require cucumber feature files
carlosthe19916 Aug 21, 2025
b6ad319
fix: flaky test
carlosthe19916 Aug 21, 2025
07f30d6
Merge branch 'main' into task/move-trustify-testsw
carlosthe19916 Aug 21, 2025
781e5b0
remove prettier extension from devcontainers
carlosthe19916 Aug 21, 2025
60dc00d
Merge branch 'main' into task/move-trustify-testsw
carlosthe19916 Aug 21, 2025
de0b5c8
Merge branch 'main' into task/move-trustify-testsw
carlosthe19916 Aug 26, 2025
9b52e56
fix: tests after merging main
carlosthe19916 Aug 26, 2025
33e76c0
fix: tests after new assets were added to main
carlosthe19916 Aug 26, 2025
830e1c4
Merge branch 'main' into task/move-trustify-testsw
carlosthe19916 Aug 26, 2025
f70fa03
Merge branch 'main' into task/move-trustify-testsw
carlosthe19916 Aug 27, 2025
21b2aa0
Merge branch 'main' into task/move-trustify-testsw
carlosthe19916 Sep 4, 2025
caafbce
Add minor verification test in the Package Details page
carlosthe19916 Sep 4, 2025
d2548d1
Merge remote-tracking branch 'origin/task/move-trustify-testsw' into …
carlosthe19916 Sep 4, 2025
02d10d3
Merge branch 'main' into task/move-trustify-testsw
carlosthe19916 Sep 8, 2025
f44c6d1
Merge branch 'main' into task/move-trustify-testsw
carlosthe19916 Sep 8, 2025
d8dcf55
delete files that are skipped due to lack of testable components
carlosthe19916 Sep 8, 2025
e4ba2b1
use pagination verbose
carlosthe19916 Sep 8, 2025
fc08513
fix typo
carlosthe19916 Sep 8, 2025
77b402f
allow limited values on pagination
carlosthe19916 Sep 9, 2025
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
7 changes: 5 additions & 2 deletions .devcontainer/template.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
{
"name": "playwright",
"image": "mcr.microsoft.com/playwright:v1.55.0-jammy",
"build": {
"dockerfile": "Dockerfile"
},
"workspaceFolder": "/workspaces/trustify-ui/e2e",
"runArgs": [
"--privileged",
Expand All @@ -27,7 +29,8 @@
"customizations": {
"vscode": {
"extensions": [
"ms-playwright.playwright"
"ms-playwright.playwright",
"alexkrechik.cucumberautocomplete"
]
}
}
Expand Down
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ PLAYWRIGHT_IMAGE=mcr.microsoft.com/playwright
PLAYWRIGHT_VERSION=v1.55.0
UBUNTU_VERSION_ALIAS=-jammy
PLAYWRIGHT_PORT=5000
PLAYWRIGHT_HOME=/home/pwuser
PLAYWRIGHT_HOME=/home/pwuser
43 changes: 43 additions & 0 deletions e2e/tests/ui/pages/DetailsPageLayout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { expect, type Page } from "@playwright/test";

export class DetailsPageLayout {
private readonly _page: Page;

private constructor(page: Page) {
this._page = page;
}

static async build(page: Page) {
await expect(page.locator("nav[aria-label='Breadcrumb']")).toBeVisible();
return new DetailsPageLayout(page);
}

async selectTab(tabName: string) {
const tab = this._page.locator("button[role='tab']", { hasText: tabName });
await expect(tab).toBeVisible();
await tab.click();
}

async clickOnPageAction(actionName: string) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should make sure to unify the usage of methods like this across the repo, since they also implemented somewhere in the UI tests directory exactly the same way and used in the tests. The better we do this, the less of a pain it will be. I'm just adding this comment, so we don't forget to create issues in the repo for this sort of thing.

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 e2e/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) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above. I'm pretty sure that Rajan implemented something similar in one of his tests, so we should make sure we don't duplicate code unnecessarily.

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);
};
42 changes: 42 additions & 0 deletions e2e/tests/ui/pages/LabelsModal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { Locator, Page } from "playwright-core";
import { expect } from "playwright/test";

export class LabelsModal {
private _dialog: Locator;

private constructor(_page: Page, dialog: Locator) {
this._dialog = dialog;
}

static async build(page: Page) {
const dialog = page.getByRole("dialog");
await expect(dialog).toBeVisible();
return new LabelsModal(page, dialog);
}

async clickSave() {
await this._dialog.locator("button[aria-label='submit']").click();
await expect(this._dialog).not.toBeVisible();
}

async addLabels(labels: string[]) {
const inputText = this._dialog.getByPlaceholder("Add label");

for (const label of labels) {
await inputText.click();
await inputText.fill(label);
await inputText.press("Enter");
}
}

async removeLabels(labels: string[]) {
for (const label of labels) {
await this._dialog
.locator(".pf-v6-c-label-group__list-item", {
hasText: label,
})
.locator("button")
.click();
}
}
}
33 changes: 33 additions & 0 deletions e2e/tests/ui/pages/Navigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { Page } from "playwright-core";

/**
* Used to navigate to different pages
*/
export class Navigation {
private readonly _page: Page;

private constructor(page: Page) {
this._page = page;
}

static async build(page: Page) {
return new Navigation(page);
}

async goToSidebar(
menu:
| "Dashboard"
| "Search"
| "SBOMs"
| "Vulnerabilities"
| "Packages"
| "Advisories"
| "Importers"
| "Upload",
) {
// By default, we do not initialize navigation at "/"" where the Dashboard is located
// This should help us to save some time loading pages as the Dashboard fetches too much data
await this._page.goto("/upload");
await this._page.getByRole("link", { name: menu }).click();
}
}
90 changes: 90 additions & 0 deletions e2e/tests/ui/pages/Pagination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { expect, type Locator, type Page } from "@playwright/test";
import type { Table } from "./Table";

export class Pagination {
private readonly _page: Page;
_pagination: Locator;

private constructor(page: Page, pagination: Locator) {
this._page = page;
this._pagination = pagination;
}

static async build(page: Page, paginationId: string) {
const pagination = page.locator(`#${paginationId}`);
await expect(pagination).toBeVisible();
return new Pagination(page, pagination);
}

/**
* Selects Number of rows per page on the table
* @param perPage Number of rows
*/
async selectItemsPerPage(perPage: 10 | 20 | 50 | 100) {
await this._pagination
.locator(`//button[@aria-haspopup='listbox']`)
.click();
await this._page
.getByRole("menuitem", { name: `${perPage} per page` })
.click();

await expect(this._pagination.locator("input")).toHaveValue("1");
}

async validatePagination() {
// Verify next buttons are enabled as there are more than 11 rows present
const nextPageButton = this._pagination.locator(
"button[data-action='next']",
);
await expect(nextPageButton).toBeVisible();
await expect(nextPageButton).not.toBeDisabled();

// Verify that previous buttons are disabled being on the first page
const prevPageButton = this._pagination.locator(
"button[data-action='previous']",
);
await expect(prevPageButton).toBeVisible();
await expect(prevPageButton).toBeDisabled();

// Verify that navigation button to last page is enabled
const lastPageButton = this._pagination.locator(
"button[data-action='last']",
);
await expect(lastPageButton).toBeVisible();
await expect(lastPageButton).not.toBeDisabled();

// Verify that navigation button to first page is disabled being on the first page
const firstPageButton = this._pagination.locator(
"button[data-action='first']",
);
await expect(firstPageButton).toBeVisible();
await expect(firstPageButton).toBeDisabled();

// Navigate to next page
await nextPageButton.click();

// Verify that previous buttons are enabled after moving to next page
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are this thorough, should we also check if the next button is disabled on the last page?

await expect(prevPageButton).toBeVisible();
await expect(prevPageButton).not.toBeDisabled();

// Verify that navigation button to first page is enabled after moving to next page
await expect(firstPageButton).toBeVisible();
await expect(firstPageButton).not.toBeDisabled();

// Moving back to the first page
await firstPageButton.click();
}

async validateItemsPerPage(columnName: string, table: Table) {
// Verify that only 10 items are displayed
await this.selectItemsPerPage(10);
await table.validateNumberOfRows({ equal: 10 }, columnName);

// Verify that items less than or equal to 20 and greater than 10 are displayed
await this.selectItemsPerPage(20);
await table.validateNumberOfRows(
{ greaterThan: 10, lessThan: 21 },
columnName,
);
}
}
92 changes: 92 additions & 0 deletions e2e/tests/ui/pages/Table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { expect, type Locator, type Page } from "@playwright/test";

export class Table {
private readonly _page: Page;
_table: Locator;

private constructor(page: Page, table: Locator) {
this._page = page;
this._table = table;
}

/**
* @param page
* @param tableAriaLabel the unique aria-label that corresponds to the DOM element that contains the Table. E.g. <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) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, this and the next method are for sure candidates for unification with what's already in some of the UI tests.

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