diff --git a/frontend/e2e/tests/dev-console/catalog.spec.ts b/frontend/e2e/tests/dev-console/catalog.spec.ts index 38618d5aebb..b2bec215d49 100644 --- a/frontend/e2e/tests/dev-console/catalog.spec.ts +++ b/frontend/e2e/tests/dev-console/catalog.spec.ts @@ -112,7 +112,17 @@ test.describe( await warmupSPA(page); }); - test('forces project selection when all namespaces URL is accessed [A-12-TC01]', async () => { + test('forces project selection when all namespaces URL is accessed [A-12-TC01]', async ({ + page, + }) => { + // TechPreview clusters run only a limited set of tests via test-prow-playwright-e2e-techpreview.sh + // and do not enable the Developer perspective. CatalogPage.tsx only shows the project selection + // message when isDevPerspective is true (see: `showCreateProjectListPage = isDevPerspective && + // isAllNamespaces`), so this test would always fail on TechPreview. Skip it there; it is + // covered by the standard e2e suite where the Developer perspective is available. + const isTechPreview = await page.evaluate(() => window.SERVER_FLAGS.techPreview); + test.skip(isTechPreview, 'Developer perspective is not enabled on TechPreview clusters'); + await test.step('Navigate to catalog all namespaces', async () => { await catalogPage.navigateToAllNamespacesCatalog(); }); @@ -176,3 +186,45 @@ test.describe( }); }, ); + +test.describe('Software Catalog operators', { tag: ['@dev-console', '@regression'] }, () => { + let catalogPage: CatalogPage; + + test.beforeEach(async ({ page }) => { + catalogPage = new CatalogPage(page); + await warmupSPA(page); + }); + + test('displays operator tiles in catalog', async ({ page }) => { + await test.step('Navigate to operator catalog', async () => { + await page.goto('/catalog/ns/default?catalogType=operator'); + }); + + await test.step('Verify operator tiles are rendered', async () => { + await expect(page.locator('[data-test^="operator-"]').first()).toBeVisible({ + timeout: 30_000, + }); + }); + }); + + test('filters operator tiles by keyword', async ({ page }) => { + await test.step('Navigate to operator catalog', async () => { + await page.goto('/catalog/ns/default?catalogType=operator'); + }); + + await test.step('Wait for tiles to render', async () => { + await expect(catalogPage.getCatalogTiles().first()).toBeVisible({ timeout: 30_000 }); + }); + + await test.step('Filter by non-matching keyword and verify no tiles shown', async () => { + await catalogPage.filterByKeyword('zzz-no-match-xyzzy'); + await expect(catalogPage.getCatalogTiles()).toHaveCount(0); + await expect(page.getByText('No results found')).toBeVisible(); + }); + + await test.step('Clear filter and verify tiles return', async () => { + await catalogPage.filterByKeyword(''); + await expect(catalogPage.getCatalogTiles().first()).toBeVisible(); + }); + }); +}); diff --git a/frontend/packages/operator-lifecycle-manager-v1/src/hooks/__tests__/useCatalogItems.spec.ts b/frontend/packages/operator-lifecycle-manager-v1/src/hooks/__tests__/useCatalogItems.spec.ts new file mode 100644 index 00000000000..563e7e6b7df --- /dev/null +++ b/frontend/packages/operator-lifecycle-manager-v1/src/hooks/__tests__/useCatalogItems.spec.ts @@ -0,0 +1,71 @@ +import { renderHook, waitFor } from '@testing-library/react'; +import { coFetch } from '@console/shared/src/utils/console-fetch'; +import { + getConsoleRequestHeaders, + normalizeConsoleHeaders, +} from '@console/shared/src/utils/console-fetch-utils'; +import useCatalogItems from '../useCatalogItems'; + +jest.mock('@console/shared/src/hooks/usePoll', () => { + const { useEffect } = jest.requireActual('react'); + return { + usePoll: function usePoll(callback: () => void) { + useEffect(() => { + callback(); + }, [callback]); + }, + }; +}); + +jest.mock('@console/shared/src/utils/console-fetch', () => ({ + coFetch: jest.fn(), +})); + +jest.mock('@console/shared/src/utils/console-fetch-utils', () => ({ + getConsoleRequestHeaders: jest.fn(), + normalizeConsoleHeaders: jest.fn(), +})); + +jest.mock('../../utils/catalog-item', () => ({ + normalizeCatalogItem: jest.fn((item) => item), +})); + +const coFetchMock = jest.mocked(coFetch); + +describe('useCatalogItems', () => { + beforeEach(() => { + jest.resetAllMocks(); + jest.mocked(getConsoleRequestHeaders).mockReturnValue({}); + jest.mocked(normalizeConsoleHeaders).mockReturnValue({}); + }); + + it('starts with loaded=false before fetch resolves', () => { + coFetchMock.mockReturnValue(new Promise(() => {})); + + const { result } = renderHook(() => useCatalogItems()); + + expect(result.current[1]).toBe(false); + }); + + it('sets loaded=true after a successful fetch', async () => { + coFetchMock.mockResolvedValue({ + status: 200, + headers: { get: () => 'Thu, 01 Jan 2026 00:00:00 GMT' }, + json: () => Promise.resolve([]), + } as unknown as Response); + + const { result } = renderHook(() => useCatalogItems()); + + await waitFor(() => expect(result.current[1]).toBe(true)); + expect(result.current[2]).toBe(''); + }); + + it('sets loaded=true after a fetch error', async () => { + coFetchMock.mockRejectedValue(new Error('Network error')); + + const { result } = renderHook(() => useCatalogItems()); + + await waitFor(() => expect(result.current[1]).toBe(true)); + expect(result.current[2]).toContain('Network error'); + }); +}); diff --git a/frontend/packages/operator-lifecycle-manager-v1/src/hooks/useCatalogCategories.ts b/frontend/packages/operator-lifecycle-manager-v1/src/hooks/useCatalogCategories.ts index 0f535059598..5ed8aade0c6 100644 --- a/frontend/packages/operator-lifecycle-manager-v1/src/hooks/useCatalogCategories.ts +++ b/frontend/packages/operator-lifecycle-manager-v1/src/hooks/useCatalogCategories.ts @@ -5,9 +5,9 @@ import useCatalogItems from './useCatalogItems'; type UseCatalogCategories = () => [CatalogCategory[], boolean, string]; const useCatalogCategories: UseCatalogCategories = () => { - const [items, loading, error] = useCatalogItems(); + const [items, loaded, error] = useCatalogItems(); const categories = useMemo(() => { - if (loading || error) { + if (!loaded || error) { return []; } return _.uniq( @@ -23,9 +23,9 @@ const useCatalogCategories: UseCatalogCategories = () => { tags: [id, label], }; }); - }, [error, items, loading]); + }, [error, items, loaded]); - return [categories, loading, error]; + return [categories, loaded, error]; }; export default useCatalogCategories; diff --git a/frontend/packages/operator-lifecycle-manager-v1/src/hooks/useCatalogItems.ts b/frontend/packages/operator-lifecycle-manager-v1/src/hooks/useCatalogItems.ts index b75a1439077..3ef5519c058 100644 --- a/frontend/packages/operator-lifecycle-manager-v1/src/hooks/useCatalogItems.ts +++ b/frontend/packages/operator-lifecycle-manager-v1/src/hooks/useCatalogItems.ts @@ -18,7 +18,7 @@ type OLMCatalogItemData = { type UseCatalogItems = () => [CatalogItem[], boolean, string]; const useCatalogItems: UseCatalogItems = () => { const [olmCatalogItems, setOLMCatalogItems] = useState([]); - const [loading, setLoading] = useState(true); + const [loaded, setLoaded] = useState(false); const [error, setError] = useState(''); const [lastModified, setLastModified] = useState(''); const abortControllerRef = useRef(); @@ -54,12 +54,12 @@ const useCatalogItems: UseCatalogItems = () => { if (olmItems !== null) { setOLMCatalogItems(olmItems); } - setLoading(false); + setLoaded(true); }) .catch((err) => { if (err.name === 'AbortError') return; setError(err.toString()); - setLoading(false); + setLoaded(true); }); }, [headers]); @@ -74,7 +74,7 @@ const useCatalogItems: UseCatalogItems = () => { const items = useMemo(() => olmCatalogItems.map(normalizeCatalogItem), [olmCatalogItems]); - return [items, loading, error]; + return [items, loaded, error]; }; export default useCatalogItems; diff --git a/frontend/packages/operator-lifecycle-manager/console-extensions.json b/frontend/packages/operator-lifecycle-manager/console-extensions.json index 7326cb98ccf..a5b98ece1f1 100644 --- a/frontend/packages/operator-lifecycle-manager/console-extensions.json +++ b/frontend/packages/operator-lifecycle-manager/console-extensions.json @@ -440,6 +440,9 @@ } } ] + }, + "flags": { + "disallowed": ["OLMV1_ENABLED"] } }, { diff --git a/test-prow-playwright-e2e-techpreview.sh b/test-prow-playwright-e2e-techpreview.sh index 611ad714601..83951b6faa6 100755 --- a/test-prow-playwright-e2e-techpreview.sh +++ b/test-prow-playwright-e2e-techpreview.sh @@ -1,8 +1,9 @@ #!/usr/bin/env bash # # Prow / CI entrypoint for Playwright E2E tech-preview tests against a live OpenShift cluster. -# Runs the operator lifecycle metadata test suite, which requires the OLMLifecycleAndCompatibility -# feature gate to be enabled on the cluster. +# Runs OLM tests that require Tech Preview to be enabled, including: +# - Operator catalog items display test (OLMv1 path active on Tech Preview clusters) +# - Operator lifecycle metadata test (requires OLMLifecycleAndCompatibility feature gate) # # Run from the openshift/console repository root. # @@ -42,6 +43,6 @@ export BRIDGE_BASE_ADDRESS="$(oc get consoles.config.openshift.io cluster -o jso pushd frontend -./integration-tests/test-playwright-e2e.sh -- --project=olm e2e/tests/olm/operator-lifecycle-metadata.spec.ts "$@" +./integration-tests/test-playwright-e2e.sh -- e2e/tests/olm/operator-lifecycle-metadata.spec.ts e2e/tests/dev-console/catalog.spec.ts "$@" popd