-
Notifications
You must be signed in to change notification settings - Fork 28
feat: add playwright tests that do not require cucumber feature files #658
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
172a29b
b6ad319
07f30d6
781e5b0
60dc00d
de0b5c8
9b52e56
33e76c0
830e1c4
f70fa03
21b2aa0
caafbce
d2548d1
02d10d3
f44c6d1
d8dcf55
e4ba2b1
fc08513
77b402f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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) { | ||
| 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); | ||
| } | ||
| } | ||
| 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) => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
| }; | ||
| 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(); | ||
| } | ||
| } | ||
| } |
| 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 | ||
carlosthe19916 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // 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(); | ||
| } | ||
| } | ||
| 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` }) | ||
carlosthe19916 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| .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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
| ); | ||
| } | ||
| } | ||
| 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) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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.