diff --git a/.github/workflows/extensions-compatibility-tests.yaml b/.github/workflows/extensions-compatibility-tests.yaml new file mode 100644 index 00000000000..b7494a2d0d5 --- /dev/null +++ b/.github/workflows/extensions-compatibility-tests.yaml @@ -0,0 +1,66 @@ +name: Extensions Compatibility Tests +on: + schedule: + - cron: "0 0 * * *" # runs at midnight every day + +env: + TEST_USERNAME: admin + TEST_PASSWORD: password + CATTLE_BOOTSTRAP_PASSWORD: password + TEST_BASE_URL: https://127.0.0.1:8005 + API: https://127.0.0.1 + TEST_PROJECT_ID: rancher-dashboard + CYPRESS_API_URL: http://139.59.134.103:1234/ + TEST_RUN_ID: ${{github.run_number}}-${{github.run_attempt}}-extensions-compatibility-tests + +jobs: + e2e-test-extensions-compatibility: + strategy: + fail-fast: false + matrix: + role: [ + { username: 'admin', tag: '@adminUser' } + ] + features: [ + ['@elemental', 'elemental'] + ] + rancherEnv: [ + ['2.10', 'v2.9-head'], + # ['2.9', 'v2.9-head'] + ] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 1 + - name: Setup env + uses: ./.github/actions/setup + + # this is where docker is set up with the enviroment + - name: Run Rancher system ${{ matrix.rancherEnv[0] }} - image:${{ matrix.rancherEnv[1] }} + run: export RANCHER_VERSION_E2E=${{ matrix.rancherEnv[1] }} && yarn e2e:docker + + ## this is just setting up rancher and user + - name: Setup Rancher and user + run: | + yarn e2e:prod + env: + GREP_TAGS: ${{ matrix.role.tag }}Setup+${{ matrix.features[0] }} --@jenkins ${{ matrix.role.tag }}Setup+${{ matrix.features[0] }} --@jenkins + TEST_USERNAME: ${{ matrix.role.username }} + TEST_ONLY: setup + + # This is the actual triggering of the e2e test specs + - name: Run user tests + run: | + export SPEC_FILE="cypress/e2e/tests/extensions/${{ matrix.features[1] }}/${{ matrix.features[1] }}.spec.ts" && yarn e2e:prod + env: + TEST_SKIP: setup + GREP_TAGS: ${{ matrix.role.tag }}+${{ matrix.features[0] }} --@jenkins ${{ matrix.role.tag }}+${{ matrix.features[0] }} --@jenkins + TEST_USERNAME: ${{ matrix.role.username }} + + - name: Upload screenshots + uses: actions/upload-artifact@v3 + if: ${{ failure() }} + with: + name: ${{github.run_number}}-${{github.run_attempt}}-extensions-compatibility-tests-screenshots-${{ matrix.role.tag }}+${{ matrix.features[0] }} + path: cypress/screenshots \ No newline at end of file diff --git a/cypress.config.ts b/cypress.config.ts index 3c5b702f850..dcf8600021b 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -9,7 +9,7 @@ require('dotenv').config(); * VARIABLES */ const hasCoverage = (process.env.TEST_INSTRUMENT === 'true') || false; // Add coverage if instrumented -const testDirs = ['priority', 'components', 'setup', 'pages', 'navigation', 'global-ui', 'features']; +const testDirs = ['priority', 'components', 'setup', 'pages', 'navigation', 'global-ui', 'features', 'extensions']; const skipSetup = process.env.TEST_SKIP?.includes('setup'); const baseUrl = (process.env.TEST_BASE_URL || 'https://localhost:8005').replace(/\/$/, ''); const DEFAULT_USERNAME = 'admin'; diff --git a/cypress/e2e/po/components/labeled-select.po.ts b/cypress/e2e/po/components/labeled-select.po.ts index ea49ed1eaed..57e4c70e76c 100644 --- a/cypress/e2e/po/components/labeled-select.po.ts +++ b/cypress/e2e/po/components/labeled-select.po.ts @@ -5,6 +5,12 @@ export default class LabeledSelectPo extends ComponentPo { return this.self().click(); } + setOptionAndClick(label: string) { + this.self().get('input[type="search"]').type(label); + + return this.clickOption(1); + } + clickOption(optionIndex: number) { return this.self().get(`.vs__dropdown-menu .vs__dropdown-option:nth-child(${ optionIndex })`).click(); } diff --git a/cypress/e2e/po/components/resource-yaml.po.ts b/cypress/e2e/po/components/resource-yaml.po.ts index c514d390a4c..75a408d03e8 100644 --- a/cypress/e2e/po/components/resource-yaml.po.ts +++ b/cypress/e2e/po/components/resource-yaml.po.ts @@ -1,6 +1,7 @@ import ComponentPo from '@/cypress/e2e/po/components/component.po'; import { CypressChainable } from '@/cypress/e2e/po/po.types'; import AsyncButtonPo from '@/cypress/e2e/po/components/async-button.po'; +import CodeMirrorPo from '@/cypress/e2e/po/components/code-mirror.po'; export default class ResourceYamlPo extends ComponentPo { constructor(parent?: CypressChainable) { @@ -15,6 +16,10 @@ export default class ResourceYamlPo extends ComponentPo { return this.self().find('.footer'); } + codeMirror() { + return CodeMirrorPo.bySelector(this.self(), '[data-testid="yaml-editor-code-mirror"]'); + } + saveOrCreate(): AsyncButtonPo { return new AsyncButtonPo('[data-testid="action-button-async-button"]', this.self()); } diff --git a/cypress/e2e/po/extensions/elemental/elemental.utils.ts b/cypress/e2e/po/extensions/elemental/elemental.utils.ts new file mode 100644 index 00000000000..505d130acd7 --- /dev/null +++ b/cypress/e2e/po/extensions/elemental/elemental.utils.ts @@ -0,0 +1,47 @@ +import ExtensionsCompatibilityUtils from '~/cypress/e2e/po/extensions/extensions-compatibility.utils'; +import BannersPo from '@/cypress/e2e/po/components/banners.po'; +import LabeledSelectPo from '@/cypress/e2e/po/components/labeled-select.po'; +import RadioGroupInputPo from '@/cypress/e2e/po/components/radio-group-input.po'; +import PagePo from '@/cypress/e2e/po/pages/page.po'; + +class DashboardPagePo extends PagePo { + private static url = '/elemental/c/_/dashboard'; + + constructor() { + super(DashboardPagePo.url); + } + + waitForTitle(): Cypress.Chainable { + return this.self().find('h1').should('contain', 'OS Management'); + } + + installOperator(): Cypress.Chainable { + return this.self().getId('charts-install-button').click(); + } + + createElementalCluster() { + return this.self().getId('button-create-elemental-cluster').click(); + } + + createUpdateGroupClick() { + return this.self().getId('create-update-group-btn').click(); + } +} + +export default class ElementalPo extends ExtensionsCompatibilityUtils { + dashboard() { + return new DashboardPagePo(); + } + + elementalClusterSelectorTemplateBanner() { + return new BannersPo('[provider="machineinventoryselectortemplate"] .banner.warning'); + } + + updateGroupTargetClustersSelect() { + return new LabeledSelectPo('[data-testid="cluster-target"]'); + } + + updateGroupImageOption() { + return new RadioGroupInputPo('[data-testid="upgrade-choice-selector"]'); + } +} diff --git a/cypress/e2e/po/extensions/extensions-compatibility.utils.ts b/cypress/e2e/po/extensions/extensions-compatibility.utils.ts new file mode 100644 index 00000000000..4151c81807d --- /dev/null +++ b/cypress/e2e/po/extensions/extensions-compatibility.utils.ts @@ -0,0 +1,41 @@ +import PagePo from '@/cypress/e2e/po/pages/page.po'; +import { InstallChartPage } from '@/cypress/e2e/po/pages/explorer/charts/install-charts.po'; +import ChartInstalledAppsPagePo from '@/cypress/e2e/po/pages/chart-installed-apps.po'; +import BaseResourceList from '@/cypress/e2e/po/lists/base-resource-list.po'; +import NameNsDescriptionPo from '@/cypress/e2e/po/components/name-ns-description.po'; +import ResourceDetailPo from '@/cypress/e2e/po/edit/resource-detail.po'; +import CodeMirrorPo from '~/cypress/e2e/po/components/code-mirror.po'; + +const installChartPage = new InstallChartPage(); +const appsPage = new ChartInstalledAppsPagePo('local', 'apps'); +const root = '.dashboard-root'; + +export default class ExtensionsCompatibilityUtils { + appsPage() { + return appsPage; + } + + chartInstallPage() { + return installChartPage; + } + + genericPage(path: string) { + return new PagePo(path); + } + + genericResourceList(): BaseResourceList { + return new BaseResourceList(cy.get(root)); + } + + genericNameNsDescription(): NameNsDescriptionPo { + return new NameNsDescriptionPo(cy.get(root)); + } + + genericResourceDetail() { + return new ResourceDetailPo(cy.get(root)); + } + + genericCodeMirror() { + return CodeMirrorPo.bySelector(cy.get(root), '[data-testid="yaml-editor-code-mirror"]'); + } +} diff --git a/cypress/e2e/po/pages/chart-installed-apps.po.ts b/cypress/e2e/po/pages/chart-installed-apps.po.ts index fb9e2064953..fa93ee8b6ad 100644 --- a/cypress/e2e/po/pages/chart-installed-apps.po.ts +++ b/cypress/e2e/po/pages/chart-installed-apps.po.ts @@ -1,5 +1,8 @@ import PagePo from '@/cypress/e2e/po/pages/page.po'; import ChartInstalledAppsListPo from '@/cypress/e2e/po/lists/chart-installed-apps.po'; +import Kubectl from '@/cypress/e2e/po/components/kubectl.po'; + +const terminal = new Kubectl(); /** * List page for catalog.cattle.io.app resources @@ -24,4 +27,20 @@ export default class ChartInstalledAppsPagePo extends PagePo { list(): ChartInstalledAppsListPo { return new ChartInstalledAppsListPo('[data-testid="installed-app-catalog-list"]'); } + + waitForInstallCloseTerminal(interceptName: string, installableParts: Array) { + cy.wait(`@${ interceptName }`, { requestTimeout: 20000 }).its('response.statusCode').should('eq', 201); + + // giving it a small buffer so that the install is properly triggered + cy.wait(15000); // eslint-disable-line cypress/no-unnecessary-waiting + terminal.closeTerminal(); + + installableParts.forEach((item:string) => { + this.list().state(item).should('contain', 'Deployed'); + }); + + // timeout to give time for everything to be setup, otherwise the extension + // won't find the chart and show the correct screen + return cy.wait(10000); // eslint-disable-line cypress/no-unnecessary-waiting + } } diff --git a/cypress/e2e/po/pages/extensions.po.ts b/cypress/e2e/po/pages/extensions.po.ts index 06055ef4fc8..97d05c1f2e8 100644 --- a/cypress/e2e/po/pages/extensions.po.ts +++ b/cypress/e2e/po/pages/extensions.po.ts @@ -6,6 +6,7 @@ import NameNsDescriptionPo from '@/cypress/e2e/po/components/name-ns-description import RepositoriesPagePo from '@/cypress/e2e/po/pages/chart-repositories.po'; import BannersPo from '@/cypress/e2e/po/components/banners.po'; import ChartRepositoriesCreateEditPo from '@/cypress/e2e/po/edit/chart-repositories.po'; +import AppClusterRepoEditPo from '@/cypress/e2e/po/edit/catalog.cattle.io.clusterrepo.po'; import { LONG_TIMEOUT_OPT } from '@/cypress/support/utils/timeouts'; export default class ExtensionsPagePo extends PagePo { @@ -83,6 +84,35 @@ export default class ExtensionsPagePo extends PagePo { appRepoList.list().state(name).should('contain', 'Active'); } + /** + * Adds a cluster repo for extensions + * @param repo - The repository url (e.g. https://github.com/rancher/ui-plugin-examples) + * @param branch - The git branch to target + * @param name - A name for the repository + * @returns {Cypress.Chainable} + */ + addExtensionsRepositoryDirectLink(repo: string, branch: string, name: string, waitForActiveState = true): Cypress.Chainable { + const appRepoList = new RepositoriesPagePo('local', 'apps'); + const appRepoCreate = new AppClusterRepoEditPo('local', 'create'); + + appRepoCreate.goTo(); + appRepoCreate.waitForPage(); + + appRepoCreate.nameNsDescription().name().self().scrollIntoView() + .should('be.visible'); + appRepoCreate.nameNsDescription().name().set(name); + appRepoCreate.selectRadioOptionGitRepo(1); + // fill the git repo form + appRepoCreate.enterGitRepoName(repo); + appRepoCreate.enterGitBranchName(branch); + appRepoCreate.create().click(); + + if (waitForActiveState) { + appRepoList.waitForPage(); + appRepoList.list().state(name).should('contain', 'Active'); + } + } + // ------------------ extension card ------------------ extensionCard(extensionName: string) { return this.self().getId(`extension-card-${ extensionName }`); @@ -113,6 +143,14 @@ export default class ExtensionsPagePo extends PagePo { return this.self().get('[data-modal="installPluginDialog"]'); } + installModalSelectVersionLabel(label: string): Cypress.Chainable { + const selectVersion = new LabeledSelectPo(this.extensionInstallModal().getId('install-ext-modal-select-version')); + + selectVersion.toggle(); + + return selectVersion.setOptionAndClick(label); + } + installModalSelectVersionClick(optionIndex: number): Cypress.Chainable { const selectVersion = new LabeledSelectPo(this.extensionInstallModal().getId('install-ext-modal-select-version')); diff --git a/cypress/e2e/po/pages/page.po.ts b/cypress/e2e/po/pages/page.po.ts index 7a64d5cfefc..40e459c39f2 100644 --- a/cypress/e2e/po/pages/page.po.ts +++ b/cypress/e2e/po/pages/page.po.ts @@ -63,6 +63,10 @@ export default class PagePo extends ComponentPo { return this.self().find('.primaryheader h1').invoke('text'); } + waitForMastheadTitle(title: string) { + return this.mastheadTitle().should('contain', title); + } + navToMenuEntry(label: string) { BurgerMenuPo.toggle(); BurgerMenuPo.burgerMenuNavToMenubyLabel(label); diff --git a/cypress/e2e/tests/extensions/elemental/elemental.spec.ts b/cypress/e2e/tests/extensions/elemental/elemental.spec.ts new file mode 100644 index 00000000000..1dc1c06f643 --- /dev/null +++ b/cypress/e2e/tests/extensions/elemental/elemental.spec.ts @@ -0,0 +1,232 @@ +import ExtensionsPagePo from '@/cypress/e2e/po/pages/extensions.po'; +import ElementalPo from '~/cypress/e2e/po/extensions/elemental/elemental.utils'; +import { NamespaceFilterPo } from '@/cypress/e2e/po/components/namespace-filter.po'; +import * as jsyaml from 'js-yaml'; +import LabeledInputPo from '@/cypress/e2e/po/components/labeled-input.po'; +import { MEDIUM_TIMEOUT_OPT } from '~/cypress/support/utils/timeouts'; + +const EXTENSION_NAME = 'elemental'; +const EXTENSION_VERSION = '2.0.0-rc2'; +const EXTENSION_REPO = 'https://github.com/rancher/elemental-ui'; +const EXTENSION_BRANCH = 'gh-pages'; +const EXTENSION_CLUSTER_REPO_NAME = 'elemental-ui-extension'; +const EXTENSION_CHART_CREATION = 'chartCreation'; + +const REG_ENDPOINT_NAME = 'reg-endpoint-1'; +const REG_ENDPOINT_DEVICE_PATH = '/dev/nvme0n123'; + +const MACHINE_INV_NAME = 'machine-inventory-1'; + +const ELEMENTAL_CLUSTER_NAME = 'elemental-cluster-1'; +const ELEMENTAL_CLUSTER_BANNER_TEXT = 'Matches all 1 existing Inventory of Machines'; +const ELEMENTAL_CLUSTER_MACHINE_CONFIG_REF = 'MachineInventorySelectorTemplate'; + +const UPDATE_GROUP_NAME = 'update-group-1'; +const UPDATE_GROUP_IMAGE_PATH = 'some/path'; + +const elementalPo = new ElementalPo(); +const namespaceFilter = new NamespaceFilterPo(); + +describe('Extensions Compatibility spec', { tags: ['@elemental', '@adminUser'] }, () => { + beforeEach(() => { + cy.login(); + }); + + it('add extension repository', () => { + // // This should be in a `before` however is flaky. Move it to an `it` to let cypress retry + const extensionsPo = new ExtensionsPagePo(); + + extensionsPo.addExtensionsRepositoryDirectLink(EXTENSION_REPO, EXTENSION_BRANCH, EXTENSION_CLUSTER_REPO_NAME, true); + // let's wait a bit so that the repo is available in extensions screen + cy.wait(10000); // eslint-disable-line cypress/no-unnecessary-waiting + }); + + it('Should install an extension', () => { + const extensionsPo = new ExtensionsPagePo(); + + extensionsPo.goTo(); + + extensionsPo.extensionTabAvailableClick(); + + // click on install button on card + extensionsPo.extensionCardInstallClick(EXTENSION_NAME); + extensionsPo.extensionInstallModal().should('be.visible'); + + // select version and click install + extensionsPo.installModalSelectVersionLabel(EXTENSION_VERSION); + extensionsPo.installModalInstallClick(); + + // let's check the extension reload banner and reload the page + extensionsPo.extensionReloadBanner().should('be.visible', MEDIUM_TIMEOUT_OPT); + extensionsPo.extensionReloadClick(); + + // make sure extension card is in the installed tab + extensionsPo.extensionTabInstalledClick(); + extensionsPo.extensionCardClick(EXTENSION_NAME); + extensionsPo.extensionDetailsTitle().should('contain', EXTENSION_NAME); + extensionsPo.extensionDetailsCloseClick(); + }); + + it('Should setup all of the needed backend parts', () => { + cy.intercept('POST', 'v1/catalog.cattle.io.clusterrepos/rancher-charts?action=install').as(EXTENSION_CHART_CREATION); + + elementalPo.dashboard().goTo(); + elementalPo.dashboard().waitForTitle(); + + elementalPo.dashboard().installOperator(); + elementalPo.chartInstallPage().waitForChartPage('rancher-charts', 'elemental'); + + // we need to change the namespace picker in order for the install check on the list view + namespaceFilter.toggle(); + namespaceFilter.clickOptionByLabel('All Namespaces'); + namespaceFilter.closeDropdown(); + + elementalPo.chartInstallPage().nextPage(); + elementalPo.chartInstallPage().installChart(); + elementalPo.appsPage().waitForInstallCloseTerminal(EXTENSION_CHART_CREATION, ['elemental-operator-crds', 'elemental-operator']); + + elementalPo.dashboard().goTo(); + cy.get('[data-testid="elemental-main-title"]').invoke('text').should('contain', 'OS Management Dashboard'); + }); + + it('Should create an Elemental registration endpoint', () => { + cy.intercept('POST', 'v1/elemental.cattle.io.machineregistrations/fleet-default').as('machineRegCreation'); + + elementalPo.dashboard().goTo(); + elementalPo.dashboard().productNav().navToSideMenuEntryByLabel('Registration Endpoints'); + + elementalPo.genericResourceList().masthead().create(); + elementalPo.genericNameNsDescription().name().set(REG_ENDPOINT_NAME); + elementalPo.genericCodeMirror().value() + .then((val) => { + // convert yaml into json to update values + const json: any = jsyaml.load(val); + + json.config.elemental.install.device = REG_ENDPOINT_DEVICE_PATH; + + elementalPo.genericCodeMirror().set(jsyaml.dump(json)); + elementalPo.genericResourceDetail().cruResource().saveOrCreate().click(); + + cy.wait('@machineRegCreation', { requestTimeout: 15000 }).then(({ response }) => { + expect(response?.statusCode).to.eq(201); + expect(response?.body.metadata).to.have.property('name', REG_ENDPOINT_NAME); + expect(response?.body.spec.config.elemental.install).to.have.property('device', REG_ENDPOINT_DEVICE_PATH); + }); + }); + }); + + it('Should create an Elemental resource via YAML (Inventory of Machines)', () => { + const maxPollingRetries = 36; + let pollingCounter = 0; + + function pollingSchemaDefinition() { + cy + .request({ + url: 'v1/schemaDefinitions/elemental.cattle.io.machineinventory', + method: 'GET', + failOnStatusCode: false + }) + .then((resp) => { + pollingCounter++; + + if (resp.status === 200 || pollingCounter === maxPollingRetries) { + return; + } + + if (pollingCounter === maxPollingRetries) { + throw new Error('schemaDefinition polling failed'); + } + + // let's wait for a bit so that we don't overload the server + // with requests + cy.wait(5000); // eslint-disable-line cypress/no-unnecessary-waiting + pollingSchemaDefinition(); + }); + } + + elementalPo.dashboard().goTo(); + elementalPo.dashboard().productNav().navToSideMenuEntryByLabel('Inventory of Machines'); + + // after we hit create from YAML we need to pool for the schemaDefinition since + // that takes while to be available https://docs.cypress.io/api/commands/request#Request-Polling + elementalPo.genericResourceList().masthead().createYaml().then(pollingSchemaDefinition); + + elementalPo.genericResourceDetail().resourceYaml().codeMirror().value() + .then((val) => { + // convert yaml into json to update values + const json: any = jsyaml.load(val); + + json.metadata.name = MACHINE_INV_NAME; + + elementalPo.genericResourceDetail().resourceYaml().codeMirror().set(jsyaml.dump(json)); + elementalPo.genericResourceDetail().resourceYaml().saveOrCreate().click(); + + elementalPo.genericPage('/elemental/c/_/elemental.cattle.io.machineinventory').waitForPage(); + elementalPo.genericResourceList().resourceTable().sortableTable().rowWithName(MACHINE_INV_NAME) + .column(2) + .should('contain', MACHINE_INV_NAME); + }); + }); + + it('Should create an Elemental cluster, targeting all of the inventory of machines', () => { + cy.intercept('POST', 'v1/provisioning.cattle.io.clusters').as('elementalClusterCreation'); + + elementalPo.dashboard().goTo(); + elementalPo.dashboard().createElementalCluster(); + + elementalPo.genericNameNsDescription().name().set(ELEMENTAL_CLUSTER_NAME); + elementalPo.elementalClusterSelectorTemplateBanner().banner().contains(ELEMENTAL_CLUSTER_BANNER_TEXT); + // hit create button + cy.get('[data-testid="rke2-custom-create-save"]').click(); + + cy.wait('@elementalClusterCreation', { requestTimeout: 15000 }).then(({ response }) => { + expect(response?.statusCode).to.eq(201); + expect(response?.body.metadata).to.have.property('name', ELEMENTAL_CLUSTER_NAME); + expect(response?.body.spec.rkeConfig.machinePools[0].machineConfigRef).to.have.property('kind', ELEMENTAL_CLUSTER_MACHINE_CONFIG_REF); + }); + }); + + it('Should create an Upgrade Group', () => { + cy.intercept('POST', 'v1/elemental.cattle.io.managedosimages').as('elementalUpdateGroupCreation'); + + elementalPo.dashboard().goTo(); + elementalPo.dashboard().createUpdateGroupClick(); + + elementalPo.genericNameNsDescription().name().set(UPDATE_GROUP_NAME); + elementalPo.updateGroupTargetClustersSelect().toggle(); + elementalPo.updateGroupTargetClustersSelect().clickOptionWithLabel(ELEMENTAL_CLUSTER_NAME); + elementalPo.updateGroupImageOption().set(1); + + LabeledInputPo.byLabel(cy.get('.dashboard-root'), 'Image path').set(UPDATE_GROUP_IMAGE_PATH); + elementalPo.genericResourceDetail().cruResource().saveOrCreate().click(); + + cy.wait('@elementalUpdateGroupCreation', { requestTimeout: 15000 }).then(({ response }) => { + expect(response?.statusCode).to.eq(201); + expect(response?.body.metadata).to.have.property('name', UPDATE_GROUP_NAME); + expect(response?.body.spec.clusterTargets[0]).to.have.property('clusterName', ELEMENTAL_CLUSTER_NAME); + expect(response?.body.spec).to.have.property('osImage', UPDATE_GROUP_IMAGE_PATH); + }); + }); + + it('Should uninstall the extension', () => { + const extensionsPo = new ExtensionsPagePo(); + + extensionsPo.goTo(); + extensionsPo.extensionTabInstalledClick(); + + // click on uninstall button on card + extensionsPo.extensionCardUninstallClick(EXTENSION_NAME); + extensionsPo.extensionUninstallModal().should('be.visible'); + extensionsPo.uninstallModaluninstallClick(); + extensionsPo.extensionReloadBanner().should('be.visible'); + + // let's check the extension reload banner and reload the page + extensionsPo.extensionReloadBanner().should('be.visible'); + extensionsPo.extensionReloadClick(); + + // make sure extension card is in the available tab + extensionsPo.extensionTabAvailableClick(); + extensionsPo.extensionCardClick(EXTENSION_NAME); + extensionsPo.extensionDetailsTitle().should('contain', EXTENSION_NAME); + }); +}); diff --git a/cypress/e2e/tests/setup/rancher-setup.spec.ts b/cypress/e2e/tests/setup/rancher-setup.spec.ts index 067353090a7..255d74095e1 100644 --- a/cypress/e2e/tests/setup/rancher-setup.spec.ts +++ b/cypress/e2e/tests/setup/rancher-setup.spec.ts @@ -6,7 +6,7 @@ import { serverUrlLocalhostCases, urlWithTrailingForwardSlash, httpUrl, nonUrlCa // Cypress or the GrepTags avoid to run multiples times the same test for each tag used. // This is a temporary solution till initialization is not handled as a test -describe('Rancher setup', { tags: ['@adminUserSetup', '@standardUserSetup', '@setup', '@components', '@navigation', '@charts', '@explorer', '@extensions', '@fleet', '@generic', '@globalSettings', '@manager', '@userMenu', '@usersAndAuths', '@vai'] }, () => { +describe('Rancher setup', { tags: ['@adminUserSetup', '@standardUserSetup', '@setup', '@components', '@navigation', '@charts', '@explorer', '@extensions', '@fleet', '@generic', '@globalSettings', '@manager', '@userMenu', '@usersAndAuths', '@elemental', '@vai'] }, () => { const rancherSetupLoginPage = new RancherSetupLoginPagePo(); const rancherSetupConfigurePage = new RancherSetupConfigurePage(); const homePage = new HomePagePo(); diff --git a/package.json b/package.json index b6789fc8d6f..0775845d6f3 100644 --- a/package.json +++ b/package.json @@ -39,11 +39,11 @@ "cy:e2e": "cypress open --e2e --browser chrome", "cy:open": "cypress open", "cy:run": "cypress run --browser chrome", - "cy:run:sorry": "./scripts/e2e", + "cy:run:sorry": "./scripts/e2e $SPEC_FILE", "e2e:pre-dev": "yarn docker:local:stop && yarn docker:local:start && NODE_ENV=dev TEST_INSTRUMENT=true yarn build", "e2e:dev": "START_SERVER_AND_TEST_INSECURE=1 server-test start:dev https-get://localhost:8005 cy:run:sorry", "e2e:build": "mkdir dist && TEST_INSTRUMENT=true ./scripts/build-e2e", - "e2e:docker": "yarn docker:local:stop && ./scripts/e2e-docker-start ", + "e2e:docker": "yarn docker:local:stop && ./scripts/e2e-docker-start $RANCHER_VERSION_E2E", "e2e:prod": "BUILD_DASHBOARD=$BUILD_DASHBOARD GREP_TAGS=$GREP_TAGS TEST_USERNAME=$TEST_USERNAME TEST_BASE_URL=https://127.0.0.1/dashboard yarn cy:run:sorry", "coverage": "npx nyc merge coverage coverage/coverage.json", "storybook": "cd storybook && yarn install && yarn storybook", diff --git a/scripts/e2e b/scripts/e2e index d961830d62e..96b53ea98d1 100755 --- a/scripts/e2e +++ b/scripts/e2e @@ -2,4 +2,12 @@ ID="$(echo ${TEST_RUN_ID:-`date +%s` } | tr '[:upper:]' '[:lower:]' | tr ' ' '-')" echo "$ID" -CYPRESS_coverage=true CYPRESS_API_URL='http://139.59.134.103:1234' cy2 run --group "$GREP_TAGS" --browser chrome --record --key rancher-dashboard --parallel --ci-build-id "$ID" +SPEC_ARG="" + +# check if script invoke contains any argument. If so, add spec to run +if [ $# -eq 1 ]; then + echo "Running e2e tests with spec: $1" + SPEC_ARG="--spec ${1}" +fi + +CYPRESS_coverage=true CYPRESS_API_URL='http://139.59.134.103:1234' cy2 run ${SPEC_ARG} --group "$GREP_TAGS" --browser chrome --record --key rancher-dashboard --parallel --ci-build-id "$ID" \ No newline at end of file diff --git a/scripts/e2e-docker-start b/scripts/e2e-docker-start index 13944eedfe7..6b4ad854807 100755 --- a/scripts/e2e-docker-start +++ b/scripts/e2e-docker-start @@ -9,9 +9,19 @@ EMBER_DIST=${DIR}/dist_ember # Image version RANCHER_IMG_VERSION=v2.9-head -docker run -d --restart=unless-stopped -p 80:80 -p 443:443 \ - -v ${DASHBOARD_DIST}:/usr/share/rancher/ui-dashboard/dashboard \ - -v ${EMBER_DIST}:/usr/share/rancher/ui \ +# Docker volume args when mounting the locally-built UI into the container +VOLUME_ARGS="-v ${DASHBOARD_DIST}:/usr/share/rancher/ui-dashboard/dashboard -v ${EMBER_DIST}:/usr/share/rancher/ui" + +# check if script invoke contains any argument. If so, adjust RANCHER_IMG_VERSION +if [ $# -eq 1 ]; then + RANCHER_IMG_VERSION=$1 + # When an image is specified, we use that image, including its front-end, so we don't want the volume args to mount the locally-built UI + VOLUME_ARGS="" +fi + +echo "Using Rancher image version: ${RANCHER_IMG_VERSION}" + +docker run -d --restart=unless-stopped -p 80:80 -p 443:443 ${VOLUME_ARGS} \ -e CATTLE_UI_OFFLINE_PREFERRED=true \ -e CATTLE_BOOTSTRAP_PASSWORD=password \ -e CATTLE_PASSWORD_MIN_LENGTH=3 \