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
54 changes: 53 additions & 1 deletion frontend/e2e/tests/dev-console/catalog.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Expand Down Expand Up @@ -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();
});
});
});
Original file line number Diff line number Diff line change
@@ -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');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type OLMCatalogItemData = {
type UseCatalogItems = () => [CatalogItem<OLMCatalogItemData>[], boolean, string];
const useCatalogItems: UseCatalogItems = () => {
const [olmCatalogItems, setOLMCatalogItems] = useState<OLMCatalogItem[]>([]);
const [loading, setLoading] = useState(true);
const [loaded, setLoaded] = useState(false);
const [error, setError] = useState('');
const [lastModified, setLastModified] = useState('');
const abortControllerRef = useRef<AbortController>();
Expand Down Expand Up @@ -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]);

Expand All @@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,9 @@
}
}
]
},
"flags": {
"disallowed": ["OLMV1_ENABLED"]
}
},
{
Expand Down
7 changes: 4 additions & 3 deletions test-prow-playwright-e2e-techpreview.sh
Original file line number Diff line number Diff line change
@@ -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.
#
Expand Down Expand Up @@ -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