From 974ee5c10722e959fc0375ae18be5851575041d1 Mon Sep 17 00:00:00 2001 From: mlahane Date: Tue, 20 Jan 2026 09:27:57 -0500 Subject: [PATCH] test: add filter type patch tests Support exact match in filter helper Use exact match for menu items https://issues.redhat.com/browse/HMS-9726 --- playwright/UI/FilterTypePatch.spec.ts | 267 +++++++++++++++++++++++ playwright/test-utils/helpers/filters.ts | 265 ++++++++++++++++++++++ playwright/test-utils/helpers/index.ts | 1 + 3 files changed, 533 insertions(+) create mode 100644 playwright/UI/FilterTypePatch.spec.ts create mode 100644 playwright/test-utils/helpers/filters.ts diff --git a/playwright/UI/FilterTypePatch.spec.ts b/playwright/UI/FilterTypePatch.spec.ts new file mode 100644 index 000000000..6c49954ba --- /dev/null +++ b/playwright/UI/FilterTypePatch.spec.ts @@ -0,0 +1,267 @@ +import { + test, + expect, + navigateToAdvisories, + navigateToPackages, + navigateToSystems, + closePopupsIfExist, + openConditionalFilter, + verifyFilterTypeExists, + applyFilterSubtype, + resetFilters, + waitForTableLoad, + getPublishDateCutoff, + parsePublishDateCell, +} 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 exactly one data row exists (header + 1 row) and that it contains the advisory ID + const rows = page.getByRole('row'); + await expect(rows).toHaveCount(2); + await expect(rows.filter({ hasText: advisoryId })).toHaveCount(1); + + 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'); + + 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' }); + + await expect(page.getByRole('grid', { name: 'Patch table view' })).toBeVisible(); + + const { minDate, maxDate } = getPublishDateCutoff(dateValue); + const publishDateCells = page.locator('td[data-label="Publish date"]'); + const cellCount = await publishDateCells.count(); + + if (cellCount > 0) { + for (let i = 0; i < cellCount; i++) { + const text = await publishDateCells.nth(i).textContent(); + const cellDate = parsePublishDateCell(text ?? ''); + if (cellDate) { + if (minDate !== undefined) { + // Cell shows date only; filter is "not older than X" so date must be >= cutoff + expect( + cellDate.getTime(), + `Publish date "${text}" should not be older than ${dateValue}`, + ).toBeGreaterThanOrEqual(minDate.getTime()); + } else if (maxDate !== undefined) { + // "More than 1 year ago" means date must be before the cutoff + expect( + cellDate.getTime(), + `Publish date "${text}" should be older than 1 year`, + ).toBeLessThanOrEqual(maxDate.getTime()); + } + } + } + } + + 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 waitForTableLoad(page); + + // Use a package name from the table so the filter is guaranteed to match a visible row + const firstPackageCell = page.locator('td[data-label="Name"]').first(); + await firstPackageCell.waitFor({ state: 'visible' }); + const packageName = (await firstPackageCell.textContent())?.trim(); + expect(packageName, 'Packages table should have at least one row').toBeDefined(); + + await openConditionalFilter(page); + + await test.step('Verify "Package" filter exists', async () => { + await verifyFilterTypeExists(page, 'Package'); + }); + + await test.step('Verify "Package" filter with search displays expected package', async () => { + await applyFilterSubtype(page, 'Package', { name: packageName!, inputType: 'search' }); + + const rows = page.getByRole('row'); + await expect(rows).toHaveCount(2); + await expect(rows.filter({ hasText: packageName })).toHaveCount(1); + + await resetFilters(page); + }); + + await test.step('Verify "Patch status" filter with all subtypes', async () => { + await verifyFilterTypeExists(page, 'Patch status'); + + // "Systems up to date": Packages default is "Systems with patches available". Uncheck it + // first, then check "Systems up to date" so only eq:0 is sent (API drops filter if both are sent). + await resetFilters(page); + await openConditionalFilter(page); + await applyFilterSubtype( + page, + 'Patch status', + { name: 'Systems with patches available', inputType: 'checkbox' }, + { verifyChip: false }, + ); + await openConditionalFilter(page); + await applyFilterSubtype(page, 'Patch status', { + name: 'Systems up to date', + inputType: 'checkbox', + }); + + let applicableCells = page.locator('td[data-label="Applicable systems"]'); + let cellCount = await applicableCells.count(); + if (cellCount > 0) { + for (let i = 0; i < cellCount; i++) { + await expect(applicableCells.nth(i)).toHaveText('0'); + } + } + + // "Systems with patches available": reset restores default (gt:0); no need to click again. + await resetFilters(page); + + applicableCells = page.locator('td[data-label="Applicable systems"]'); + cellCount = await applicableCells.count(); + if (cellCount > 0) { + for (let i = 0; i < cellCount; i++) { + const text = (await applicableCells.nth(i).textContent())?.trim() ?? ''; + const num = parseInt(text, 10); + expect( + Number.isNaN(num) ? 0 : num, + 'Applicable systems should be > 0 for "Systems with patches available"', + ).toBeGreaterThan(0); + } + } + }); + + 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', true); + }); + + await test.step('Verify "Status" filter exists', async () => { + await verifyFilterTypeExists(page, 'Status', true); + }); + + 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..3f306181c --- /dev/null +++ b/playwright/test-utils/helpers/filters.ts @@ -0,0 +1,265 @@ +/** + * 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 + * - Publish date filter helpers (cutoff calculation, cell parsing) + */ + +import { Page } from '@playwright/test'; + +/** Returns { minDate } for "Last X" filters or { maxDate } for "More than 1 year ago". */ +export function getPublishDateCutoff(filterLabel: string): { + minDate?: Date; + maxDate?: Date; +} { + const now = new Date(); + const dayMs = 24 * 60 * 60 * 1000; + if (filterLabel === 'More than 1 year ago') { + const max = new Date(now.getTime() - 365 * dayMs); + return { maxDate: max }; + } + const days = + filterLabel === 'Last 7 days' + ? 7 + : filterLabel === 'Last 30 days' + ? 30 + : filterLabel === 'Last 90 days' + ? 90 + : 365; + const min = new Date(now.getTime() - days * dayMs); + return { minDate: min }; +} + +/** Parse date from table cell (format "DD Mon YYYY" from processDate). */ +export function parsePublishDateCell(text: string): Date | null { + const trimmed = text.trim(); + if (trimmed === 'N/A' || !trimmed) { + return null; + } + const d = new Date(trimmed); + return Number.isNaN(d.getTime()) ? null : d; +} +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') + * @param exact - If true, match the filter type name exactly (e.g. "System" not "Operating system") + */ +export const verifyFilterTypeExists = async (page: Page, filterType: string, exact?: boolean) => { + const menuitem = page.getByRole('menuitem', { name: filterType, exact: exact ?? false }); + + // 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';