diff --git a/playwright/UI/FilterTypePatch.spec.ts b/playwright/UI/FilterTypePatch.spec.ts new file mode 100644 index 000000000..4f54cb644 --- /dev/null +++ b/playwright/UI/FilterTypePatch.spec.ts @@ -0,0 +1,180 @@ +import { + test, + expect, + navigateToAdvisories, + navigateToPackages, + navigateToSystems, + closePopupsIfExist, + openConditionalFilter, + verifyFilterTypeExists, + applyFilterSubtype, + resetFilters, +} from 'test-utils'; + +/** + * Filter tests for Patch pages: Advisories, Packages, and Systems. + */ + +test.describe('Patch Filters', () => { + test('Filter types on Advisory page', async ({ page, request, systems }) => { + const system = await systems.add('filter-advisory-test', 'base'); + + // Fetch an advisory ID from the created system + const advisoriesResponse = await request + .get(`/api/patch/v3/systems/${system.id}/advisories?limit=1`) + .then((r) => r.json()); + const advisoryId = advisoriesResponse?.data?.[0]?.id ?? 'RHSA'; + + await navigateToAdvisories(page); + await closePopupsIfExist(page); + await openConditionalFilter(page); + + await test.step('Verify "Advisory" filter with search subtype', async () => { + await verifyFilterTypeExists(page, 'Advisory'); + await applyFilterSubtype(page, 'Advisory', { name: advisoryId, inputType: 'search' }); + + // Assert the advisory appears in the filtered results + await expect(page.getByRole('row').filter({ hasText: advisoryId })).toBeVisible(); + + await resetFilters(page); + }); + + await test.step('Verify "Type" filter with all subtypes', async () => { + await verifyFilterTypeExists(page, 'Type'); + + // Test each type filter and verify all results match + for (const typeValue of ['Security', 'Bugfix', 'Enhancement', 'Other']) { + await applyFilterSubtype(page, 'Type', { name: typeValue, inputType: 'checkbox' }); + + // Get all cells in the Type column using data-label attribute + const typeCells = page.locator('td[data-label="Type"]'); + const cellCount = await typeCells.count(); + + if (cellCount > 0) { + // Verify ALL Type column cells contain the filtered value + for (let i = 0; i < cellCount; i++) { + await expect(typeCells.nth(i)).toHaveText(typeValue); + } + } + + await resetFilters(page); + } + }); + + await test.step('Verify "Severity" filter with all subtypes', async () => { + await verifyFilterTypeExists(page, 'Severity'); + + // Test each severity filter and verify all results match + for (const severityValue of ['None', 'Low', 'Moderate', 'Important', 'Critical']) { + await applyFilterSubtype(page, 'Severity', { name: severityValue, inputType: 'checkbox' }); + + // Get all cells in the Severity column using data-label attribute + const severityCells = page.locator('td[data-label="Severity"]'); + const cellCount = await severityCells.count(); + + if (cellCount > 0) { + // Verify ALL Severity column cells contain the filtered value + for (let i = 0; i < cellCount; i++) { + await expect(severityCells.nth(i)).toHaveText(severityValue); + } + } + + await resetFilters(page); + } + }); + + await test.step('Verify "Publish date" filter with all subtypes', async () => { + await verifyFilterTypeExists(page, 'Publish date'); + + // Test each publish date option - dates can't be verified directly in cells + for (const dateValue of [ + 'Last 7 days', + 'Last 30 days', + 'Last 90 days', + 'Last year', + 'More than 1 year ago', + ]) { + await applyFilterSubtype(page, 'Publish date', { name: dateValue, inputType: 'option' }); + + // Verify table responds to filter (dates shown in cells won't match filter name) + await expect(page.getByRole('grid', { name: 'Patch table view' })).toBeVisible(); + + await resetFilters(page); + } + }); + + await test.step('Verify "Reboot" filter with all subtypes', async () => { + await verifyFilterTypeExists(page, 'Reboot'); + + // Test each reboot filter and verify all results match + for (const rebootValue of ['Required', 'Not required']) { + await applyFilterSubtype(page, 'Reboot', { name: rebootValue, inputType: 'checkbox' }); + + // Get all cells in the Reboot column using data-label attribute + const rebootCells = page.locator('td[data-label="Reboot"]'); + const cellCount = await rebootCells.count(); + + if (cellCount > 0) { + // Verify ALL Reboot column cells contain the filtered value + for (let i = 0; i < cellCount; i++) { + await expect(rebootCells.nth(i)).toHaveText(rebootValue); + } + } + + await resetFilters(page); + } + }); + }); + + test('Filter types on Packages page', async ({ page, systems }) => { + await systems.add('filter-packages-test', 'base'); + + await navigateToPackages(page); + await closePopupsIfExist(page); + await openConditionalFilter(page); + + await test.step('Verify "Package" filter exists', async () => { + await verifyFilterTypeExists(page, 'Package'); + }); + + await test.step('Verify "Patch status" filter exists', async () => { + await verifyFilterTypeExists(page, 'Patch status'); + }); + + await expect(page.getByRole('button', { name: 'Conditional filter toggle' })).toBeVisible(); + }); + + test('Filter types on Systems page', async ({ page, systems }) => { + await systems.add('filter-systems-test', 'base'); + + await navigateToSystems(page); + await closePopupsIfExist(page); + await openConditionalFilter(page); + + await test.step('Verify "Operating system" filter exists', async () => { + await verifyFilterTypeExists(page, 'Operating system'); + }); + + await test.step('Verify "Workspace" filter exists', async () => { + await verifyFilterTypeExists(page, 'Workspace'); + }); + + await test.step('Verify "Tag" filter exists', async () => { + await verifyFilterTypeExists(page, 'Tag'); + }); + + await test.step('Verify "System" filter exists', async () => { + await verifyFilterTypeExists(page, 'System'); + }); + + await test.step('Verify "Status" filter exists', async () => { + await verifyFilterTypeExists(page, 'Status'); + }); + + await test.step('Verify "Patch status" filter exists', async () => { + await verifyFilterTypeExists(page, 'Patch status'); + }); + + await expect(page.getByRole('button', { name: 'Conditional filter toggle' })).toBeVisible(); + }); +}); diff --git a/playwright/test-utils/helpers/filters.ts b/playwright/test-utils/helpers/filters.ts new file mode 100644 index 000000000..ce5f98990 --- /dev/null +++ b/playwright/test-utils/helpers/filters.ts @@ -0,0 +1,230 @@ +/** + * Filter interaction helpers for Playwright tests. + * + * This module provides utilities for: + * - Opening and interacting with conditional filter dropdowns + * - Applying different types of filters (checkbox, option, search) + * - Verifying filter types and subtypes exist + * - Verifying filter chips appear + * - Resetting filters + */ + +import { Page } from '@playwright/test'; +import { expect, waitForTableLoad } from 'test-utils'; + +type FilterInputType = 'checkbox' | 'option' | 'search'; + +export interface FilterConfig { + name: string; // Button/dropdown name (e.g., 'Type', 'Severity') + type: FilterInputType; // How to interact with the filter + value: string; // Value to select/enter +} + +/** + * Filter subtype configuration for verifying and applying filter subtypes. + */ +export interface FilterSubtype { + name: string; // Display name of the subtype (e.g., 'Security', 'Bugfix') + inputType: FilterInputType; // How to interact with this subtype +} + +/** + * Opens the conditional filter dropdown. + * + * @param page - Playwright Page object + */ +export const openConditionalFilter = async (page: Page) => { + await page.getByRole('button', { name: 'Conditional filter toggle' }).click(); +}; + +/** + * Verifies that a filter type exists in the filter dropdown. + * Opens the conditional filter dropdown if it's not already open. + * + * @param page - Playwright Page object + * @param filterType - Name of the filter type (e.g., 'Type', 'Severity', 'Advisory') + */ +export const verifyFilterTypeExists = async (page: Page, filterType: string) => { + const menuitem = page.getByRole('menuitem', { name: filterType }); + + // Open the conditional filter dropdown if the menuitem isn't visible + if (!(await menuitem.isVisible())) { + await openConditionalFilter(page); + } + + await expect(menuitem).toBeVisible(); +}; + +/** + * Selects a filter type from the conditional filter dropdown. + * Opens the dropdown if it's not already open, then clicks the filter type. + * + * @param page - Playwright Page object + * @param filterType - Name of the filter type to select + */ +export const selectFilterType = async (page: Page, filterType: string) => { + const menuitem = page.getByRole('menuitem', { name: filterType }); + + // Open the conditional filter dropdown if the menuitem isn't visible + if (!(await menuitem.isVisible())) { + await openConditionalFilter(page); + } + + await menuitem.click(); +}; + +/** + * Verify a filter chip is visible. + * + * @param page - Playwright Page object + * @param text - Text to find in the filter chip + */ +export const expectFilterChip = async (page: Page, text: string) => { + await expect(page.locator('.pf-v6-c-label__content').filter({ hasText: text })).toBeVisible(); +}; + +/** + * Verify a filter chip is hidden/removed. + * + * @param page - Playwright Page object + * @param text - Text that should not be in any filter chip + */ +export const expectFilterChipHidden = async (page: Page, text: string) => { + await expect(page.locator('.pf-v6-c-label__content').filter({ hasText: text })).toBeHidden(); +}; + +/** + * Applies a filter with its subtype value and optionally verifies the filter chip. + * Handles different input types: search, checkbox, and option/select. + * + * @param page - Playwright Page object + * @param filterType - Name of the filter type (e.g., 'Type', 'Severity') + * @param subtype - Subtype configuration with name and input type + * @param options - Optional settings + * @param options.verifyChip - Whether to verify the filter chip appears (default: true) + * @param options.chipText - Custom text to verify in the chip (defaults to subtype.name) + */ +export const applyFilterSubtype = async ( + page: Page, + filterType: string, + subtype: FilterSubtype, + options: { verifyChip?: boolean; chipText?: string } = {}, +) => { + const { verifyChip = true, chipText = subtype.name } = options; + + // Select the filter type first + await selectFilterType(page, filterType); + + // Apply the subtype based on its input type + switch (subtype.inputType) { + case 'search': + await page.getByRole('textbox', { name: 'search-field' }).fill(subtype.name); + break; + + case 'checkbox': { + const dropdown = page.getByRole('button', { name: 'Options menu' }); + await dropdown.click(); + await page.getByRole('menuitem', { name: subtype.name, exact: true }).click(); + // Dropdown auto-closes after clicking menuitem, wait for table to update + break; + } + + case 'option': { + const dropdown = page.getByRole('button', { name: 'Options menu' }); + await dropdown.click(); + await page.getByRole('option', { name: subtype.name }).click(); + break; + } + } + + await waitForTableLoad(page); + + // Verify the filter chip if requested + if (verifyChip) { + await expectFilterChip(page, chipText); + } +}; + +/** + * Verifies subtypes exist for a filter type. + * Opens the filter dropdown and checks that all specified subtypes are present. + * + * @param page - Playwright Page object + * @param filterType - Name of the filter type + * @param subtypes - Array of subtype names to verify + */ +export const verifyFilterSubtypesExist = async ( + page: Page, + filterType: string, + subtypes: string[], +) => { + // Select the filter type + await selectFilterType(page, filterType); + + // Open the dropdown to see subtypes + const dropdown = page.getByRole('button', { name: 'Options menu' }); + await dropdown.click(); + + // Verify each subtype exists + for (const subtype of subtypes) { + await expect( + page + .getByRole('menuitem', { name: subtype, exact: true }) + .or(page.getByRole('option', { name: subtype, exact: true })), + ).toBeVisible(); + } + + // Close the dropdown + await dropdown.click(); +}; + +/** + * Apply a single filter to the page. + * + * @param page - Playwright Page object + * @param filter - Filter configuration + */ +export const applyFilter = async (page: Page, filter: FilterConfig) => { + if (filter.type === 'search') { + await page.getByRole('textbox', { name: 'search-field' }).fill(filter.value); + } else { + const dropdown = page.getByRole('button', { name: 'Options menu' }); + await dropdown.click(); + + if (filter.type === 'checkbox') { + await page.getByRole('menuitem', { name: filter.value, exact: true }).click(); + // Dropdown auto-closes after clicking menuitem + } else { + await page.getByRole('option', { name: filter.value, exact: true }).click(); + } + } + await waitForTableLoad(page); +}; + +/** + * Remove/uncheck a filter value. + * + * @param page - Playwright Page object + * @param filter - Filter configuration (only works for checkbox type) + */ +export const removeFilter = async (page: Page, filter: FilterConfig) => { + if (filter.type === 'checkbox') { + const dropdown = page.getByRole('button', { name: 'Options menu' }); + await dropdown.click(); + await page.getByRole('menuitem', { name: filter.value, exact: true }).click(); + // Dropdown auto-closes after clicking menuitem + await waitForTableLoad(page); + } +}; + +/** + * Reset/clear all filters. + * + * @param page - Playwright Page object + */ +export const resetFilters = async (page: Page) => { + // Close any open dropdowns first + await page.keyboard.press('Escape'); + await page.getByRole('button', { name: /Reset filters/i }).click(); + await waitForTableLoad(page); +}; diff --git a/playwright/test-utils/helpers/index.ts b/playwright/test-utils/helpers/index.ts index 8db3f7b8a..80d351531 100644 --- a/playwright/test-utils/helpers/index.ts +++ b/playwright/test-utils/helpers/index.ts @@ -3,3 +3,4 @@ export * from './auth'; export * from './navigation'; export * from './systems'; export * from './tables'; +export * from './filters';