From 74df7747e9a9b2d5cac2480cfa731071386c2117 Mon Sep 17 00:00:00 2001 From: Leon Kladnitsky Date: Tue, 2 Jun 2026 20:40:21 +0300 Subject: [PATCH 1/4] CNV-87983: Add Cypress E2E tests for networking plugin Migrate plugin-owned UI tests from kubevirt-ui to this repo. Tests cover NADs, UDNs, NetworkPolicies, Services, Routes, and Ingresses. Includes GitHub Actions hot-cluster CI workflow. Original integration-tests/ kept intact for Prow; new tests live in cypress/ and run via `npm run test-e2e`. Co-authored-by: Cursor --- .env.example | 5 + .github/workflows/e2e.yml | 47 ++++ .gitignore | 2 + cleanup.sh | 22 ++ integration-tests/support/login.ts | 4 +- integration-tests/tests/example-page.cy.ts | 2 +- package-lock.json | 14 + package.json | 15 +- research-flakiness.sh | 120 +++++++++ setup.sh | 43 ++++ test-cypress.sh | 38 +++ ui-tests-cy/.eslintrc | 17 ++ ui-tests-cy/cypress.config.js | 31 +++ ui-tests-cy/plugins/index.ts | 31 +++ ui-tests-cy/reporter-config.json | 14 + ui-tests-cy/support/commands.ts | 48 ++++ ui-tests-cy/support/index.ts | 18 ++ ui-tests-cy/support/login.ts | 60 +++++ ui-tests-cy/support/nav.ts | 28 ++ ui-tests-cy/support/selectors.ts | 81 ++++++ ui-tests-cy/tests/all.cy.ts | 10 + ui-tests-cy/tests/networking/ingresses.cy.ts | 20 ++ ui-tests-cy/tests/networking/nad-bridge.cy.ts | 9 + .../tests/networking/nad-localnet.cy.ts | 14 + ui-tests-cy/tests/networking/nad-ovn.cy.ts | 9 + .../tests/networking/net-policies.cy.ts | 31 +++ ui-tests-cy/tests/networking/routes.cy.ts | 32 +++ ui-tests-cy/tests/networking/services.cy.ts | 19 ++ ui-tests-cy/tests/networking/udn.cy.ts | 46 ++++ ui-tests-cy/tests/setup/login.cy.ts | 5 + ui-tests-cy/tests/setup/visit-pages.cy.ts | 35 +++ ui-tests-cy/tsconfig.json | 9 + ui-tests-cy/utils/const/base.ts | 23 ++ ui-tests-cy/utils/const/nad.ts | 28 ++ ui-tests-cy/utils/types/nad.ts | 11 + ui-tests-cy/views/actions.ts | 12 + ui-tests-cy/views/nad.ts | 77 ++++++ ui-tests-cy/views/selector-common.ts | 14 + ui-tests-cy/views/udn.ts | 39 +++ ui/MIGRATION.md | 55 ++++ ui/NMSTATE_MIGRATION_GUIDE.md | 96 +++++++ ui/PLAN.md | 242 ++++++++++++++++++ 42 files changed, 1467 insertions(+), 9 deletions(-) create mode 100644 .env.example create mode 100644 .github/workflows/e2e.yml create mode 100755 cleanup.sh create mode 100755 research-flakiness.sh create mode 100755 setup.sh create mode 100755 test-cypress.sh create mode 100644 ui-tests-cy/.eslintrc create mode 100644 ui-tests-cy/cypress.config.js create mode 100644 ui-tests-cy/plugins/index.ts create mode 100644 ui-tests-cy/reporter-config.json create mode 100644 ui-tests-cy/support/commands.ts create mode 100644 ui-tests-cy/support/index.ts create mode 100644 ui-tests-cy/support/login.ts create mode 100644 ui-tests-cy/support/nav.ts create mode 100644 ui-tests-cy/support/selectors.ts create mode 100644 ui-tests-cy/tests/all.cy.ts create mode 100644 ui-tests-cy/tests/networking/ingresses.cy.ts create mode 100644 ui-tests-cy/tests/networking/nad-bridge.cy.ts create mode 100644 ui-tests-cy/tests/networking/nad-localnet.cy.ts create mode 100644 ui-tests-cy/tests/networking/nad-ovn.cy.ts create mode 100644 ui-tests-cy/tests/networking/net-policies.cy.ts create mode 100644 ui-tests-cy/tests/networking/routes.cy.ts create mode 100644 ui-tests-cy/tests/networking/services.cy.ts create mode 100644 ui-tests-cy/tests/networking/udn.cy.ts create mode 100644 ui-tests-cy/tests/setup/login.cy.ts create mode 100644 ui-tests-cy/tests/setup/visit-pages.cy.ts create mode 100644 ui-tests-cy/tsconfig.json create mode 100644 ui-tests-cy/utils/const/base.ts create mode 100644 ui-tests-cy/utils/const/nad.ts create mode 100644 ui-tests-cy/utils/types/nad.ts create mode 100644 ui-tests-cy/views/actions.ts create mode 100644 ui-tests-cy/views/nad.ts create mode 100644 ui-tests-cy/views/selector-common.ts create mode 100644 ui-tests-cy/views/udn.ts create mode 100644 ui/MIGRATION.md create mode 100644 ui/NMSTATE_MIGRATION_GUIDE.md create mode 100644 ui/PLAN.md diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..9550c3d1 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +BRIDGE_BASE_ADDRESS=https://console-openshift-console.apps.your-cluster.example.com +BRIDGE_KUBEADMIN_PASSWORD=your-password +TEST_NS=cy-test-ns +UDN_NS=udn-test-ns +HIDE_XHR=true diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 00000000..b9c2d6e1 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,47 @@ +name: E2E Tests + +on: + pull_request: + branches: [main, release-*] + workflow_dispatch: + +concurrency: + group: e2e-${{ github.head_ref || github.ref_name }} + cancel-in-progress: true + +jobs: + e2e: + name: Cypress E2E + runs-on: ubuntu-latest + timeout-minutes: 30 + + env: + BRIDGE_BASE_ADDRESS: ${{ secrets.CONSOLE_URL }} + BRIDGE_KUBEADMIN_PASSWORD: ${{ secrets.KUBEADMIN_PASSWORD }} + BRIDGE_E2E_BROWSER_NAME: electron + TEST_NS: ${{ secrets.TEST_NS || 'cy-test-ns' }} + UDN_NS: udn-test-ns + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 18 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run E2E tests + run: npm run test-e2e + + - name: Upload test artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: e2e-results + path: cypress/gui-test-screenshots/ + retention-days: 7 diff --git a/.gitignore b/.gitignore index bde351d5..ceb70bf2 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ integration-tests/screenshots integration-tests/.DS_Store yarn-error.log .DS_Store +cypress/gui-test-screenshots/ +.env diff --git a/cleanup.sh b/cleanup.sh new file mode 100755 index 00000000..31460387 --- /dev/null +++ b/cleanup.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +# Cleanup test resources from previous runs +# Sources .env for namespace names + +source .env 2>/dev/null || true +TEST_NS="${TEST_NS:-cy-test-ns}" +UDN_NS="${UDN_NS:-udn-test-ns}" + +cleanup () { + echo "Cleaning up test resources..." + oc delete --ignore-not-found=true -n ${TEST_NS} net-attach-def network-bridge network-ovn network-localnet --wait=false + oc delete --ignore-not-found=true -n ${TEST_NS} service example --wait=false + oc delete --ignore-not-found=true -n ${TEST_NS} route auto-test-route --wait=false + oc delete --ignore-not-found=true -n ${TEST_NS} ingress example --wait=false + oc delete --ignore-not-found=true -n ${TEST_NS} networkpolicy auto-test-net-policy --wait=true --timeout=60s + oc delete --ignore-not-found=true -n ${TEST_NS} multi-networkpolicy auto-test-multi-policy --wait=false + oc delete --ignore-not-found=true ClusterUserDefinedNetwork cluster-udn --wait=false + oc delete --ignore-not-found=true -n ${UDN_NS} UserDefinedNetwork primary-udn --wait=false + oc delete nncp --all --ignore-not-found --wait=false + echo "Cleanup done." +} diff --git a/integration-tests/support/login.ts b/integration-tests/support/login.ts index 0c590742..0887092c 100644 --- a/integration-tests/support/login.ts +++ b/integration-tests/support/login.ts @@ -8,7 +8,7 @@ declare global { } const KUBEADMIN_USERNAME = 'kubeadmin'; -const loginUsername = Cypress.env('BRIDGE_KUBEADMIN_PASSWORD') ? 'user-dropdown' : 'username'; +const loginUsername = cy.env('BRIDGE_KUBEADMIN_PASSWORD') ? 'user-dropdown' : 'username'; // This will add 'cy.login(...)' // ex: cy.login('my-user', 'my-password') @@ -24,7 +24,7 @@ Cypress.Commands.add('login', (username: string, password: string) => { cy.clearCookie('openshift-session-token'); cy.get('#inputUsername').type(username || KUBEADMIN_USERNAME); - cy.get('#inputPassword').type(password || Cypress.env('BRIDGE_KUBEADMIN_PASSWORD')); + cy.get('#inputPassword').type(cy.env('BRIDGE_KUBEADMIN_PASSWORD') || password); cy.get('button[type=submit]').click(); cy.get(`[data-test="${loginUsername}"]`).should('be.visible'); diff --git a/integration-tests/tests/example-page.cy.ts b/integration-tests/tests/example-page.cy.ts index 922011c7..9e1ef5ac 100644 --- a/integration-tests/tests/example-page.cy.ts +++ b/integration-tests/tests/example-page.cy.ts @@ -1,7 +1,7 @@ import { checkErrors } from '../support'; const PLUGIN_TEMPLATE_NAME = 'networking-console-plugin'; -const PLUGIN_TEMPLATE_PULL_SPEC = Cypress.env('PLUGIN_TEMPLATE_PULL_SPEC'); +const PLUGIN_TEMPLATE_PULL_SPEC = Cypress.expose('PLUGIN_TEMPLATE_PULL_SPEC'); export const isLocalDevEnvironment = Cypress.config('baseUrl').includes('localhost'); const installHelmChart = (path: string) => { diff --git a/package-lock.json b/package-lock.json index 6ef25bc7..d80cf074 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "css-loader": "^6.7.1", "cypress": "^15.0.0", "cypress-multi-reporters": "^2.0.0", + "dotenv": "^17.4.2", "esbuild-loader": "^4.4.2", "eslint": "^8.57.1", "eslint-config-prettier": "^8.10.0", @@ -7638,6 +7639,19 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", diff --git a/package.json b/package.json index 3632871c..ef189fdc 100644 --- a/package.json +++ b/package.json @@ -13,12 +13,14 @@ "start-console": "./start-console.sh", "i18n": "./i18n-scripts/build-i18n.sh && node ./i18n-scripts/set-english-defaults.js", "ts-node": "ts-node -O '{\"module\":\"commonjs\"}'", - "lint": "eslint ./src ./integration-tests", - "lint-fix": "eslint ./src ./integration-tests --fix", - "test-cypress": "cd integration-tests s && cypress open --env openshift=true", + "lint": "eslint ./src ./integration-tests ./cypress", + "lint-fix": "eslint ./src ./integration-tests ./cypress --fix", + "test-cypress": "cd integration-tests && cypress open --env openshift=true", "test-cypress-headless": "cd integration-tests && node --max-old-space-size=4096 ../node_modules/.bin/cypress run --env openshift=true --browser ${BRIDGE_E2E_BROWSER_NAME:=electron}", - "cypress-merge": "mochawesome-merge ./integration-tests/screenshots/cypress_report*.json > ./integration-tests/screenshots/cypress.json", - "cypress-generate": "marge -o ./integration-tests/screenshots/ -f cypress-report -t 'OpenShift Console Plugin Template Cypress Test Results' -p 'OpenShift Cypress Plugin Template Test Results' --showPassed false --assetsDir ./integration-tests/screenshots/cypress/assets ./integration-tests/screenshots/cypress.json", + "test-e2e": "cd cypress && node --max-old-space-size=4096 ../node_modules/.bin/cypress run --env openshift=true --browser ${BRIDGE_E2E_BROWSER_NAME:=electron}", + "test-e2e-open": "cd cypress && cypress open --env openshift=true", + "cypress-merge": "mochawesome-merge ./cypress/gui-test-screenshots/cypress_report*.json > ./cypress/gui-test-screenshots/cypress.json", + "cypress-generate": "marge -o ./cypress/gui-test-screenshots/ -f cypress-report -t 'Networking Console Plugin Test Results' -p 'Networking Plugin Test Results' --showPassed false --assetsDir ./cypress/gui-test-screenshots/assets ./cypress/gui-test-screenshots/cypress.json", "cypress-postreport": "npm run cypress-merge && npm run cypress-generate", "memsource-upload": "./i18n-scripts/memsource-upload.sh", "memsource-download": "./i18n-scripts/memsource-download.sh", @@ -58,6 +60,7 @@ "css-loader": "^6.7.1", "cypress": "^15.0.0", "cypress-multi-reporters": "^2.0.0", + "dotenv": "^17.4.2", "esbuild-loader": "^4.4.2", "eslint": "^8.57.1", "eslint-config-prettier": "^8.10.0", @@ -79,8 +82,8 @@ "pluralize": "^8.0.0", "prettier": "^3.2.5", "react": "18.3.1", - "react-dom": "18.3.1", "react-copy-to-clipboard": "^5.1.0", + "react-dom": "18.3.1", "react-i18next": "~16.5.8", "react-router": "~7.13.1", "resolve-url-loader": "^5.0.0", diff --git a/research-flakiness.sh b/research-flakiness.sh new file mode 100755 index 00000000..459d6ba1 --- /dev/null +++ b/research-flakiness.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash + +set -x +set +e + +PROBLEM_DIR="problem" +TOTAL_RUNS=10 +PASS_COUNT=0 +FAIL_COUNT=0 + +mkdir -p "${PROBLEM_DIR}" + +for i in $(seq -w 1 ${TOTAL_RUNS}); do + echo "========================================" + echo " RUN ${i} of ${TOTAL_RUNS}" + echo "========================================" + + # Clean previous test artifacts + rm -rf cypress/gui-test-screenshots/* + + # Run the test suite + ./test-cypress.sh + exit_code=$? + + if [ ${exit_code} -ne 0 ]; then + FAIL_COUNT=$((FAIL_COUNT + 1)) + RUN_DIR="${PROBLEM_DIR}/run${i}" + mkdir -p "${RUN_DIR}" + + # Copy screenshots and videos + if [ -d "cypress/gui-test-screenshots/screenshots" ]; then + cp -r cypress/gui-test-screenshots/screenshots "${RUN_DIR}/" + fi + if [ -d "cypress/gui-test-screenshots/videos" ]; then + cp -r cypress/gui-test-screenshots/videos "${RUN_DIR}/" + fi + if [ -f "cypress/gui-test-screenshots/build.log" ]; then + cp cypress/gui-test-screenshots/build.log "${RUN_DIR}/" + fi + + # Extract failure info into analysis.md + cat > "${RUN_DIR}/analysis.md" </dev/null || echo "Could not extract test names") + +## Error messages + +$(grep -A5 "^\s+\d\) " cypress/gui-test-screenshots/build.log 2>/dev/null | head -60 || echo "Could not extract errors") + +## Observations + +- The NAD creation form uses react-hook-form with auto-generated names +- The naive approach (clear + type without waits) races with React re-renders +- The form may overwrite user input after async state updates + +ANALYSIS + + echo " -> FAILED (evidence saved to ${RUN_DIR}/)" + else + PASS_COUNT=$((PASS_COUNT + 1)) + echo " -> PASSED" + fi +done + +echo "" +echo "========================================" +echo " RESULTS: ${PASS_COUNT} passed, ${FAIL_COUNT} failed out of ${TOTAL_RUNS} runs" +echo "========================================" + +# Write summary +cat > "${PROBLEM_DIR}/summary.md" </dev/null || true +TEST_NS="${TEST_NS:-cy-test-ns}" +UDN_NS="${UDN_NS:-udn-test-ns}" +UDN_LABEL="k8s.ovn.org/primary-user-defined-network" + +setup () { + echo "Setting up test namespaces..." + oc get namespace ${TEST_NS} 2>/dev/null || oc create namespace ${TEST_NS} + + # UDN namespace must have the label at creation time (admission policy prevents adding later) + if oc get namespace ${UDN_NS} 2>/dev/null; then + if ! oc get namespace ${UDN_NS} -o jsonpath='{.metadata.labels}' | grep -q "${UDN_LABEL}"; then + echo "UDN namespace exists but missing label, recreating..." + oc delete namespace ${UDN_NS} --wait=true --timeout=120s + oc wait --for=delete namespace/${UDN_NS} --timeout=120s 2>/dev/null || true + oc create -f - < { + const options = { + webpackOptions: { + module: { + rules: [ + { + exclude: /node_modules/, + loader: 'esbuild-loader', + test: /\.ts$/, + }, + ], + }, + resolve: { + extensions: ['.ts', '.tsx', '.js'], + }, + }, + }; + on('file:preprocessor', wp(options)); + config.baseUrl = `${process.env.BRIDGE_BASE_ADDRESS || 'http://localhost:9000/'}`; + config.env.BRIDGE_KUBEADMIN_PASSWORD = process.env.BRIDGE_KUBEADMIN_PASSWORD; + config.env.TEST_NS = process.env.TEST_NS; + config.env.UDN_NS = process.env.UDN_NS; + config.env.HIDE_XHR = process.env.HIDE_XHR; + return config; +}; diff --git a/ui-tests-cy/reporter-config.json b/ui-tests-cy/reporter-config.json new file mode 100644 index 00000000..83ca6c5d --- /dev/null +++ b/ui-tests-cy/reporter-config.json @@ -0,0 +1,14 @@ +{ + "reporterEnabled": "mocha-junit-reporter, mochawesome", + "mochaJunitReporterReporterOptions": { + "mochaFile": "./gui-test-screenshots/junit_cypress-[hash].xml", + "toConsole": false + }, + "mochawesomeReporterOptions": { + "reportDir": "./gui-test-screenshots/", + "reportFilename": "cypress_report", + "overwrite": false, + "html": true, + "json": true + } +} \ No newline at end of file diff --git a/ui-tests-cy/support/commands.ts b/ui-tests-cy/support/commands.ts new file mode 100644 index 00000000..c2a7ebed --- /dev/null +++ b/ui-tests-cy/support/commands.ts @@ -0,0 +1,48 @@ +import { MINUTE, SECOND } from '../utils/const/base'; + +declare global { + namespace Cypress { + interface Chainable { + deleteResource(kind: string, name: string, namespace?: string): void; + switchProject(projectName: string): void; + } + } +} + +Cypress.Commands.add('deleteResource', (kind: string, name: string, namespace?: string) => { + if (!namespace) { + cy.exec(`oc delete --ignore-not-found=true ${kind} ${name} --wait=true --timeout=300s`, { + failOnNonZeroExit: false, + timeout: 5 * MINUTE, + }); + return; + } + cy.exec( + `oc delete --ignore-not-found=true -n ${namespace} ${kind} ${name} --wait=true --timeout=300s`, + { failOnNonZeroExit: false, timeout: 5 * MINUTE }, + ); +}); + +Cypress.Commands.add('switchProject', (projectName: string) => { + cy.byLegacyTestID('namespace-bar-dropdown').contains('Project:').click(); + if (projectName === 'All Projects') { + cy.contains('All Projects').click(); + return; + } + cy.get('body').then(($body) => { + if ($body.find('[data-test="showSystemSwitch"]').length) { + cy.byTestID('showSystemSwitch').check(); + cy.wait(3 * SECOND); + } + }); + cy.get( + 'input[data-test="dropdown-text-filter"], input[placeholder*="name"], input[aria-label*="filter"]', + { timeout: 10000 }, + ) + .first() + .clear() + .type(projectName); + cy.contains('[data-test="dropdown-menu-item-link"], [role="option"], button', projectName, { + timeout: 10000, + }).click(); +}); diff --git a/ui-tests-cy/support/index.ts b/ui-tests-cy/support/index.ts new file mode 100644 index 00000000..588da326 --- /dev/null +++ b/ui-tests-cy/support/index.ts @@ -0,0 +1,18 @@ +import './login'; +import './selectors'; +import './commands'; +import './nav'; + +Cypress.on('uncaught:exception', () => { + return false; +}); + +if (Cypress.env('HIDE_XHR')) { + const origLog = Cypress.log; + Cypress.log = function (opts, ...other) { + if (opts.displayName === 'fetch' || opts.displayName === 'xhr') { + return; + } + return origLog(opts, ...other); + }; +} diff --git a/ui-tests-cy/support/login.ts b/ui-tests-cy/support/login.ts new file mode 100644 index 00000000..484828d5 --- /dev/null +++ b/ui-tests-cy/support/login.ts @@ -0,0 +1,60 @@ +import { MINUTE } from '../utils/const/base'; + +const KUBEADMIN_USERNAME = 'kubeadmin'; +const KUBEADMIN_IDP = 'kube:admin'; +const TOUR_DISMISS = '[data-test="tour-step-footer-secondary"]'; + +declare global { + namespace Cypress { + interface Chainable { + login(providerName?: string, username?: string, password?: string): Chainable; + logout(): void; + } + } +} + +Cypress.Commands.add('login', (provider: string, username: string, password: string) => { + const usr = username || KUBEADMIN_USERNAME; + const pwd = password || Cypress.env('BRIDGE_KUBEADMIN_PASSWORD'); + const idp = provider || KUBEADMIN_IDP; + + cy.visit(''); + + cy.origin( + Cypress.config('baseUrl').replace('console-openshift-console', 'oauth-openshift'), + { args: { idp, pwd, usr } }, + ({ idp: originIdp, pwd: originPwd, usr: originUsr }) => { + // Wait for either the login form or IDP selection page + cy.get('body', { timeout: 180000 }).should('be.visible'); + cy.get('body').then(($body) => { + if ($body.find('#inputUsername').length === 0) { + // IDP selection page — click the matching provider + if ($body.text().includes(originIdp)) { + cy.contains('a', originIdp).click(); + } else if ($body.text().includes('kubeadmin')) { + cy.contains('a', 'kubeadmin').click(); + } else { + cy.get('a').first().click(); + } + } + }); + cy.get('#inputUsername', { timeout: 180000 }).should('be.visible'); + cy.get('#inputUsername').type(originUsr); + cy.get('#inputPassword').type(originPwd); + cy.get('button[type=submit]').click(); + }, + ); + + cy.url({ timeout: 2 * MINUTE }).should('include', 'console-openshift-console'); + cy.get('body').then(($body) => { + if ($body.find(TOUR_DISMISS).length) { + cy.get(TOUR_DISMISS).click(); + } + }); +}); + +Cypress.Commands.add('logout', () => { + cy.get('[data-test="user-dropdown"]').click(); + cy.get('[data-test="log-out"]').should('be.visible'); + cy.get('[data-test="log-out"]').click({ force: true }); +}); diff --git a/ui-tests-cy/support/nav.ts b/ui-tests-cy/support/nav.ts new file mode 100644 index 00000000..bde8f12d --- /dev/null +++ b/ui-tests-cy/support/nav.ts @@ -0,0 +1,28 @@ +import { MINUTE } from '../utils/const/base'; + +declare global { + namespace Cypress { + interface Chainable { + visitNAD(): void; + visitService(): void; + visitUDN(): void; + } + } +} + +Cypress.Commands.add('visitNAD', () => { + const ns = Cypress.env('TEST_NS') || 'cy-test-ns'; + cy.visit(`/k8s/ns/${ns}/k8s.cni.cncf.io~v1~NetworkAttachmentDefinition`); + cy.checkTitle('NetworkAttachmentDefinitions', MINUTE); +}); + +Cypress.Commands.add('visitUDN', () => { + cy.visit('/k8s/all-namespaces/k8s.ovn.org~v1~UserDefinedNetwork'); + cy.checkTitle('UserDefinedNetworks', MINUTE); + cy.byButtonText('Create').should('be.visible'); +}); + +Cypress.Commands.add('visitService', () => { + cy.visit('/k8s/all-namespaces/services'); + cy.checkTitle('Services', MINUTE); +}); diff --git a/ui-tests-cy/support/selectors.ts b/ui-tests-cy/support/selectors.ts new file mode 100644 index 00000000..935fb8ec --- /dev/null +++ b/ui-tests-cy/support/selectors.ts @@ -0,0 +1,81 @@ +import Loggable = Cypress.Loggable; +import Timeoutable = Cypress.Timeoutable; +import Withinable = Cypress.Withinable; +import Shadow = Cypress.Shadow; +import { MINUTE, SECOND } from '../utils/const/base'; + +export {}; +declare global { + namespace Cypress { + interface Chainable { + byButtonText(selector: string): Chainable; + byLegacyTestID(selector: string): Chainable; + byTestActionID(selector: string): Chainable; + byTestID( + selector: string, + options?: Partial, + ): Chainable; + byTestRows(selector: string): Chainable; + checkSubTitle(title: string, timeout?: number): void; + checkTitle(title: string, timeout?: number): void; + clickBtn(btnTxt: string): void; + clickNavLink(path: [string, string?]): Chainable; + clickNextBtn(): void; + clickSaveBtn(): void; + } + } +} + +Cypress.Commands.add( + 'byTestID', + (selector: string, options?: Partial) => { + cy.get(`[data-test="${selector}"]`, options); + }, +); + +Cypress.Commands.add('byLegacyTestID', (selector: string) => + cy.get(`[data-test-id="${selector}"]`, { timeout: MINUTE * 3 }), +); + +Cypress.Commands.add('byTestRows', (selector: string) => + cy.get(`[data-test-rows="${selector}"]`, { timeout: MINUTE }), +); + +Cypress.Commands.add('byTestActionID', (selector: string) => + cy.get(`[data-test-action="${selector}"]:not(.pf-m-disabled)`), +); + +Cypress.Commands.add('clickNavLink', (path: [string, string?]) => { + cy.byTestID('nav', { timeout: MINUTE }) + .contains(path[0], { timeout: SECOND * 10 }) + .should(($el) => { + if ($el.attr('aria-expanded') == 'false') { + $el.click(); + } + }); + if (path.length > 1) { + cy.get('#page-sidebar').contains(path[1]).click(); + } +}); + +Cypress.Commands.add('byButtonText', (selector: string) => cy.contains('button', `${selector}`)); + +Cypress.Commands.add('clickSaveBtn', () => { + cy.get('button[data-test="save-button"]').click({ force: true }); +}); + +Cypress.Commands.add('checkTitle', (title: string, timeout?: number) => { + const t_o = timeout ? timeout : MINUTE * 3; + cy.contains('h1', title, { timeout: t_o }).should('exist'); +}); + +Cypress.Commands.add('checkSubTitle', (subTitle: string, timeout?: number) => { + const t_o = timeout ? timeout : MINUTE * 3; + cy.contains('h2', subTitle, { timeout: t_o }).should('exist'); +}); + +Cypress.Commands.add('clickNextBtn', () => cy.contains('button[type="submit"]', 'Next').click()); + +Cypress.Commands.add('clickBtn', (btnTxt?: string) => { + return cy.contains('button[type="submit"]', btnTxt).click(); +}); diff --git a/ui-tests-cy/tests/all.cy.ts b/ui-tests-cy/tests/all.cy.ts new file mode 100644 index 00000000..a78e00c0 --- /dev/null +++ b/ui-tests-cy/tests/all.cy.ts @@ -0,0 +1,10 @@ +import './setup/login.cy.ts'; +import './setup/visit-pages.cy.ts'; +import './networking/udn.cy.ts'; +import './networking/nad-bridge.cy.ts'; +import './networking/nad-localnet.cy.ts'; +import './networking/nad-ovn.cy.ts'; +import './networking/services.cy.ts'; +import './networking/routes.cy.ts'; +import './networking/ingresses.cy.ts'; +import './networking/net-policies.cy.ts'; diff --git a/ui-tests-cy/tests/networking/ingresses.cy.ts b/ui-tests-cy/tests/networking/ingresses.cy.ts new file mode 100644 index 00000000..c6f820ba --- /dev/null +++ b/ui-tests-cy/tests/networking/ingresses.cy.ts @@ -0,0 +1,20 @@ +import { EXAMPLE, MINUTE, TEST_NS } from '../../utils/const/base'; +import { checkActionMenu } from '../../views/actions'; +import { createBtn } from '../../views/nad'; +import { brCrumbItem, itemFilter, row } from '../../views/selector-common'; + +describe('Check Ingresses page', () => { + it('create Ingress with YAML', () => { + cy.visit(`/k8s/ns/${TEST_NS}/ingresses`); + cy.checkTitle('Ingresses', MINUTE); + cy.byButtonText('Create Ingress').click(); + cy.get('.monaco-editor', { timeout: 30000 }).should('exist'); + cy.get(createBtn).click(); + cy.checkTitle(EXAMPLE); + checkActionMenu('Ingress'); + cy.contains(brCrumbItem, 'Ingresses').find('a').click(); + cy.get(itemFilter).clear(); + cy.get(itemFilter).type(EXAMPLE); + cy.contains(row, EXAMPLE).should('exist'); + }); +}); diff --git a/ui-tests-cy/tests/networking/nad-bridge.cy.ts b/ui-tests-cy/tests/networking/nad-bridge.cy.ts new file mode 100644 index 00000000..e77949fa --- /dev/null +++ b/ui-tests-cy/tests/networking/nad-bridge.cy.ts @@ -0,0 +1,9 @@ +import { adminOnlyDescribe } from '../../utils/const/base'; +import { NAD_BRIDGE } from '../../utils/const/nad'; +import { createNAD } from '../../views/nad'; + +adminOnlyDescribe('Test linux bridge NAD', () => { + it('create NAD with MAC Spoof checked', () => { + createNAD(NAD_BRIDGE); + }); +}); diff --git a/ui-tests-cy/tests/networking/nad-localnet.cy.ts b/ui-tests-cy/tests/networking/nad-localnet.cy.ts new file mode 100644 index 00000000..a92c4b1d --- /dev/null +++ b/ui-tests-cy/tests/networking/nad-localnet.cy.ts @@ -0,0 +1,14 @@ +import { adminOnlyDescribe } from '../../utils/const/base'; +import { NAD_LOCALNET } from '../../utils/const/nad'; +import { createNAD, deleteNAD } from '../../views/nad'; + +adminOnlyDescribe('Test secondary localnet NAD', () => { + it('create NAD with secondary localnet network', () => { + createNAD(NAD_LOCALNET); + }); + + it('delete NAD', () => { + cy.visitNAD(); + deleteNAD(NAD_LOCALNET.name); + }); +}); diff --git a/ui-tests-cy/tests/networking/nad-ovn.cy.ts b/ui-tests-cy/tests/networking/nad-ovn.cy.ts new file mode 100644 index 00000000..dae5402c --- /dev/null +++ b/ui-tests-cy/tests/networking/nad-ovn.cy.ts @@ -0,0 +1,9 @@ +import { adminOnlyDescribe } from '../../utils/const/base'; +import { NAD_OVN } from '../../utils/const/nad'; +import { createNAD } from '../../views/nad'; + +adminOnlyDescribe('Test L2 overlay NAD', () => { + it('create NAD with L2 overlay network', () => { + createNAD(NAD_OVN); + }); +}); diff --git a/ui-tests-cy/tests/networking/net-policies.cy.ts b/ui-tests-cy/tests/networking/net-policies.cy.ts new file mode 100644 index 00000000..ca9edb24 --- /dev/null +++ b/ui-tests-cy/tests/networking/net-policies.cy.ts @@ -0,0 +1,31 @@ +import { MINUTE, TEST_NS, TEST_POL_NAME } from '../../utils/const/base'; +import { checkActionMenu, nameInput } from '../../views/actions'; +import { brCrumbItem, egressOff, ingressOff, itemFilter, row } from '../../views/selector-common'; + +const denyTraffic = () => { + cy.get(ingressOff).check(); + cy.get('body').then(($body) => { + if ($body.find(egressOff).length) { + cy.get(egressOff).check(); + } + }); +}; + +describe('Check NetworkPolicies page', () => { + it('create NetworkPolicy with form', () => { + cy.visit(`/k8s/ns/${TEST_NS}/networkpolicies`); + cy.checkTitle('NetworkPolicies', MINUTE); + cy.byButtonText('Create NetworkPolicy').click(); + cy.get('input#form').check(); + cy.get(nameInput, { timeout: 10000 }).should('be.visible').clear(); + cy.get(nameInput).type(TEST_POL_NAME); + cy.get(nameInput).should('have.value', TEST_POL_NAME); + denyTraffic(); + cy.clickBtn('Create'); + cy.checkTitle(TEST_POL_NAME); + checkActionMenu('NetworkPolicy'); + cy.contains(brCrumbItem, 'NetworkPolicy').find('a').click(); + cy.get(itemFilter).type(TEST_POL_NAME); + cy.contains(row, TEST_POL_NAME).should('exist'); + }); +}); diff --git a/ui-tests-cy/tests/networking/routes.cy.ts b/ui-tests-cy/tests/networking/routes.cy.ts new file mode 100644 index 00000000..3ea07dd9 --- /dev/null +++ b/ui-tests-cy/tests/networking/routes.cy.ts @@ -0,0 +1,32 @@ +import { EXAMPLE, MINUTE, TEST_NS, TEST_ROUTE_NAME } from '../../utils/const/base'; +import { checkActionMenu, nameInput } from '../../views/actions'; +import { createBtn } from '../../views/nad'; +import { brCrumbItem, itemFilter, row } from '../../views/selector-common'; + +describe('Check Routes page', () => { + it('create Route with form', () => { + cy.exec( + `oc get service ${EXAMPLE} -n ${TEST_NS} 2>/dev/null || oc create service clusterip ${EXAMPLE} --tcp=80:80 -n ${TEST_NS}`, + { failOnNonZeroExit: false }, + ); + cy.visit(`/k8s/ns/${TEST_NS}/routes`); + cy.checkTitle('Routes', MINUTE); + cy.byButtonText('Create Route').click(); + cy.get('input#form').check(); + cy.get(nameInput).clear(); + cy.get(nameInput).type(TEST_ROUTE_NAME); + cy.byButtonText('Select a Service').click(); + cy.byButtonText(EXAMPLE).click(); + cy.byButtonText('Select target port').click(); + cy.get('#target-port').within(() => { + cy.get('button').click(); + }); + cy.get(createBtn).click(); + cy.checkTitle(TEST_ROUTE_NAME); + checkActionMenu('Route'); + cy.contains(brCrumbItem, 'Routes').find('a').click(); + cy.get(itemFilter).clear(); + cy.get(itemFilter).type(TEST_ROUTE_NAME); + cy.contains(row, TEST_ROUTE_NAME).should('exist'); + }); +}); diff --git a/ui-tests-cy/tests/networking/services.cy.ts b/ui-tests-cy/tests/networking/services.cy.ts new file mode 100644 index 00000000..8f502647 --- /dev/null +++ b/ui-tests-cy/tests/networking/services.cy.ts @@ -0,0 +1,19 @@ +import { EXAMPLE, MINUTE, TEST_NS } from '../../utils/const/base'; +import { checkActionMenu } from '../../views/actions'; +import { createBtn } from '../../views/nad'; +import { brCrumbItem, itemFilter, row } from '../../views/selector-common'; + +describe('Check Services page', () => { + it('create Service with YAML', () => { + cy.visit(`/k8s/ns/${TEST_NS}/services`); + cy.checkTitle('Services', MINUTE); + cy.byButtonText('Create Service').click(); + cy.get('.monaco-editor', { timeout: 30000 }).should('exist'); + cy.get(createBtn).click(); + cy.checkTitle(EXAMPLE); + checkActionMenu('Service'); + cy.contains(brCrumbItem, 'Services').find('a').click(); + cy.get(itemFilter).type(EXAMPLE); + cy.contains(row, EXAMPLE).should('exist'); + }); +}); diff --git a/ui-tests-cy/tests/networking/udn.cy.ts b/ui-tests-cy/tests/networking/udn.cy.ts new file mode 100644 index 00000000..cc6d5599 --- /dev/null +++ b/ui-tests-cy/tests/networking/udn.cy.ts @@ -0,0 +1,46 @@ +import { adminOnlyDescribe, MINUTE, UDN_NS } from '../../utils/const/base'; +import { checkActionMenu } from '../../views/actions'; +import { brCrumbItem } from '../../views/selector-common'; +import { createUDN } from '../../views/udn'; + +const SUBNET = '192.168.1.1/24'; +const UDN_CLST = 'cluster-udn'; + +adminOnlyDescribe('Test UDN', () => { + it('create UDN', () => { + createUDN(UDN_NS, SUBNET); + }); + + it('create CUDN', () => { + cy.visit('/k8s/all-namespaces/k8s.ovn.org~v1~UserDefinedNetwork'); + cy.checkTitle('UserDefinedNetworks', MINUTE); + cy.get( + '[data-test="item-create"] .pf-v6-c-menu-toggle__controls, [data-test="item-create"] [aria-label="Menu toggle"]', + ).click(); + cy.contains('ClusterUserDefinedNetwork').click(); + cy.byTestID('input-name', { timeout: 30000 }).should('be.visible'); + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(1000); + cy.byTestID('input-name').clear(); + cy.byTestID('input-name').type(UDN_CLST); + cy.byTestID('input-udn-subnet').clear(); + cy.byTestID('input-udn-subnet').type(SUBNET); + cy.byTestID('tags-input').clear(); + cy.byTestID('tags-input').type(`kubernetes.io/metadata.name=${UDN_NS}`); + cy.byButtonText('Review selected').click(); + cy.byTestID('create-udn-submit').click(); + cy.checkTitle(UDN_CLST); + checkActionMenu('ClusterUserDefinedNetwork'); + cy.contains(brCrumbItem, 'ClusterUserDefinedNetworks').find('a').eq(0).click(); + }); + + it('delete CUDN', () => { + cy.visit('/k8s/all-namespaces/k8s.ovn.org~v1~UserDefinedNetwork'); + cy.checkTitle('UserDefinedNetworks', MINUTE); + cy.contains('[data-label="name"]', UDN_CLST).find('a').click(); + cy.byButtonText('Actions').click(); + cy.get('[data-test-action="Delete ClusterUserDefinedNetwork"]').click(); + cy.byTestID('confirm-action').click(); + cy.contains(UDN_CLST).should('not.exist'); + }); +}); diff --git a/ui-tests-cy/tests/setup/login.cy.ts b/ui-tests-cy/tests/setup/login.cy.ts new file mode 100644 index 00000000..00492e98 --- /dev/null +++ b/ui-tests-cy/tests/setup/login.cy.ts @@ -0,0 +1,5 @@ +describe('Login', () => { + it('login to OpenShift console', () => { + cy.login(); + }); +}); diff --git a/ui-tests-cy/tests/setup/visit-pages.cy.ts b/ui-tests-cy/tests/setup/visit-pages.cy.ts new file mode 100644 index 00000000..6ec553f9 --- /dev/null +++ b/ui-tests-cy/tests/setup/visit-pages.cy.ts @@ -0,0 +1,35 @@ +import { MINUTE } from '../../utils/const/base'; + +describe('Visit networking pages', () => { + it('visit NetworkAttachmentDefinitions page', () => { + cy.get('[data-quickstart-id="qs-nav-networking"]', { timeout: MINUTE }).scrollIntoView(); + cy.contains('Networking').should('be.visible'); + cy.clickNavLink(['Networking', 'NetworkAttachmentDefinitions']); + cy.checkTitle('NetworkAttachmentDefinitions', MINUTE); + }); + + it('visit UserDefinedNetworks page', () => { + cy.clickNavLink(['Networking', 'UserDefinedNetworks']); + cy.checkTitle('UserDefinedNetworks', MINUTE); + }); + + it('visit Services page', () => { + cy.clickNavLink(['Networking', 'Services']); + cy.checkTitle('Services', MINUTE); + }); + + it('visit Routes page', () => { + cy.clickNavLink(['Networking', 'Routes']); + cy.checkTitle('Routes', MINUTE); + }); + + it('visit Ingresses page', () => { + cy.clickNavLink(['Networking', 'Ingresses']); + cy.checkTitle('Ingresses', MINUTE); + }); + + it('visit NetworkPolicies page', () => { + cy.clickNavLink(['Networking', 'NetworkPolicies']); + cy.checkTitle('NetworkPolicies', MINUTE); + }); +}); diff --git a/ui-tests-cy/tsconfig.json b/ui-tests-cy/tsconfig.json new file mode 100644 index 00000000..c8f71161 --- /dev/null +++ b/ui-tests-cy/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "noEmit": true, + "types":["cypress","node"], + "isolatedModules": false + }, + "include": ["../node_modules/cypress", "./**/*.ts"] +} \ No newline at end of file diff --git a/ui-tests-cy/utils/const/base.ts b/ui-tests-cy/utils/const/base.ts new file mode 100644 index 00000000..432cbbfd --- /dev/null +++ b/ui-tests-cy/utils/const/base.ts @@ -0,0 +1,23 @@ +export const adminOnlyDescribe = Cypress.env('NON_PRIV') ? xdescribe : describe; +export const adminOnlyIT = Cypress.env('NON_PRIV') ? xit : it; + +export const TEST_NS = Cypress.env('TEST_NS') || 'cy-test-ns'; +export const UDN_NS = Cypress.env('UDN_NS') || 'udn-test-ns'; + +export const EXAMPLE = 'example'; +export const ALL_PROJ_NS = 'All Projects'; +export const TEST_ROUTE_NAME = 'auto-test-route'; +export const TEST_POL_NAME = 'auto-test-net-policy'; +export const TEST_MULTI_NAME = 'auto-test-multi-policy'; + +export const SECOND = 1000; +export const MINUTE = 60000; + +export const UDN_LABEL = 'k8s.ovn.org/primary-user-defined-network'; + +export enum K8S_KIND { + CUDN = 'ClusterUserDefinedNetwork', + NAD = 'net-attach-def', + Project = 'project', + UDN = 'UserDefinedNetwork', +} diff --git a/ui-tests-cy/utils/const/nad.ts b/ui-tests-cy/utils/const/nad.ts new file mode 100644 index 00000000..96cc3434 --- /dev/null +++ b/ui-tests-cy/utils/const/nad.ts @@ -0,0 +1,28 @@ +import { NadData } from '../types/nad'; + +export const bridge = 'br0'; + +export const NAD_BRIDGE: NadData = { + bridge: bridge, + description: 'bridge nad', + macSpoof: true, + name: 'network-bridge', + type: 'Bridge', + vlan: '100', +}; + +export const NAD_OVN: NadData = { + description: 'ovn nad', + name: 'network-ovn', + type: 'OVN', + vlan: '200', +}; + +export const NAD_LOCALNET: NadData = { + bridge: bridge, + description: 'localnet nad', + mtu: '1500', + name: 'network-localnet', + type: 'Localnet', + vlan: '300', +}; diff --git a/ui-tests-cy/utils/types/nad.ts b/ui-tests-cy/utils/types/nad.ts new file mode 100644 index 00000000..58116cb6 --- /dev/null +++ b/ui-tests-cy/utils/types/nad.ts @@ -0,0 +1,11 @@ +export type NadData = { + name?: string; + description?: string; + type?: string; + bridge?: string; + vlan?: string; + mtu?: string; + macSpoof?: boolean; + subnet?: string; + exclude?: string; +}; diff --git a/ui-tests-cy/views/actions.ts b/ui-tests-cy/views/actions.ts new file mode 100644 index 00000000..f41d8972 --- /dev/null +++ b/ui-tests-cy/views/actions.ts @@ -0,0 +1,12 @@ +export const nameInput = 'input#name'; + +export const getRow = (name: string, within: VoidFunction) => + cy.byTestRows('resource-row').contains(name).parents('tr').within(within); + +export const checkActionMenu = (item: string) => { + cy.byButtonText('Actions').click(); + cy.contains('button', 'Edit labels').should('exist'); + cy.contains('button', 'Edit annotations').should('exist'); + cy.contains('button', `Edit ${item}`).should('exist'); + cy.contains('button', `Delete ${item}`).should('exist'); +}; diff --git a/ui-tests-cy/views/nad.ts b/ui-tests-cy/views/nad.ts new file mode 100644 index 00000000..f7e2e63d --- /dev/null +++ b/ui-tests-cy/views/nad.ts @@ -0,0 +1,77 @@ +import { NadData } from '../utils/types/nad'; + +import { row } from './selector-common'; + +export const name = 'input[name="name"]'; +export const description = 'input[name="description"]'; +export const type = '#toggle-nads-network-type'; +export const cnvBridgeLink = '[data-test-dropdown-menu="cnv-bridge"]'; +export const ovnNetwork = 'button[id="ovn-k8s-cni-overlay-link"]'; +export const localnet = 'button[id="ovn-k8s-cni-overlay-localnet-link"]'; +export const bridgeName = 'input[name="bridge.bridge"]'; +export const bridgeVlan = 'input[name="bridge.vlanTagNum"]'; +export const bridgeMap = 'input[name="ovn-k8s-cni-overlay-localnet.bridgeMapping"]'; +export const bridgeMTU = 'input[name="ovn-k8s-cni-overlay-localnet.mtu"]'; +export const localnetVlan = 'input[name="ovn-k8s-cni-overlay-localnet.vlanID"]'; +export const confirmBtn = '[data-test="confirm-action"]'; +export const heading = '[data-test-section-heading="NetworkAttachmentDefinition details"]'; +export const createBtn = '#save-changes'; +export const macSpoofCHK = 'input[id="bridge.macspoofchk"]'; +export const ovnSubnet = 'input[name="ovn-k8s-cni-overlay.subnets"]'; +export const localnetSubnet = 'input[name="ovn-k8s-cni-overlay-localnet.subnets"]'; +export const exclude = 'input[name="ovn-k8s-cni-overlay-localnet.excludeSubnets"]'; + +export const createNAD = (nad: NadData) => { + cy.visitNAD(); + cy.byButtonText('Create NetworkAttachmentDefinition').click(); + cy.get('#form').check(); + cy.get(name, { timeout: 10000 }).should('be.visible'); + cy.get(name).clear().type(nad.name); + cy.get(description).clear().type(nad.description); + cy.get(type).click(); + switch (nad.type) { + case 'Bridge': { + cy.byButtonText('Linux bridge').click(); + cy.get(bridgeName).clear().type(nad.bridge); + if (nad.vlan) { + cy.get(bridgeVlan).clear().type(nad.vlan); + } + if (!nad.macSpoof) { + cy.get(macSpoofCHK).uncheck(); + } + break; + } + case 'OVN': { + cy.byButtonText('L2 overlay').click(); + if (nad.subnet) { + cy.get(ovnSubnet).type(nad.subnet); + } + break; + } + case 'Localnet': { + cy.byButtonText('secondary localnet').click(); + cy.get(bridgeMap).clear().type(nad.bridge); + cy.get(bridgeMTU).clear().type(nad.mtu); + if (nad.vlan) { + cy.get(localnetVlan).clear().type(nad.vlan); + } + if (nad.subnet) { + cy.get(localnetSubnet).type(nad.subnet); + cy.get(exclude).type(nad.exclude); + } + break; + } + } + cy.get(createBtn).click(); + cy.get(heading).should('exist'); + cy.contains('[data-test-selector="details-item-label__Description"]', 'Description') + .parent() + .should('contain', nad.description); +}; + +export const deleteNAD = (nadName: string) => { + cy.contains(row, nadName).find('.pf-v6-c-table__action').find('button').click(); + cy.byButtonText('Delete').click(); + cy.get(confirmBtn).click(); + cy.contains(row, nadName).should('not.exist'); +}; diff --git a/ui-tests-cy/views/selector-common.ts b/ui-tests-cy/views/selector-common.ts new file mode 100644 index 00000000..77ed1559 --- /dev/null +++ b/ui-tests-cy/views/selector-common.ts @@ -0,0 +1,14 @@ +export const row = '[data-test-rows="resource-row"]'; +export const actionsBtn = 'button.pf-v6-c-menu-toggle.pf-m-plain'; +export const itemCreateBtn = '[data-test="item-create"]'; +export const labelContent = '.pf-v6-c-label__content'; +export const brCrumbItem = '[aria-label="Breadcrumb"]'; +export const itemFilter = '[data-test-id="item-filter"]'; +export const createBtn = '#save-changes'; +export const confirmBtn = '[data-test="confirm-action"]'; +export const dropDownItem = '.pf-v6-c-dropdown__menu-item'; + +export const ingressHeader = '#ingress-header'; +export const egressHeader = '#egress-header'; +export const ingressOff = '#denyAllIngress'; +export const egressOff = '#denyAllEgress'; diff --git a/ui-tests-cy/views/udn.ts b/ui-tests-cy/views/udn.ts new file mode 100644 index 00000000..8b063796 --- /dev/null +++ b/ui-tests-cy/views/udn.ts @@ -0,0 +1,39 @@ +import { ALL_PROJ_NS } from '../utils/const/base'; + +import { checkActionMenu } from './actions'; +import { row } from './selector-common'; +import { brCrumbItem } from './selector-common'; + +export const UDN_PRIM_UDN_NS = 'primary-udn'; + +export const createUDN = (project: string, subnet: string) => { + cy.visitUDN(); + cy.byButtonText('Create').click(); + cy.get('body').then(($body) => { + if ($body.find('[data-test="list-page-create-dropdown-item-UserDefinedNetwork"]').length) { + cy.get('[data-test="list-page-create-dropdown-item-UserDefinedNetwork"]') + .find('button') + .click(); + } + }); + cy.checkTitle('Create UserDefinedNetwork'); + cy.get('#typeahead-select-input').find('input').type(project); + cy.get(`#select-typeahead-${project}`, { timeout: 10000 }).click(); + cy.byTestID('input-udn-subnet').clear(); + cy.byTestID('input-udn-subnet').type(subnet); + cy.byTestID('create-udn-submit').should('not.be.disabled').click(); + cy.checkTitle(UDN_PRIM_UDN_NS); + checkActionMenu('UserDefinedNetwork'); + cy.contains(brCrumbItem, 'UserDefinedNetworks').find('a').eq(0).click(); + cy.contains(row, UDN_PRIM_UDN_NS).should('exist'); +}; + +export const deleteUDN = () => { + cy.visitUDN(); + cy.switchProject(ALL_PROJ_NS); + cy.contains('[data-label="name"]', UDN_PRIM_UDN_NS).find('a').click(); + cy.byButtonText('Actions').click(); + cy.get('[data-test-action="Delete UserDefinedNetwork"]').click(); + cy.byTestID('confirm-action').click(); + cy.contains(UDN_PRIM_UDN_NS).should('not.exist'); +}; diff --git a/ui/MIGRATION.md b/ui/MIGRATION.md new file mode 100644 index 00000000..7862ac12 --- /dev/null +++ b/ui/MIGRATION.md @@ -0,0 +1,55 @@ +# E2E Test Migration Log + +**Epic:** [CNV-87983](https://redhat.atlassian.net/browse/CNV-87983) +**Started:** 2026-06-02 + +## Progress + +### 2026-06-02 — Planning complete + +- Analyzed kubevirt-ui `release-4.21` (Cypress source) and `main` (Playwright reference) +- Cypress tests removed from kubevirt-ui `main` — Playwright is the only remaining version +- The `release-4.21` Cypress tests are the primary copy source (did not differ much from main) +- Classified tests by plugin ownership: + - **networking-console-plugin**: NADs, UDNs, NetworkPolicies, Services, Routes, Ingresses (~14 tests) + - **nmstate-console-plugin**: NNCPs, NNS, Physical networks, VM networks + - **kubevirt-plugin**: VM-dependent tests (stay in kubevirt-ui with API-based setup) +- Plan saved to `ui/PLAN.md` + +### 2026-06-02 — Cypress infrastructure and specs created + +Created support structure: +- `support/selectors.ts` — `cy.byTestID()`, `cy.byButtonText()`, `cy.checkTitle()`, `cy.clickNavLink()`, etc. +- `support/commands.ts` — `cy.deleteResource()`, `cy.beforeSpec()`, `cy.switchProject()`, `cy.setupUdnNamespace()` +- `support/nav.ts` — `cy.visitNAD()`, `cy.visitUDN()`, `cy.visitService()` +- `support/index.ts` — updated to import all new support files + +Created views: +- `views/nad.ts` — `createNAD()`, `deleteNAD()` +- `views/udn.ts` — `createUDN()`, `deleteUDN()` +- `views/actions.ts` — `checkActionMenu()`, `getRow()` +- `views/selector-common.ts` — shared selectors + +Created utils: +- `utils/const/index.ts` — `TEST_NS`, `UDN_NS`, `K8S_KIND`, `adminOnlyDescribe` +- `utils/const/nad.ts` — `NAD_BRIDGE`, `NAD_OVN`, `NAD_LOCALNET` +- `utils/const/scale.ts` — `MINUTE`, `SECOND` +- `utils/types/nad.ts` — `NadData` interface + +Created specs (8 files, ~14 test cases): +- `tests/nad-bridge.cy.ts` — create bridge NAD (CNV-3256) +- `tests/nad-localnet.cy.ts` — create + delete localnet NAD (CNV-3256, CNV-4288) +- `tests/nad-ovn.cy.ts` — create L2 overlay NAD (CNV-3256) +- `tests/udn.cy.ts` — create UDN (CNV-11867), create CUDN (CNV-11871), delete CUDN (CNV-11874) +- `tests/net-policies.cy.ts` — visit page, create NetworkPolicy with form +- `tests/services.cy.ts` — visit page, create Service with YAML +- `tests/routes.cy.ts` — visit page, create Route with form +- `tests/ingresses.cy.ts` — visit page, create Ingress with YAML + +Removed: `tests/example-page.cy.ts` (template placeholder) + +### Next steps + +- [ ] Set up GitHub Actions hot-cluster CI +- [ ] Verify Prow integration works +- [ ] Update Jira epic CNV-87983 diff --git a/ui/NMSTATE_MIGRATION_GUIDE.md b/ui/NMSTATE_MIGRATION_GUIDE.md new file mode 100644 index 00000000..1d2e351e --- /dev/null +++ b/ui/NMSTATE_MIGRATION_GUIDE.md @@ -0,0 +1,96 @@ +# nmstate-console-plugin E2E Test Migration Guide + +## Context + +This guide describes how to migrate nmstate UI E2E tests from `kubevirt-ui` (internal GitLab) to `openshift/nmstate-console-plugin` (GitHub), following the same proven process used for networking-console-plugin. + +**Jira Epic:** [CNV-87983](https://redhat.atlassian.net/browse/CNV-87983) + +## Source Files + +- **Primary source:** `kubevirt-ui` branch `release-4.21` at `cypress/tests/tier2/networking/nnc.cy.ts` (174 lines) — already Cypress +- **Reference:** `kubevirt-ui` branch `main` at `playwright/tests/tier2/networking/nnc-p.spec.ts` (432 lines) — Playwright, more complete +- The Cypress tests were removed from `main`; only Playwright remains there + +## Tests to Migrate (nmstate-console-plugin owned) + +From `nnc-p.spec.ts` / `nnc.cy.ts`: +- NNC topology view (CNV-11524) +- Bridge on NNS (CNV-10366) +- Create NNCP from form (CNV-11890) +- Create NNCP with YAML (CNV-11891) +- NNCP nodes summary (CNV-11892) +- NNCP page controls (CNV-11893) +- Edit NNCP modal (CNV-11894) +- Define OVS SLB for NNCP (CNV-13742) +- Delete NNCP (CNV-11895) +- Create Physical network (CNV-15220) +- Create Virtual machine network for project (CNV-13432) +- Create Virtual machine network for label (CNV-13433) +- Edit projects mapping (CNV-13434) +- Delete Virtual machine network (CNV-13436) + +## Architecture (proven pattern) + +``` +nmstate-console-plugin/ + .env # Cluster credentials (gitignored) + .env.example # Template + cleanup.sh # Delete test resources (sourced by test-cypress.sh) + setup.sh # Create test namespaces (sourced by test-cypress.sh) + test-cypress.sh # Main runner: cleanup -> setup -> cypress run -> report -> cleanup on success + research-flakiness.sh # 10x stability testing + cypress/ + cypress.config.js # testIsolation: false, pageLoadTimeout: 120000 + plugins/index.ts # dotenv loader + env mapping + reporter-config.json # mochawesome HTML + JUnit + support/ + index.ts # Imports all support files + HIDE_XHR + uncaught exception handler + login.ts # cy.login() with cy.origin() for OAuth cross-origin + IDP handling + selectors.ts # cy.byTestID(), cy.byButtonText(), cy.checkTitle(), cy.clickNavLink() + commands.ts # cy.deleteResource(), cy.switchProject() + nav.ts # cy.visitNNCP(), cy.visitNNS(), etc. (URL-based navigation) + tests/ + all.cy.ts # Ordered imports: login -> visit-pages -> specs + setup/login.cy.ts # Login test (runs first) + setup/visit-pages.cy.ts # Navigate all pages via sidebar clicks + nmstate/*.cy.ts # Actual test specs + utils/const/base.ts # TEST_NS, adminOnlyDescribe, MINUTE, SECOND + views/*.ts # View helpers (createNNCP, deleteNNCP, etc.) +``` + +## Key Lessons Learned + +1. **Use URL-based navigation** (`cy.visit('/k8s/ns/...')`) not sidebar clicks for test reliability. Sidebar clicks are only for the `visit-pages.cy.ts` smoke test. + +2. **Cleanup via shell script, not Cypress `before()` hooks.** The `cleanup.sh` runs via `oc` before Cypress starts. In-test `cy.exec('oc delete...')` failed silently when `oc` session expired. + +3. **Login requires `cy.origin()`** for Cypress 15+ because OAuth is on a different subdomain (`oauth-openshift.*` vs `console-openshift-console.*`). Handle both direct login and IDP selection page. + +4. **`testIsolation: false`** is required for ordered test execution via `all.cy.ts` imports. Login state persists across tests. + +5. **No anti-flakiness workarounds needed** for form interactions. Simple `cy.get(selector).clear().type(value)` works when cluster state is clean. Flakiness was 100% caused by stale resources from prior failed runs. + +6. **`pageLoadTimeout: 120000`** — clusters can be slow; 60s is not enough. + +7. **The `dotenv` package** loads `.env` from the repo root in `plugins/index.ts`. Cypress 15 also natively reads `.env` but the plugin approach gives more control. + +8. **Tests run in order** via `all.cy.ts` importing specs sequentially. This allows tests to depend on prior state. + +9. **Always verify `oc` is logged into the correct cluster** before running. The cleanup/setup use `oc` and will fail silently if the session expired. + +10. **The `research-flakiness.sh` script** runs 10 iterations and captures failures with screenshots/video/logs — useful for proving stability. + +## Steps to Implement + +1. Create `cypress/` folder in nmstate-console-plugin with the same support structure (copy from networking-console-plugin, adjust nav commands) +2. Copy `cleanup.sh`, `setup.sh`, `test-cypress.sh`, `.env.example` from networking-console-plugin +3. Adapt `cleanup.sh` to delete nmstate resources (NNCPs, physical networks, VM networks) +4. Create view helpers for nmstate pages (similar to `views/nad.ts`) +5. Translate specs from `kubevirt-ui/cypress/tests/tier2/networking/nnc.cy.ts` (release-4.21) +6. Cross-reference with `kubevirt-ui/playwright/tests/tier2/networking/nnc-p.spec.ts` (main) for any newer logic +7. Run `research-flakiness.sh` to verify stability + +## Reference Implementation + +The full working implementation is at: `openshift/networking-console-plugin` branch `qe-ui` (fork: `lkladnit/networking-console-plugin`) diff --git a/ui/PLAN.md b/ui/PLAN.md new file mode 100644 index 00000000..2bd4e3c1 --- /dev/null +++ b/ui/PLAN.md @@ -0,0 +1,242 @@ +# Migrate Networking E2E Tests to networking-console-plugin + +**Jira Epic:** [CNV-87983](https://redhat.atlassian.net/browse/CNV-87983) — "Move network & nmstate E2E tests from kubevirt-ui to upstream plugin repos" + +## Source Analysis + +Two branches of kubevirt-ui are relevant: + +- **`release-4.21`** — contains the Cypress tests (`cypress/tests/tier2/networking/`). These are the **primary source to copy** since they are already Cypress and did not differ much from `main`. +- **`main`** — Cypress tests removed; only Playwright versions remain (`playwright/tests/tier2/networking/`). Use as **reference for any newer test logic** added after the Cypress versions were dropped. + +| Cypress file (release-4.21) | Lines | Plugin owner | +|---|---|---| +| `services.cy.ts` | 32 | networking-console-plugin | +| `routes.cy.ts` | 40 | networking-console-plugin | +| `ingresses.cy.ts` | 32 | networking-console-plugin | +| `nad-bridge.cy.ts` | 139 | networking + kubevirt (VM parts) | +| `nad-localnet.cy.ts` | 63 | networking + kubevirt (VM parts) | +| `nad-ovn.cy.ts` | 67 | networking + kubevirt (VM parts) | +| `net-policies.cy.ts` | 98 | networking-console-plugin | +| `udn.cy.ts` | 192 | networking + kubevirt (VM parts) | + +| Playwright file (main) | Lines | Plugin owner | +|---|---|---| +| `net-nad.spec.ts` | 727 | networking + kubevirt | +| `s-r-i.spec.ts` | 62 | networking-console-plugin | +| `nnc-p.spec.ts` | 432 | nmstate-console-plugin | +| `hotplug.spec.ts` | 31 | kubevirt-plugin | + +## Tests to Migrate (networking-console-plugin owned) + +**From `net-nad.spec.ts` — UDN section:** +- create UDN-enabled namespace (via shell/oc, not UI) +- ID(CNV-11867) create UDN +- ID(CNV-11871) create CUDN +- ID(CNV-11874) delete CUDN + +**From `net-nad.spec.ts` — NAD section:** +- ID(CNV-3256) create Linux bridge NAD with MAC Spoof checked +- ID(CNV-3256) create secondary localnet NAD +- ID(CNV-4288) delete secondary localnet NAD +- ID(CNV-3256) create L2 overlay NAD + +**From `net-nad.spec.ts` — NetworkPolicy section:** +- visit NetworkPolicies page +- create NetworkPolicy with form +- create MultiNetworkPolicy with form (currently `test.skip`) + +**From `s-r-i.spec.ts`:** +- Create Service (YAML) +- Create Route (form) +- Create Ingress (YAML) + +**Stays in kubevirt-ui** (VM-dependent — needs API-based setup after migration): +- VM creation tests (CNV-11869, CNV-11868, CNV-11873, CNV-11872) — create VM with UDN/CUDN +- VM + NAD tests (create VMs with bridge/localnet/OVN NAD, verify IP) +- NAD hotplug swap (CNV-15953) +- `hotplug.spec.ts` (all stubs) + +These tests currently rely on preceding tests in the same file to create UDN/CUDN/NAD resources. After migration, those resources must be created via API (oc/kubectl) as test setup in kubevirt-ui. + +**Goes to nmstate-console-plugin:** +- `nnc-p.spec.ts` entirely (NNCP, NNS, Physical networks, VM networks) + +## Architecture + +```mermaid +graph TD + subgraph ciSystems [CI Systems] + ghActions["GitHub Actions (hot cluster, ~5 min)"] + prow["Prow (ephemeral cluster, ~30+ min)"] + end + + subgraph repo [networking-console-plugin] + subgraph integrationTests [cypress/] + cypressConfig[cypress.config.js] + subgraph support [support/] + login[login.ts - existing] + commands[commands.ts - NEW] + nav[nav.ts - NEW] + selectors[selectors.ts - NEW] + end + subgraph views [views/] + nadView[nad.ts - NEW] + udnView[udn.ts - NEW] + netPolView[net-policies.ts - NEW] + actionsView[actions.ts - NEW] + selectorCommon[selector-common.ts - NEW] + end + subgraph tests [tests/] + nadSpec[nad.cy.ts - NEW] + udnSpec[udn.cy.ts - NEW] + netPolSpec[net-policies.cy.ts - NEW] + sriSpec[services-routes-ingresses.cy.ts - NEW] + end + end + prowScript["test-prow-e2e.sh (existing)"] + ghWorkflow[".github/workflows/e2e.yml (NEW)"] + end + + ghActions --> ghWorkflow + prow --> prowScript + ghWorkflow --> cypressConfig + prowScript --> cypressConfig +``` + +## Migration Strategy + +The `release-4.21` Cypress tests are the primary source — copy them and adapt (fix imports, strip VM tests). Cross-reference with `main` Playwright specs only to check for newer test logic that may have been added after the Cypress versions were dropped. + +### What to copy from kubevirt-ui `release-4.21` + +| Source | Copy to networking-console-plugin | +|---|---| +| `cypress/views/nad.ts` | `cypress/views/nad.ts` | +| `cypress/views/udn.ts` | `cypress/views/udn.ts` | +| `cypress/views/actions.ts` (partial) | `cypress/views/actions.ts` | +| `cypress/views/selector-common.ts` (partial) | `cypress/views/selector-common.ts` | +| `cypress/views/selector-template.ts` (partial) | `cypress/views/selector-common.ts` | +| `cypress/support/nav.ts` (networking parts) | `cypress/support/nav.ts` | +| `cypress/support/selectors.ts` | `cypress/support/selectors.ts` | +| `cypress/support/commands.ts` (partial) | `cypress/support/commands.ts` | +| `cypress/utils/const/nad.ts` | `cypress/utils/const/nad.ts` | +| `cypress/utils/const/index.ts` (partial) | `cypress/utils/const/index.ts` | +| `cypress/utils/types/nad.ts` | `cypress/utils/types/nad.ts` | + +### What needs adaptation + +- **Remove VM-dependent code**: all `vm.*` calls, `cy.deleteVM()`, `VirtualMachineData` imports, VM status assertions +- **Remove kubevirt-specific imports**: `TEMPLATE`, `vm-flow`, `tab`, `vm` view modules +- **Remove kubevirt-perspective switching**: `cy.beforeSpec()` switches to Virtualization perspective — networking tests should stay in Administrator perspective +- **Fix relative import paths**: `../../../views/` -> `../views/` (flatter structure) +- **Keep only networking custom commands**: `cy.visitNAD()`, `cy.visitUDN()`, `cy.switchProject()`, `cy.deleteResource()` + +## Implementation Steps + +### 1. Cypress support infrastructure (`cypress/support/`) + +- **`support/selectors.ts`** — `cy.byTestID()`, `cy.byButtonText()`, `cy.checkTitle()`, `cy.checkSubTitle()`, `cy.switchProject()`, `cy.clickNavLink()`, `cy.clickBtn()` +- **`support/commands.ts`** — `cy.deleteResource(kind, name, ns)`, `cy.beforeSpec()` +- **`support/nav.ts`** — `cy.visitNAD()`, `cy.visitUDN()`, `cy.visitService()` + +### 2. Views and constants + +- **`views/nad.ts`** — `createNAD(nad: NadData)`, `deleteNAD(name: string)` + selectors +- **`views/udn.ts`** — `createUDN(project, subnet)`, `createClusterUDN(name, subnet, nsSelector)`, `deleteClusterUDN(name)` +- **`views/net-policies.ts`** — `denyTraffic()`, form radio/name fill helpers +- **`views/actions.ts`** — `checkActionMenu(kind)`, `getRow(name, within)` +- **`views/selector-common.ts`** — `row`, `brCrumbItem`, `itemFilter`, `createBtn`, `confirmBtn` +- **`utils/const/index.ts`** — `TEST_NS`, `UDN_NS`, `adminOnlyDescribe`, test names +- **`utils/const/nad.ts`** — `NAD_BRIDGE`, `NAD_OVN`, `NAD_LOCALNET` data objects +- **`utils/types/nad.ts`** — `NadData` interface + +### 3. Copy and adapt spec files + +| New file | Copies from (release-4.21) | Adaptations | +|---|---|---| +| `tests/udn.cy.ts` | `cypress/tests/tier2/networking/udn.cy.ts` | Remove VM creation tests + Passt section; keep create UDN, create CUDN, delete CUDN. UDN-enabled namespace created via shell (`oc`/`kubectl`) in `before()` hook, not via UI | +| `tests/nad-bridge.cy.ts` | `cypress/tests/tier2/networking/nad-bridge.cy.ts` | Remove VM creation/IP verification tests; keep `createNAD(NAD_BRIDGE)` | +| `tests/nad-localnet.cy.ts` | `cypress/tests/tier2/networking/nad-localnet.cy.ts` | Remove VM test; keep create + delete NAD | +| `tests/nad-ovn.cy.ts` | `cypress/tests/tier2/networking/nad-ovn.cy.ts` | Remove VM tests; keep `createNAD(NAD_OVN)` | +| `tests/net-policies.cy.ts` | `cypress/tests/tier2/networking/net-policies.cy.ts` | Keep as-is (MultiNetworkPolicy already xit) | +| `tests/services.cy.ts` | `cypress/tests/tier2/networking/services.cy.ts` | Adapt imports only | +| `tests/routes.cy.ts` | `cypress/tests/tier2/networking/routes.cy.ts` | Adapt imports only | +| `tests/ingresses.cy.ts` | `cypress/tests/tier2/networking/ingresses.cy.ts` | Adapt imports only | + +**Total: ~14 test cases** across 8 files + +### 4. GitHub Actions hot-cluster CI + +Create `.github/workflows/e2e.yml` following kubevirt-plugin PR #3713: +- Self-hosted runner on persistent OpenShift cluster +- Secrets: `CONSOLE_URL`, `KUBEADMIN_PASSWORD` +- Runs `npm run test-cypress-headless` +- Uploads JUnit + screenshots as artifacts +- ~5 min feedback loop + +### 5. Prow CI (existing) + +[`test-prow-e2e.sh`](../test-prow-e2e.sh) already runs `npm run test-cypress-headless`. Once specs are in place, it will run them with no script changes. A Prow job definition may need to be added/updated in `openshift/release`. + +### 6. Update kubevirt-ui (post-migration) + +The VM-dependent tests remaining in kubevirt-ui will break because they relied on UDN/CUDN/NAD creation from preceding tests in the same file. These need API-based setup: +- `oc apply -f` or `cy.exec('oc create ...')` to create UDN/CUDN/NAD resources before VM tests run +- This is tracked as part of the kubevirt-ui side of CNV-87983 + +### 7. Update Jira CNV-87983 + +- Update epic description with PR links +- Progress subtask CNV-87989 ("automated tests") +- Document which tests were migrated, which stay (with API setup), and which go to nmstate-console-plugin + +## File Structure (final) + +``` +cypress/ + cypress.config.js + tsconfig.json + plugins/ + index.ts + support/ + index.ts + login.ts + commands.ts + nav.ts + selectors.ts + views/ + nad.ts + udn.ts + actions.ts + selector-common.ts + utils/ + types/ + nad.ts + const/ + index.ts + nad.ts + scale.ts + tests/ + nad-bridge.cy.ts + nad-localnet.cy.ts + nad-ovn.cy.ts + udn.cy.ts + net-policies.cy.ts + services.cy.ts + routes.cy.ts + ingresses.cy.ts +.github/ + workflows/ + e2e.yml (hot-cluster CI) +test-prow-e2e.sh (updated screenshots path) +``` + +## Key Design Decisions + +- **Copy from Cypress (release-4.21)** — the primary source; tests did not differ much between release-4.21 and main +- **Cross-reference Playwright (main)** — check for any newer logic added after Cypress was dropped +- **Strip VM-dependent tests, don't delete them** — they stay in kubevirt-ui and will need API-based setup (create UDN/CUDN/NAD via `oc apply`) as a precondition instead of relying on prior UI tests +- **adminOnlyDescribe** — NAD/UDN tests require admin privileges; guard with `Cypress.expose('NON_PRIV')` check +- **beforeSpec without Virtualization perspective** — kubevirt-ui's `cy.beforeSpec()` switches to Virtualization perspective; our version should remain in Administrator perspective since networking resources are accessed from there +- **UDN namespace via shell** — UDN-enabled namespace creation uses `cy.exec('oc ...')` (shell/API), matching the kubevirt-ui approach where it's done in global setup via `setupTestNamespace(namespace, { 'k8s.ovn.org/primary-user-defined-network': '' })`. In Cypress this translates to `cy.exec('oc create namespace ...')` + label application From fe2c1f46b06a528d61773124d7a3874cc70248aa Mon Sep 17 00:00:00 2001 From: Leon Kladnitsky Date: Mon, 15 Jun 2026 09:01:47 +0300 Subject: [PATCH 2/4] fix: point test-e2e scripts and CI workflow to ui-tests-cy/ directory The Cypress tests live in ui-tests-cy/, not cypress/. The npm scripts and GH Actions artifact path referenced a non-existent directory. Co-authored-by: Cursor --- .github/workflows/e2e.yml | 2 +- package.json | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index b9c2d6e1..10daba96 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -43,5 +43,5 @@ jobs: uses: actions/upload-artifact@v4 with: name: e2e-results - path: cypress/gui-test-screenshots/ + path: ui-tests-cy/gui-test-screenshots/ retention-days: 7 diff --git a/package.json b/package.json index ef189fdc..811d0242 100644 --- a/package.json +++ b/package.json @@ -17,10 +17,10 @@ "lint-fix": "eslint ./src ./integration-tests ./cypress --fix", "test-cypress": "cd integration-tests && cypress open --env openshift=true", "test-cypress-headless": "cd integration-tests && node --max-old-space-size=4096 ../node_modules/.bin/cypress run --env openshift=true --browser ${BRIDGE_E2E_BROWSER_NAME:=electron}", - "test-e2e": "cd cypress && node --max-old-space-size=4096 ../node_modules/.bin/cypress run --env openshift=true --browser ${BRIDGE_E2E_BROWSER_NAME:=electron}", - "test-e2e-open": "cd cypress && cypress open --env openshift=true", - "cypress-merge": "mochawesome-merge ./cypress/gui-test-screenshots/cypress_report*.json > ./cypress/gui-test-screenshots/cypress.json", - "cypress-generate": "marge -o ./cypress/gui-test-screenshots/ -f cypress-report -t 'Networking Console Plugin Test Results' -p 'Networking Plugin Test Results' --showPassed false --assetsDir ./cypress/gui-test-screenshots/assets ./cypress/gui-test-screenshots/cypress.json", + "test-e2e": "cd ui-tests-cy && node --max-old-space-size=4096 ../node_modules/.bin/cypress run --env openshift=true --browser ${BRIDGE_E2E_BROWSER_NAME:=electron}", + "test-e2e-open": "cd ui-tests-cy && cypress open --env openshift=true", + "cypress-merge": "mochawesome-merge ./ui-tests-cy/gui-test-screenshots/cypress_report*.json > ./ui-tests-cy/gui-test-screenshots/cypress.json", + "cypress-generate": "marge -o ./ui-tests-cy/gui-test-screenshots/ -f cypress-report -t 'Networking Console Plugin Test Results' -p 'Networking Plugin Test Results' --showPassed false --assetsDir ./ui-tests-cy/gui-test-screenshots/assets ./ui-tests-cy/gui-test-screenshots/cypress.json", "cypress-postreport": "npm run cypress-merge && npm run cypress-generate", "memsource-upload": "./i18n-scripts/memsource-upload.sh", "memsource-download": "./i18n-scripts/memsource-download.sh", From 66951d83160c63021087c6d0b3aa0b6924194720 Mon Sep 17 00:00:00 2001 From: Leon Kladnitsky Date: Mon, 15 Jun 2026 11:10:11 +0300 Subject: [PATCH 3/4] fix: bump node to 20 for Cypress 15 tsx compatibility Cypress 15.x requires --import for tsx which is only supported on Node.js >= 20. The workflow was using Node 18. Co-authored-by: Cursor --- .github/workflows/e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 10daba96..418e3455 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -29,7 +29,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 20 cache: npm - name: Install dependencies From 928780b7d7cc599d41b0ed1820022d63e386e2a5 Mon Sep 17 00:00:00 2001 From: Leon Kladnitsky Date: Mon, 15 Jun 2026 11:33:55 +0300 Subject: [PATCH 4/4] ci: disable auto-trigger, infrastructure not ready yet Switch E2E workflow to manual-only (workflow_dispatch) until a self-hosted runner with cluster access is available. Co-authored-by: Cursor --- .github/workflows/e2e.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 418e3455..4c92121b 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -1,9 +1,12 @@ name: E2E Tests on: - pull_request: - branches: [main, release-*] workflow_dispatch: + inputs: + reason: + description: 'Reason for manual run' + required: false + type: string concurrency: group: e2e-${{ github.head_ref || github.ref_name }}