diff --git a/.github/chatmodes/playwright-tester.chatmode.md b/.github/chatmodes/playwright-tester.chatmode.md index 4332c3afb..03d571f81 100644 --- a/.github/chatmodes/playwright-tester.chatmode.md +++ b/.github/chatmodes/playwright-tester.chatmode.md @@ -37,7 +37,7 @@ mode: 'agent' - Automatically run test with: ```bash cd $PROJECT_ROOT/e2e - npx playwright test --project='bdd' --trace on -g "scenario name here" --headed + npx playwright test --project='bdd' --trace on -g "scenario name here" ``` - In case of test failures, the above command launched HTML server to host the test output Press `Ctrl+C` to stop the server diff --git a/e2e/tests/common/dataset/advisory/cve/CVE-2023-1906.json.bz2 b/e2e/tests/common/dataset/advisory/cve/CVE-2023-1906.json.bz2 new file mode 100644 index 000000000..a9e02d3dd Binary files /dev/null and b/e2e/tests/common/dataset/advisory/cve/CVE-2023-1906.json.bz2 differ diff --git a/e2e/tests/common/dataset/advisory/cve/CVE-2025-22130.json.bz2 b/e2e/tests/common/dataset/advisory/cve/CVE-2025-22130.json.bz2 new file mode 100644 index 000000000..d3112b685 Binary files /dev/null and b/e2e/tests/common/dataset/advisory/cve/CVE-2025-22130.json.bz2 differ diff --git a/e2e/tests/common/dataset/sbom/mrg-m-3.0.0.json.bz2 b/e2e/tests/common/dataset/sbom/mrg-m-3.0.0.json.bz2 new file mode 100644 index 000000000..91be43b69 Binary files /dev/null and b/e2e/tests/common/dataset/sbom/mrg-m-3.0.0.json.bz2 differ diff --git a/e2e/tests/common/dataset/sbom/rhn_satellite_6.17.json.bz2 b/e2e/tests/common/dataset/sbom/rhn_satellite_6.17.json.bz2 new file mode 100644 index 000000000..b937e8f15 Binary files /dev/null and b/e2e/tests/common/dataset/sbom/rhn_satellite_6.17.json.bz2 differ diff --git a/e2e/tests/ui/assertions/DialogMatchers.ts b/e2e/tests/ui/assertions/DialogMatchers.ts new file mode 100644 index 000000000..edba9fe71 --- /dev/null +++ b/e2e/tests/ui/assertions/DialogMatchers.ts @@ -0,0 +1,35 @@ +import { expect as baseExpect } from "@playwright/test"; +import type { DeletionConfirmDialog } from "../pages/ConfirmDialog"; +import type { MatcherResult } from "./types"; + +export interface DialogMatchers { + toHaveTitle(expectedTitle: string): Promise; +} + +type DialogMatcherDefinitions = { + readonly [K in keyof DialogMatchers]: ( + receiver: DeletionConfirmDialog, + ...args: Parameters + ) => Promise; +}; + +export const dialogAssertions = baseExpect.extend({ + toHaveTitle: async ( + dialog: DeletionConfirmDialog, + expectedTitle: string, + ): Promise => { + try { + const dialogTitle = dialog.getDeletionConfirmDialogHeading(); + await baseExpect(dialogTitle).toHaveText(expectedTitle); + return { + pass: true, + message: () => `Dialog has title "${expectedTitle}"`, + }; + } catch (error) { + return { + pass: false, + message: () => (error instanceof Error ? error.message : String(error)), + }; + } + }, +}); diff --git a/e2e/tests/ui/assertions/index.ts b/e2e/tests/ui/assertions/index.ts index 58f71e451..7a877edb5 100644 --- a/e2e/tests/ui/assertions/index.ts +++ b/e2e/tests/ui/assertions/index.ts @@ -13,10 +13,14 @@ import type { Toolbar } from "../pages/Toolbar"; import type { TFilterValue } from "../pages/utils"; import { toolbarAssertions, type ToolbarMatchers } from "./ToolbarMatchers"; +import type { DeletionConfirmDialog } from "../pages/ConfirmDialog"; +import { dialogAssertions, type DialogMatchers } from "./DialogMatchers"; + const merged = mergeExpects( tableAssertions, paginationAssertions, toolbarAssertions, + dialogAssertions, // Add more custom assertions here ); @@ -59,6 +63,17 @@ function typedExpect< > & ToolbarMatchers; +/** + * Overload from DialogMatchers.ts + */ +function typedExpect( + value: DeletionConfirmDialog, +): Omit< + ReturnType>, + keyof DialogMatchers +> & + DialogMatchers; + // Default overload function typedExpect(value: T): ReturnType>; function typedExpect(value: T): unknown { diff --git a/e2e/tests/ui/features/@advisory-explorer/advisory-explorer.feature b/e2e/tests/ui/features/@advisory-explorer/advisory-explorer.feature index d084635e4..121c00e41 100644 --- a/e2e/tests/ui/features/@advisory-explorer/advisory-explorer.feature +++ b/e2e/tests/ui/features/@advisory-explorer/advisory-explorer.feature @@ -70,3 +70,24 @@ Scenario: Display vulnerabilities tied to a single advisory Examples: | advisoryName | vulnerabilityID | advisoryType | | CVE-2023-3223 | CVE-2023-3223 | csaf | + +Scenario: Delete an advisory from the Advisory Explorer page + Given User visits Advisory details Page of "" + When User Clicks on Actions button and Selects Delete option from the drop down + When User select Delete button from the Permanently delete Advisory model window + Then The Advisory deleted message is displayed + And Application Navigates to Advisory list page + And The "" should not be present on Advisory list page as it is deleted + Examples: + | advisoryID | + | CVE-2025-22130 | + +Scenario: Delete an advisory from the Advisory List Page + When User Deletes "" using the toggle option from Advisory List Page + When User select Delete button from the Permanently delete Advisory model window + Then The Advisory deleted message is displayed + And Application Navigates to Advisory list page + And The "" should not be present on Advisory list page as it is deleted + Examples: + | advisoryID | + | CVE-2023-1906 | diff --git a/e2e/tests/ui/features/@advisory-explorer/advisory-explorer.step.ts b/e2e/tests/ui/features/@advisory-explorer/advisory-explorer.step.ts index ec25cdb07..2212fea7d 100644 --- a/e2e/tests/ui/features/@advisory-explorer/advisory-explorer.step.ts +++ b/e2e/tests/ui/features/@advisory-explorer/advisory-explorer.step.ts @@ -1,8 +1,11 @@ import { createBdd } from "playwright-bdd"; -import { expect } from "playwright/test"; +import { expect } from "../../assertions"; import { ToolbarTable } from "../../helpers/ToolbarTable"; import { SearchPage } from "../../helpers/SearchPage"; +import { AdvisoryListPage } from "../../pages/advisory-list/AdvisoryListPage"; import { test } from "../../fixtures"; +import { DeletionConfirmDialog } from "../../pages/ConfirmDialog"; +import { DetailsPage } from "../../helpers/DetailsPage"; export const { Given, When, Then } = createBdd(test); @@ -151,3 +154,58 @@ Then( ).toBeVisible(); }, ); + +When( + "User Deletes {string} using the toggle option from Advisory List Page", + async ({ page }, advisoryID) => { + const listPage = await AdvisoryListPage.build(page); + const toolbar = await listPage.getToolbar(); + await toolbar.applyFilter({ "Filter text": advisoryID }); + const table = await listPage.getTable(); + const rowToDelete = 0; + await table.clickAction("Delete", rowToDelete); + }, +); + +When( + "User Clicks on Actions button and Selects Delete option from the drop down", + async ({ page }) => { + const details = new DetailsPage(page); + await details.clickOnPageAction("Delete"); + }, +); + +When( + "User select Delete button from the Permanently delete Advisory model window", + async ({ page }) => { + const dialog = await DeletionConfirmDialog.build(page, "Confirm dialog"); + await expect(dialog).toHaveTitle( + "Warning alert:Permanently delete Advisory?", + ); + await dialog.clickConfirm(); + }, +); + +Then( + "The {string} should not be present on Advisory list page as it is deleted", + async ({ page }, advisoryID: string) => { + const list = await AdvisoryListPage.build(page); + const toolbar = await list.getToolbar(); + const table = await list.getTable(); + await toolbar.applyFilter({ "Filter text": advisoryID }); + await expect(table).toHaveEmptyState(); + }, +); + +Then("Application Navigates to Advisory list page", async ({ page }) => { + await expect( + page.getByRole("heading", { level: 1, name: "Advisories" }), + ).toBeVisible(); +}); + +Then("The Advisory deleted message is displayed", async ({ page }) => { + const alertHeading = page.getByRole("heading", { level: 4 }).filter({ + hasText: /The Advisory .+ was deleted/, + }); + await expect(alertHeading).toBeVisible({ timeout: 10000 }); +}); diff --git a/e2e/tests/ui/features/@sbom-explorer/sbom-explorer.feature b/e2e/tests/ui/features/@sbom-explorer/sbom-explorer.feature index b0f53b1fd..c3e6a9d68 100644 --- a/e2e/tests/ui/features/@sbom-explorer/sbom-explorer.feature +++ b/e2e/tests/ui/features/@sbom-explorer/sbom-explorer.feature @@ -119,9 +119,7 @@ Feature: SBOM Explorer - View SBOM details When User visits SBOM details Page of "" When User selects the Tab "Vulnerabilities" Then Table column "Description" is not sortable - Then Sorting of "Id, Affected dependencies, Published, Updated" Columns Works - #Then Sorting of "CVSS" Columns works - # Bug: https://issues.redhat.com/browse/TC-2598 + Then Sorting of "Id, CVSS, Affected dependencies, Published, Updated" Columns Works Examples: | sbomName | | quarkus-bom | @@ -134,3 +132,25 @@ Feature: SBOM Explorer - View SBOM details Examples: | sbomName | Labels | | ubi9-minimal-container | RANDOM_LABELS | + + Scenario Outline: Delete SBOM from SBOM Explorer Page + Given An ingested SBOM "" is available + When User visits SBOM details Page of "" + When User Clicks on Actions button and Selects Delete option from the drop down + When User select Delete button from the Permanently delete SBOM model window + Then The SBOM deleted message is displayed + And Application Navigates to SBOM list page + And The "" should not be present on SBOM list page as it is deleted + Examples: + | sbomName | + | MRG-M-3.0.0 | + + Scenario Outline: Delete SBOM from SBOM List Page + When User Deletes "" using the toggle option from SBOM List Page + When User select Delete button from the Permanently delete SBOM model window + Then The SBOM deleted message is displayed + And Application Navigates to SBOM list page + And The "" should not be present on SBOM list page as it is deleted + Examples: + | sbomName | + | rhn_satellite | \ No newline at end of file diff --git a/e2e/tests/ui/features/@sbom-explorer/sbom-explorer.step.ts b/e2e/tests/ui/features/@sbom-explorer/sbom-explorer.step.ts index d17ef0b9c..c1fd5f28c 100644 --- a/e2e/tests/ui/features/@sbom-explorer/sbom-explorer.step.ts +++ b/e2e/tests/ui/features/@sbom-explorer/sbom-explorer.step.ts @@ -2,6 +2,8 @@ import { createBdd } from "playwright-bdd"; import { DetailsPage } from "../../helpers/DetailsPage"; import { ToolbarTable } from "../../helpers/ToolbarTable"; +import { DeletionConfirmDialog } from "../../pages/ConfirmDialog"; +import { SbomListPage } from "../../pages/sbom-list/SbomListPage"; import { test } from "../../fixtures"; import { expect } from "../../assertions"; @@ -168,3 +170,57 @@ Then( await detailsPage.verifyLabels(labelsToVerify, sbomName, infoSection); }, ); + +When( + "User Clicks on Actions button and Selects Delete option from the drop down", + async ({ page }) => { + const details = new DetailsPage(page); + await details.clickOnPageAction("Delete"); + }, +); + +When( + "User select Delete button from the Permanently delete SBOM model window", + async ({ page }) => { + const dialog = await DeletionConfirmDialog.build(page, "Confirm dialog"); + await expect(dialog).toHaveTitle("Warning alert:Permanently delete SBOM?"); + await dialog.clickConfirm(); + }, +); + +When( + "User Deletes {string} using the toggle option from SBOM List Page", + async ({ page }, sbomName) => { + const listPage = await SbomListPage.build(page); + const toolbar = await listPage.getToolbar(); + await toolbar.applyFilter({ "Filter text": sbomName }); + const table = await listPage.getTable(); + const rowToDelete = 0; + await table.clickAction("Delete", rowToDelete); + }, +); + +Then("Application Navigates to SBOM list page", async ({ page }) => { + await expect( + page.getByRole("heading", { level: 1, name: "SBOMs" }), + ).toBeVisible(); +}); + +Then( + "The {string} should not be present on SBOM list page as it is deleted", + async ({ page }, sbomName: string) => { + const list = await SbomListPage.build(page); + const toolbar = await list.getToolbar(); + const table = await list.getTable(); + await toolbar.applyFilter({ "Filter text": sbomName }); + await expect(table).toHaveEmptyState(); + }, +); + +Then("The SBOM deleted message is displayed", async ({ page }) => { + // PatternFly toast alerts render the title as a heading inside AlertGroup + const alertHeading = page.getByRole("heading", { level: 4 }).filter({ + hasText: /The SBOM .+ was deleted/, + }); + await expect(alertHeading).toBeVisible({ timeout: 10000 }); +}); diff --git a/e2e/tests/ui/pages/ConfirmDialog.ts b/e2e/tests/ui/pages/ConfirmDialog.ts new file mode 100644 index 000000000..a66c248a2 --- /dev/null +++ b/e2e/tests/ui/pages/ConfirmDialog.ts @@ -0,0 +1,28 @@ +import { expect, type Locator, type Page } from "@playwright/test"; + +export class DeletionConfirmDialog { + _deleteConfirmationDialog: Locator; + + private constructor(_deleteConfirmationDialog: Locator) { + this._deleteConfirmationDialog = _deleteConfirmationDialog; + } + + static async build(page: Page, dialogAriaLabel: string) { + const dialog = page.locator(`div[aria-label="${dialogAriaLabel}"]`); + await expect(dialog).toBeVisible(); + return new DeletionConfirmDialog(dialog); + } + + getDeletionConfirmDialogHeading() { + return this._deleteConfirmationDialog.getByRole("heading", { level: 1 }); + } + + async clickConfirm() { + const confirmBtn = this._deleteConfirmationDialog.getByRole("button", { + name: "confirm", + }); + await expect(confirmBtn).toBeVisible(); + await expect(confirmBtn).toBeEnabled(); + await confirmBtn.click(); + } +} diff --git a/e2e/tests/ui/pages/sbom-list/SbomListPage.ts b/e2e/tests/ui/pages/sbom-list/SbomListPage.ts index cfc873535..8cfb05b48 100644 --- a/e2e/tests/ui/pages/sbom-list/SbomListPage.ts +++ b/e2e/tests/ui/pages/sbom-list/SbomListPage.ts @@ -40,7 +40,7 @@ export class SbomListPage { Dependencies: { isSortable: false }, Vulnerabilities: { isSortable: false }, }, - ["Edit labels", "Download SBOM", "Download License Report"], + ["Edit labels", "Download SBOM", "Download License Report", "Delete"], ); }