tests: Add UI tests for the Search page#721
tests: Add UI tests for the Search page#721matejnesuta wants to merge 30 commits intoguacsec:mainfrom
Conversation
Reviewer's GuideThis PR implements end-to-end BDD search tests by extending and refactoring UI helper classes for robust table and page interactions (pagination, filtering, sorting, autocomplete, tabs), adds new step definitions and feature files for search scenarios, and updates global test configuration to capture failure artifacts. Sequence diagram for search autocomplete interactionsequenceDiagram
actor User
participant "SearchPage"
participant "Autocomplete Engine"
User->>SearchPage: Type in search box
SearchPage->>"Autocomplete Engine": Request suggestions
"Autocomplete Engine"-->>SearchPage: Return suggestions
SearchPage-->>User: Display suggestions
User->>SearchPage: Select suggestion
SearchPage->>SearchPage: Validate selection
SearchPage-->>User: Show filtered results
Sequence diagram for table sorting and pagination in search resultssequenceDiagram
actor User
participant "ToolbarTable"
User->>ToolbarTable: Click column to sort
ToolbarTable->>ToolbarTable: Sort rows
ToolbarTable-->>User: Display sorted table
User->>ToolbarTable: Click next page
ToolbarTable->>ToolbarTable: Load next page (maxPages limit)
ToolbarTable-->>User: Display new page
Class diagram for updated UI helpers (ToolbarTable, SearchPage, Tabs, DetailsPage)classDiagram
class ToolbarTable {
+verifyColumnContents()
+verifyDownloadLinks()
+verifyRowLimits()
+verifyVisibility()
+verifyDateFilters()
+navigate()
+verifyLinkPresence()
+getTableRows(maxPages)
+sortColumn()
+waitForTableLoad()
+waitForPaginationControls()
+waitForSorting()
}
class SearchPage {
+openSearchPage()
+typeAutocomplete()
+validateAutocompleteSuggestions()
+countResultsByCategory()
}
class Tabs {
+selectTab()
+verifyTabVisibility()
+verifySelectionState()
+verifyBadgeCounts()
}
class DetailsPage {
+verifyPageHeader()
}
ToolbarTable <|-- SearchPage
SearchPage o-- Tabs
SearchPage o-- ToolbarTable
DetailsPage o-- ToolbarTable
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey there - I've reviewed your changes - here's some feedback:
- The ToolbarTable helper methods use a lot of explicit any types—consider replacing those with stronger types (e.g. string) to improve type safety and clarity.
- Selectors like "#autocomplete-search .pf-v6-c-menu" and table locators are repeated—extract them into shared constants or private helper methods to reduce duplication and ease maintenance.
- Several step definitions bundle large conditional branches for different types; refactor these into parameterized or data-driven steps to make the BDD definitions more concise and maintainable.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The ToolbarTable helper methods use a lot of explicit any types—consider replacing those with stronger types (e.g. string) to improve type safety and clarity.
- Selectors like "#autocomplete-search .pf-v6-c-menu" and table locators are repeated—extract them into shared constants or private helper methods to reduce duplication and ease maintenance.
- Several step definitions bundle large conditional branches for different types; refactor these into parameterized or data-driven steps to make the BDD definitions more concise and maintainable.
## Individual Comments
### Comment 1
<location> `e2e/tests/ui/features/@search/search.step.ts:117` </location>
<code_context>
+ await detailsPage.verifyPageHeader(arg);
+});
+
+Then('the user should be able to filter {string}', async ({page}, arg: string) => {
+ const table = new ToolbarTable(page,getTableInfo(arg)[0]);
+ if (arg === "SBOMs"){
</code_context>
<issue_to_address>
**suggestion (testing):** Add negative filter tests to verify that filtering for non-existent values yields no results.
Consider adding tests that filter for values not present in the dataset to confirm the UI handles empty results as expected.
Suggested implementation:
```typescript
Then('the user should be able to filter {string}', async ({page}, arg: string) => {
const table = new ToolbarTable(page,getTableInfo(arg)[0]);
if (arg === "SBOMs"){
await table.filterByDate("12/22/2025","12/22/2025");
await table.verifyColumnDoesNotContainText("Name","quarkus-bom");
await table.clearFilter();
await table.verifyColumnContainsText("Name","quarkus-bom");
// Negative filter test: filter for a non-existent SBOM name
await table.filterByText("Name", "non-existent-sbom");
await table.verifyColumnDoesNotContainText("Name", "non-existent-sbom");
await table.verifyTableIsEmpty(); // Optionally verify table is empty
await table.clearFilter();
}else if (arg === "Vulnerabilities"){
await page.getByLabel('Critical').check();
await table.verifyColumnDoesNotContainText("ID","CVE-2022-45787");
await table.clearFilter();
await table.verifyColumnContainsText("ID","CVE-2022-45787");
// Negative filter test: filter for a non-existent Vulnerability ID
await table.filterByText("ID", "NON-EXISTENT-CVE");
await table.verifyColumnDoesNotContainText("ID", "NON-EXISTENT-CVE");
await table.verifyTableIsEmpty(); // Optionally verify table is empty
await table.clearFilter();
}else if (arg == "Packages"){
```
- Ensure that the `ToolbarTable` class has a `filterByText` and `verifyTableIsEmpty` method. If not, you will need to implement these helper methods.
- Add similar negative filter tests for "Packages" if applicable, using a column and value that do not exist.
</issue_to_address>
### Comment 2
<location> `e2e/tests/ui/features/@search/search.step.ts:161-166` </location>
<code_context>
+});
+
+
+Then('the {string} list should be sortable', async ({page}, arg: string) => {
+ var columns:string[] = getColumns(arg);
+ var id:string = getPaginationId(arg);
+
+ const table = new ToolbarTable(page,getTableInfo(arg)[0]);
+ await table.verifySorting(`xpath=//div[@id="${id}"]`,columns);
+});
+
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests for sorting columns with mixed data types and blank cells.
Please add tests that verify sorting behavior for columns containing both valid values and blank cells, to confirm correct handling after the recent logic update.
Suggested implementation:
```typescript
Then('the {string} list should be sortable', async ({page}, arg: string) => {
var columns:string[] = getColumns(arg);
var id:string = getPaginationId(arg);
const table = new ToolbarTable(page,getTableInfo(arg)[0]);
await table.verifySorting(`xpath=//div[@id="${id}"]`,columns);
});
// Test sorting for columns with mixed data types and blank cells
Then('the {string} list should sort columns with mixed data types and blank cells correctly', async ({page}, arg: string) => {
// Example: Assume getMixedTypeColumns returns columns with mixed types and blank cells for the given list
var mixedColumns: string[] = getMixedTypeColumns(arg);
var id: string = getPaginationId(arg);
const table = new ToolbarTable(page, getTableInfo(arg)[0]);
for (const column of mixedColumns) {
// Optionally, you can pass a custom comparator or check for blank cell handling if verifySorting supports it
await table.verifySorting(`xpath=//div[@id="${id}"]`, [column]);
}
});
```
- You will need to implement or update the `getMixedTypeColumns(arg: string): string[]` function to return the relevant columns for each list that contain mixed data types and blank cells.
- Ensure your test data in the UI contains columns with both valid values and blank cells for the lists being tested.
- If `verifySorting` does not already check for blank cell handling, you may need to enhance it to do so.
</issue_to_address>
### Comment 3
<location> `e2e/tests/ui/features/@search/search.step.ts:209-212` </location>
<code_context>
+ await tabs.verifyTabHasAtLeastResults(arg,count);
+});
+
+Then('the autofill dropdown should display items matching the {string}', async ({page}, arg: string) => {
+ const searchPage = new SearchPage(page);
+ await searchPage.autoFillHasRelevantResults(arg);
+});
+
</code_context>
<issue_to_address>
**suggestion (testing):** Consider testing autocomplete with special characters and very long input strings.
Testing with these inputs will help catch potential UI issues and ensure the autocomplete remains reliable.
```suggestion
Then('the autofill dropdown should display items matching the {string}', async ({page}, arg: string) => {
const searchPage = new SearchPage(page);
await searchPage.autoFillHasRelevantResults(arg);
});
Then('the autofill dropdown should handle special characters input', async ({page}) => {
const searchPage = new SearchPage(page);
const specialCharsInput = '!@#$%^&*()_+-=[]{}|;:\'",.<>/?`~';
await searchPage.autoFillHasRelevantResults(specialCharsInput);
});
Then('the autofill dropdown should handle very long input strings', async ({page}) => {
const searchPage = new SearchPage(page);
const longInput = 'a'.repeat(256); // 256 characters long
await searchPage.autoFillHasRelevantResults(longInput);
});
```
</issue_to_address>
### Comment 4
<location> `e2e/tests/ui/features/@search/search.step.ts:162` </location>
<code_context>
</code_context>
<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/avoid-using-var))
<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.
From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>
### Comment 5
<location> `e2e/tests/ui/features/@search/search.step.ts:163` </location>
<code_context>
</code_context>
<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/avoid-using-var))
<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.
From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>
### Comment 6
<location> `e2e/tests/ui/features/@search/search.step.ts:179` </location>
<code_context>
</code_context>
<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/avoid-using-var))
<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.
From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>
### Comment 7
<location> `e2e/tests/ui/features/@search/search.step.ts:188` </location>
<code_context>
</code_context>
<issue_to_address>
**issue (code-quality):** Use `const` or `let` instead of `var`. ([`avoid-using-var`](https://docs.sourcery.ai/Reference/Rules-and-In-Line-Suggestions/TypeScript/Default-Rules/avoid-using-var))
<details><summary>Explanation</summary>`const` is preferred as it ensures you cannot reassign references (which can lead to buggy and confusing code).
`let` may be used if you need to reassign references - it's preferred to `var` because it is block- rather than
function-scoped.
From the [Airbnb JavaScript Style Guide](https://airbnb.io/javascript/#references--prefer-const)
</details>
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| Then('the {string} list should be sortable', async ({page}, arg: string) => { | ||
| var columns:string[] = getColumns(arg); | ||
| var id:string = getPaginationId(arg); | ||
|
|
||
| const table = new ToolbarTable(page,getTableInfo(arg)[0]); | ||
| await table.verifySorting(`xpath=//div[@id="${id}"]`,columns); |
There was a problem hiding this comment.
suggestion (testing): Add tests for sorting columns with mixed data types and blank cells.
Please add tests that verify sorting behavior for columns containing both valid values and blank cells, to confirm correct handling after the recent logic update.
Suggested implementation:
Then('the {string} list should be sortable', async ({page}, arg: string) => {
var columns:string[] = getColumns(arg);
var id:string = getPaginationId(arg);
const table = new ToolbarTable(page,getTableInfo(arg)[0]);
await table.verifySorting(`xpath=//div[@id="${id}"]`,columns);
});
// Test sorting for columns with mixed data types and blank cells
Then('the {string} list should sort columns with mixed data types and blank cells correctly', async ({page}, arg: string) => {
// Example: Assume getMixedTypeColumns returns columns with mixed types and blank cells for the given list
var mixedColumns: string[] = getMixedTypeColumns(arg);
var id: string = getPaginationId(arg);
const table = new ToolbarTable(page, getTableInfo(arg)[0]);
for (const column of mixedColumns) {
// Optionally, you can pass a custom comparator or check for blank cell handling if verifySorting supports it
await table.verifySorting(`xpath=//div[@id="${id}"]`, [column]);
}
});- You will need to implement or update the
getMixedTypeColumns(arg: string): string[]function to return the relevant columns for each list that contain mixed data types and blank cells. - Ensure your test data in the UI contains columns with both valid values and blank cells for the lists being tested.
- If
verifySortingdoes not already check for blank cell handling, you may need to enhance it to do so.
| Then('the autofill dropdown should display items matching the {string}', async ({page}, arg: string) => { | ||
| const searchPage = new SearchPage(page); | ||
| await searchPage.autoFillHasRelevantResults(arg); | ||
| }); |
There was a problem hiding this comment.
suggestion (testing): Consider testing autocomplete with special characters and very long input strings.
Testing with these inputs will help catch potential UI issues and ensure the autocomplete remains reliable.
| Then('the autofill dropdown should display items matching the {string}', async ({page}, arg: string) => { | |
| const searchPage = new SearchPage(page); | |
| await searchPage.autoFillHasRelevantResults(arg); | |
| }); | |
| Then('the autofill dropdown should display items matching the {string}', async ({page}, arg: string) => { | |
| const searchPage = new SearchPage(page); | |
| await searchPage.autoFillHasRelevantResults(arg); | |
| }); | |
| Then('the autofill dropdown should handle special characters input', async ({page}) => { | |
| const searchPage = new SearchPage(page); | |
| const specialCharsInput = '!@#$%^&*()_+-=[]{}|;:\'",.<>/?`~'; | |
| await searchPage.autoFillHasRelevantResults(specialCharsInput); | |
| }); | |
| Then('the autofill dropdown should handle very long input strings', async ({page}) => { | |
| const searchPage = new SearchPage(page); | |
| const longInput = 'a'.repeat(256); // 256 characters long | |
| await searchPage.autoFillHasRelevantResults(longInput); | |
| }); |
vobratil
left a comment
There was a problem hiding this comment.
@matejnesuta There are some useful scenarios being tested in this PR, but unfortunately I think it introduces a lot of redundancy and uses the helper classes that are now deprecated. Could you please move any new page-related functionality to appropriate classes in the pages directory (not the helpers directory) and re-factor the PR, so that it uses what is already defined there?
mrrajan
left a comment
There was a problem hiding this comment.
@matejnesuta I regret for the late review - Please find my review comments. Additionally,
- Capitalize the first word on the feature file
- On the typescript methods and step definitions, use relevant naming instead of
arg
Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: Carlos Feria <2582866+carlosthe19916@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Carlos Feria <2582866+carlosthe19916@users.noreply.github.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #721 +/- ##
==========================================
+ Coverage 64.98% 65.75% +0.76%
==========================================
Files 195 195
Lines 3339 3341 +2
Branches 751 753 +2
==========================================
+ Hits 2170 2197 +27
+ Misses 872 845 -27
- Partials 297 299 +2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
carlosthe19916
left a comment
There was a problem hiding this comment.
Thanks for the PR. I made a quick review mainly focusing in the patterns for code to be applied. Please see my commends below
e2e/tests/ui/pages/Table.ts
Outdated
| static getTableInfo(type: string): { columnKey: string; columnName: string } { | ||
| switch (type) { | ||
| case "SBOMs": | ||
| case "SBOM": | ||
| return { columnKey: "name", columnName: "Name" }; | ||
| case "Advisories": | ||
| case "Advisory": | ||
| return { columnKey: "identifier", columnName: "ID" }; | ||
| case "Vulnerabilities": | ||
| case "CVE": | ||
| return { columnKey: "identifier", columnName: "ID" }; | ||
| case "Packages": | ||
| case "Package": | ||
| return { columnKey: "name", columnName: "Name" }; | ||
| default: | ||
| throw new Error(`Unknown type: ${type}`); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Returns sortable column names for a given entity type | ||
| * @param type Category of the data (e.g., "SBOMs", "Packages", "Vulnerabilities", "Advisories") | ||
| * @returns Array of sortable column names for the entity type | ||
| */ | ||
| static getSortableColumns(type: string): string[] { | ||
| switch (type) { | ||
| case "Vulnerabilities": | ||
| return ["ID", "CVSS", "Published"]; | ||
| case "Advisories": | ||
| return ["ID", "Revision"]; | ||
| case "Packages": | ||
| return ["Name", "Namespace", "Version"]; | ||
| case "SBOMs": | ||
| return ["Name", "Created on"]; | ||
| default: | ||
| throw new Error(`Unknown type: ${type}`); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Converts plural entity type to singular form | ||
| * @param pluralType Plural entity type (e.g., "SBOMs", "Packages", "Vulnerabilities", "Advisories") | ||
| * @returns Singular form of the entity type | ||
| */ | ||
| static toSingular(pluralType: string): string { | ||
| switch (pluralType) { | ||
| case "SBOMs": | ||
| return "SBOM"; | ||
| case "Packages": | ||
| return "Package"; | ||
| case "Vulnerabilities": | ||
| return "CVE"; | ||
| case "Advisories": | ||
| return "Advisory"; | ||
| default: | ||
| throw new Error(`Unknown plural type: ${pluralType}`); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Returns the default sort configuration for a given entity type | ||
| * @param type Category of the data (e.g., "SBOMs", "Packages", "Vulnerabilities", "Advisories") | ||
| * @returns Object containing the default sort column name and direction | ||
| */ | ||
| static getDefaultSort(type: string): { | ||
| column: string; | ||
| direction: "ascending" | "descending"; | ||
| } { | ||
| switch (type) { | ||
| case "SBOMs": | ||
| return { column: "Name", direction: "ascending" }; | ||
| case "Packages": | ||
| return { column: "Name", direction: "ascending" }; | ||
| case "Vulnerabilities": | ||
| return { column: "Published", direction: "descending" }; | ||
| case "Advisories": | ||
| return { column: "Revision", direction: "descending" }; | ||
| default: | ||
| throw new Error(`Unknown type: ${type}`); | ||
| } | ||
| } |
There was a problem hiding this comment.
Could we move this logic outside the class Table?
The class Table should contain logic can can be reused across any page and not be constraint with any particular one.
If the content of the Table class cannot be moved and applied to an external repository then likely that code does not belong to this class.
e2e/tests/ui/pages/SearchPageTabs.ts
Outdated
| async verifyTabHasAtLeastResults(tabName: string, minCount: number) { | ||
| const tab = this._page.locator("button[role='tab']", { hasText: tabName }); | ||
| const badge = tab.locator(".pf-v6-c-badge"); | ||
|
|
||
| // Wait until the badge has some text | ||
| await expect(badge).toHaveText(/[\d]/, { timeout: 60000 }); | ||
|
|
||
| const countText = await badge.textContent(); | ||
|
|
||
| // Remove anything that isn't a digit | ||
| const match = countText?.match(/\d+/); | ||
| if (!match) { | ||
| throw new Error( | ||
| `Could not parse badge count for tab "${tabName}": got "${countText}"`, | ||
| ); | ||
| } | ||
|
|
||
| const count = parseInt(match[0], 10); | ||
| expect(count).toBeGreaterThanOrEqual(minCount); | ||
| } |
There was a problem hiding this comment.
SearchPageTabs should only contains Selectors and Action Triggers never assertions. Move this code outside this class.
| async autoFillIsNotVisible() { | ||
| const menu = this._page.locator("ul[role='menu']"); | ||
| await expect(menu).not.toBeVisible(); | ||
| } |
There was a problem hiding this comment.
This is an assertion, please move out of the class. Let's keep the pattern of not writing assertion function inside Page Objects.
Assertions are not necessarily reusable and pollutes reusable Page Objects.
| async expectCategoriesWithinLimitByHref(limit: number) { | ||
| const menu = this._page.locator("ul[role='menu']"); | ||
| const menuItems = menu.locator("li[role='none'] a"); | ||
|
|
||
| const categoryCount: Record<string, number> = { | ||
| advisories: 0, | ||
| packages: 0, | ||
| sboms: 0, | ||
| vulnerabilities: 0, | ||
| }; | ||
|
|
||
| const count = await menuItems.count(); | ||
|
|
||
| for (let i = 0; i < count; i++) { | ||
| const link = menuItems.nth(i); | ||
| const href = await link.getAttribute("href"); | ||
|
|
||
| if (href?.includes("/advisories/")) { | ||
| categoryCount.advisories++; | ||
| } else if (href?.includes("/packages/")) { | ||
| categoryCount.packages++; | ||
| } else if (href?.includes("/sboms/")) { | ||
| categoryCount.sboms++; | ||
| } else if (href?.includes("/vulnerabilities/")) { | ||
| categoryCount.vulnerabilities++; | ||
| } | ||
| } | ||
|
|
||
| // Each category should have at most 'limit' items | ||
| for (const [_category, count] of Object.entries(categoryCount)) { | ||
| expect(count).toBeLessThanOrEqual(limit); | ||
| } | ||
| } |
There was a problem hiding this comment.
Same as above, this whole function is about asserting things. Move it out of this class
Summary by Sourcery
Introduce end-to-end BDD tests for the Search page and extend helper classes to support search, filtering, pagination, sorting, and tab interactions across Vulnerabilities, SBOMs, Packages, and Advisories.
New Features:
Enhancements:
CI: