diff --git a/Makefile b/Makefile index 1cb27237e..235ed3b38 100644 --- a/Makefile +++ b/Makefile @@ -428,3 +428,8 @@ console-build-arm64: generate-dockerfile-console-plugin console-multiarch-manife console-push: ## Uploads the container to quay.io/validatedpatterns/${CONSOLE_PLUGIN_IMAGE} @echo "Uploading the ${REGISTRY}/${CONSOLE_PLUGIN_IMAGE} container to ${UPLOADREGISTRY}/${CONSOLE_PLUGIN_IMAGE}" buildah manifest push --all "${REGISTRY}/${CONSOLE_PLUGIN_IMAGE}" "docker://${UPLOADREGISTRY}/${CONSOLE_PLUGIN_IMAGE}" + +.PHONY: console-integration-tests +console-integration-tests: ## Run console integration tests (requires running cluster) + @echo "Running console integration tests..." + cd console; ./scripts/test-prow-e2e.sh diff --git a/console/integration-tests/cypress.config.js b/console/integration-tests/cypress.config.js index 01ab788db..c5a34461f 100644 --- a/console/integration-tests/cypress.config.js +++ b/console/integration-tests/cypress.config.js @@ -17,6 +17,7 @@ module.exports = defineConfig({ openMode: 0, }, e2e: { + testIsolation: false, setupNodeEvents(on, config) { return require('./plugins/index.ts')(on, config); }, diff --git a/console/integration-tests/support/login.ts b/console/integration-tests/support/login.ts index 25c81b505..ce4b46972 100644 --- a/console/integration-tests/support/login.ts +++ b/console/integration-tests/support/login.ts @@ -3,6 +3,7 @@ declare global { interface Chainable { login(username?: string, password?: string): Chainable; logout(): Chainable; + dismissTour(): Chainable; } } } @@ -30,6 +31,14 @@ Cypress.Commands.add('login', (username: string, password: string) => { }); }); +Cypress.Commands.add('dismissTour', () => { + cy.get('body').then(($body) => { + if ($body.find('[data-test="tour-step-footer-secondary"]').length > 0) { + cy.get('[data-test="tour-step-footer-secondary"]').contains('Skip tour').click(); + } + }); +}); + Cypress.Commands.add('logout', () => { // Check if auth is disabled (for a local development environment). cy.window().then((win) => { diff --git a/console/integration-tests/tests/install-pattern-page.cy.ts b/console/integration-tests/tests/install-pattern-page.cy.ts new file mode 100644 index 000000000..21c47ce9f --- /dev/null +++ b/console/integration-tests/tests/install-pattern-page.cy.ts @@ -0,0 +1,72 @@ +const navigateToInstallPage = () => { + cy.visit('/patterns'); + cy.get('.patterns-operator__card', { timeout: 60000 }).should('exist'); + cy.get('.patterns-operator__card-actions') + .contains('button:not(:disabled)', 'Install') + .first() + .click(); + cy.contains('h1', 'Install Pattern', { timeout: 60000 }).should('be.visible'); +}; + +describe('Install Pattern Page', () => { + before(function () { + cy.login(); + cy.dismissTour(); + + // Check if Install is available; skip the entire suite if not + cy.visit('/patterns'); + cy.get('.patterns-operator__card', { timeout: 60000 }).should('exist'); + cy.get('body').then(($body) => { + const installBtn = $body.find( + '.patterns-operator__card-actions button:not(:disabled):contains("Install")', + ); + if (installBtn.length === 0) { + this.skip(); + } + }); + }); + + after(() => { + cy.logout(); + }); + + it('displays the Install Pattern title', () => { + navigateToInstallPage(); + cy.contains('h1', 'Install Pattern').should('be.visible'); + }); + + it('form fields are pre-populated from catalog data', () => { + navigateToInstallPage(); + cy.get('#pattern-name').invoke('val').should('not.be.empty'); + cy.get('#pattern-target-repo').invoke('val').should('not.be.empty'); + cy.get('#pattern-target-revision').should('have.value', 'main'); + }); + + it('target repo is disabled by default', () => { + navigateToInstallPage(); + cy.get('#pattern-target-repo').should('be.disabled'); + }); + + it('use-own-fork checkbox enables the target repo field', () => { + navigateToInstallPage(); + cy.get('#pattern-target-repo').should('be.disabled'); + cy.get('#use-own-fork').check(); + cy.get('#pattern-target-repo').should('not.be.disabled'); + cy.get('#use-own-fork').uncheck(); + cy.get('#pattern-target-repo').should('be.disabled'); + }); + + it('has Install and Cancel buttons', () => { + navigateToInstallPage(); + cy.contains('button', 'Install').scrollIntoView().should('be.visible'); + cy.contains('button', 'Cancel').scrollIntoView().should('be.visible'); + }); + + it('Cancel button returns to the catalog', () => { + navigateToInstallPage(); + cy.contains('button', 'Cancel').click(); + cy.url().should('include', '/patterns'); + cy.url().should('not.include', '/install'); + cy.contains('h1', 'Pattern Catalog', { timeout: 60000 }).should('be.visible'); + }); +}); diff --git a/console/integration-tests/tests/pattern-catalog-page.cy.ts b/console/integration-tests/tests/pattern-catalog-page.cy.ts index e0c325af7..74aaf2e8c 100644 --- a/console/integration-tests/tests/pattern-catalog-page.cy.ts +++ b/console/integration-tests/tests/pattern-catalog-page.cy.ts @@ -1,53 +1,110 @@ -import { checkErrors } from '../support'; +const visitCatalog = () => { + cy.visit('/patterns'); + cy.get('.patterns-operator__card', { timeout: 60000 }).should('exist'); +}; -const PLUGIN_NAME = 'patterns-operator-console-plugin'; -export const isLocalDevEnvironment = Cypress.config('baseUrl').includes('localhost'); +describe('Pattern Catalog Page', () => { + before(() => { + cy.login(); + cy.dismissTour(); + }); -// Check if console plugin is installed and enabled (operator-managed) -const checkPluginInstalled = () => { - cy.visit('/k8s/cluster/operator.openshift.io~v1~Console/cluster/console-plugins'); - cy.get(`[data-test="${PLUGIN_NAME}-status"]`).should('include.text', 'loaded'); -}; + after(() => { + cy.logout(); + }); -// For operator-managed deployment, we just need to verify the plugin exists -const verifyOperatorDeployment = () => { - cy.exec('oc get consoleplugin patterns-operator-console-plugin', { - failOnNonZeroExit: false, - }).then((result) => { - if (result.code !== 0) { - cy.log('Console plugin not found - operator may not be installed'); - } else { - cy.log('Console plugin found via operator deployment'); - } + it('displays the page title', () => { + cy.visit('/patterns'); + cy.contains('h1', 'Pattern Catalog', { timeout: 60000 }).should('be.visible'); }); -}; -describe('Console plugin template test', () => { - before(() => { - cy.login(); - cy.get(`[data-test="tour-step-footer-secondary"]`).contains('Skip tour').click(); + it('loads and displays pattern cards', () => { + visitCatalog(); + cy.get('.patterns-operator__card').should('have.length.greaterThan', 0); + }); - if (!isLocalDevEnvironment) { - console.log('Verifying operator-managed console plugin deployment'); - verifyOperatorDeployment(); - } else { - console.log('Local development environment - assuming plugin is running via yarn start'); - } + it('pattern cards show tier labels', () => { + visitCatalog(); + cy.get('.patterns-operator__card').first().within(() => { + cy.get('.pf-v6-c-label').should('exist'); + }); }); - afterEach(() => { - checkErrors(); + it('at least one pattern card displays a description', () => { + visitCatalog(); + cy.get('.patterns-operator__card-description') + .should('have.length.greaterThan', 0) + .first() + .invoke('text') + .should('not.be.empty'); }); - after(() => { - // No cleanup needed for operator-managed deployment - cy.logout(); + it('pattern cards have external Docs and Repo links', () => { + visitCatalog(); + cy.get('.patterns-operator__card-links').first().within(() => { + cy.contains('a', 'Docs') + .should('have.attr', 'target', '_blank') + .and('have.attr', 'href'); + cy.contains('a', 'Repo') + .should('have.attr', 'target', '_blank') + .and('have.attr', 'href'); + }); + }); + + it('pattern cards have action buttons', () => { + visitCatalog(); + cy.get('.patterns-operator__card-actions').first().within(() => { + cy.get('button').should('have.length.greaterThan', 0); + }); + }); + + it('tier filter dropdown shows all tier options', () => { + visitCatalog(); + // Default filter shows "Maintained"; click the toggle button + cy.contains('button', 'Maintained').click(); + // Options are capitalized ("Tested", "Sandbox") and unique to the dropdown + cy.contains('Tested').should('be.visible'); + cy.contains('Sandbox').should('be.visible'); + // Close dropdown by clicking the toggle again + cy.contains('button', 'Maintained').click(); + }); + + it('selecting all tiers shows at least as many cards as maintained only', () => { + visitCatalog(); + cy.get('.patterns-operator__card').its('length').then((maintainedCount) => { + // Open filter and add Tested + cy.contains('button', 'Maintained').click(); + cy.contains('Tested').click(); + // Dropdown may close after selection; re-open to add Sandbox + cy.contains('button', /Maintained/).click(); + cy.contains('Sandbox').click(); + // Close dropdown + cy.get('body').click(0, 0); + // With more tiers selected, card count should be >= maintained only + cy.get('.patterns-operator__card').should('have.length.gte', maintainedCount); + }); + }); + + it('clicking Install navigates to the install page', () => { + visitCatalog(); + cy.get('body').then(($body) => { + const installBtn = $body.find('.patterns-operator__card-actions button:not(:disabled):contains("Install")'); + if (installBtn.length === 0) { + cy.log('No Install button available (a pattern may already be installed)'); + return; + } + cy.get('.patterns-operator__card-actions') + .contains('button:not(:disabled)', 'Install') + .first() + .click(); + cy.url().should('include', '/patterns/install/'); + cy.contains('h1', 'Install Pattern', { timeout: 60000 }).should('be.visible'); + }); }); - it('Verify the example page title', () => { - cy.get('[data-quickstart-id="qs-nav-home"]').click(); - cy.get('[data-test="nav"]').contains('Plugin Example').click(); - cy.url().should('include', '/example'); - cy.get('[data-test="pattern-catalog-page-title"]').should('contain', 'Pattern Catalog'); + it('Patterns section is visible in the sidebar navigation', () => { + cy.visit('/patterns'); + cy.contains('h1', 'Pattern Catalog', { timeout: 60000 }).should('be.visible'); + cy.get('nav').contains('Patterns').should('be.visible'); }); }); diff --git a/console/test-prow-e2e.sh b/console/scripts/test-prow-e2e.sh similarity index 85% rename from console/test-prow-e2e.sh rename to console/scripts/test-prow-e2e.sh index 1157b0cfb..5a2bb0b15 100755 --- a/console/test-prow-e2e.sh +++ b/console/scripts/test-prow-e2e.sh @@ -22,7 +22,11 @@ trap copyArtifacts EXIT # don't log kubeadmin-password set +x -BRIDGE_KUBEADMIN_PASSWORD="$(cat "${KUBEADMIN_PASSWORD_FILE:-${INSTALLER_DIR}/auth/kubeadmin-password}")" +if [ -z "${KUBEADMIN_PASSWORD:-}" ]; then + echo "ERROR: KUBEADMIN_PASSWORD is not set" >&2 + exit 1 +fi +BRIDGE_KUBEADMIN_PASSWORD="${KUBEADMIN_PASSWORD}" export BRIDGE_KUBEADMIN_PASSWORD set -x BRIDGE_BASE_ADDRESS="$(oc get consoles.config.openshift.io cluster -o jsonpath='{.status.consoleURL}')" diff --git a/console/src/components/PatternCatalogPage.tsx b/console/src/components/PatternCatalogPage.tsx index bfa6f54f6..3371d483f 100644 --- a/console/src/components/PatternCatalogPage.tsx +++ b/console/src/components/PatternCatalogPage.tsx @@ -202,10 +202,10 @@ export default function PatternCatalogPage() { {catalogImage ? ( - {t('Pattern Catalog')} + {t('Pattern Catalog')} ) : ( - {t('Pattern Catalog')} + {t('Pattern Catalog')} )} {catalogDescription && (