Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/chatmodes/playwright-tester.chatmode.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ mode: 'agent'
- Automatically run test with:
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.

suggestion (typo): Consider changing "run test" to "run tests" for grammatical correctness.

You could also phrase it as "Automatically run tests with:" to better match the plural and improve the sentence flow.

Suggested change
- Automatically run test with:
- Automatically run tests 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
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.

suggestion (typo): Tense, article usage, and punctuation could be improved in this sentence.

Consider rephrasing to: "In case of test failures, the above command launches an HTML server to host the test output. Press Ctrl+C to stop the server."


Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
35 changes: 35 additions & 0 deletions e2e/tests/ui/assertions/DialogMatchers.ts
Original file line number Diff line number Diff line change
@@ -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<MatcherResult>;
}

type DialogMatcherDefinitions = {
readonly [K in keyof DialogMatchers]: (
receiver: DeletionConfirmDialog,
...args: Parameters<DialogMatchers[K]>
) => Promise<MatcherResult>;
};

export const dialogAssertions = baseExpect.extend<DialogMatcherDefinitions>({
toHaveTitle: async (
dialog: DeletionConfirmDialog,
expectedTitle: string,
): Promise<MatcherResult> => {
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)),
};
}
},
});
15 changes: 15 additions & 0 deletions e2e/tests/ui/assertions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
);

Expand Down Expand Up @@ -59,6 +63,17 @@ function typedExpect<
> &
ToolbarMatchers<TFilter, TFilterName>;

/**
* Overload from DialogMatchers.ts
*/
function typedExpect(
value: DeletionConfirmDialog,
): Omit<
ReturnType<typeof merged<DeletionConfirmDialog>>,
keyof DialogMatchers
> &
DialogMatchers;

// Default overload
function typedExpect<T>(value: T): ReturnType<typeof merged<T>>;
function typedExpect<T>(value: T): unknown {
Expand Down
21 changes: 21 additions & 0 deletions e2e/tests/ui/features/@advisory-explorer/advisory-explorer.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<advisoryID>"
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 "<advisoryID>" 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 "<advisoryID>" 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 "<advisoryID>" should not be present on Advisory list page as it is deleted
Examples:
| advisoryID |
| CVE-2023-1906 |
Original file line number Diff line number Diff line change
@@ -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);

Expand Down Expand Up @@ -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();
Comment on lines +201 to +203
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This can be replaced by:

const navigation = await Navigation.build(page);
await navigation.goToSidebar("Advisories");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This step verifies that the application automatically navigated to the “Advisories” screen by confirming that the header is visible

});

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 });
});
26 changes: 23 additions & 3 deletions e2e/tests/ui/features/@sbom-explorer/sbom-explorer.feature
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,7 @@ Feature: SBOM Explorer - View SBOM details
When User visits SBOM details Page of "<sbomName>"
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 |
Expand All @@ -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 "<sbomName>" is available
When User visits SBOM details Page of "<sbomName>"
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 "<sbomName>" 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 "<sbomName>" 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 "<sbomName>" should not be present on SBOM list page as it is deleted
Examples:
| sbomName |
| rhn_satellite |
56 changes: 56 additions & 0 deletions e2e/tests/ui/features/@sbom-explorer/sbom-explorer.step.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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 });
});
28 changes: 28 additions & 0 deletions e2e/tests/ui/pages/ConfirmDialog.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}
2 changes: 1 addition & 1 deletion e2e/tests/ui/pages/sbom-list/SbomListPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
);
}

Expand Down