Skip to content

Commit 0f734d1

Browse files
[main] fix(ui-tests): upgrade Cypress to 15.6.0 for OCP 4.20+ support (#3931)
* fix(ui-tests): upgrade Cypress to 15.6.0 for OCP 4.20+ support - Cypress 13.7.0 → 15.6.0 (Electron 27→37, Chromium 118→138) - Fix PatternFly v6 selectors: OCP 4.19 uses data-test-id, 4.20+ uses data-test - Add chromeWebSecurity: false for OAuth cross-origin flow - Update HTTP proxy patch for Cypress 15 dependency structure - Consolidate reporter config, improve test script argument handling * fix(ui-tests): delete videos for passing tests - only keep videos when tests fail, saves artifact storage * fix(ui-tests): update selectors for OCP 4.19/4.20 PatternFly v6 - external link: .pf-v6-c-clipboard-copy a.pf-m-link - dropdown: .pf-v6-c-menu button (4.20+), with .pf-m-expanded for 4.19 - add oc version logging - add semver unit tests (11 tests, ES module support) --------- Co-authored-by: Chris Suszyński <ksuszyns@redhat.com>
1 parent 04b7edc commit 0f734d1

File tree

14 files changed

+1022
-667
lines changed

14 files changed

+1022
-667
lines changed

test/ui-e2e-tests.sh

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,6 @@ function check_node {
2121
logger.info "NPM version: $(npm --version)"
2222
}
2323

24-
function archive_cypress_artifacts {
25-
mkdir -p "${ARTIFACTS}/ui/screenshots" "${ARTIFACTS}/ui/videos" "${ARTIFACTS}/ui/results"
26-
pushd "$(dirname "${BASH_SOURCE[0]}")/ui/cypress" >/dev/null
27-
ln -sf "${ARTIFACTS}/ui/screenshots" "${ARTIFACTS}/ui/videos" .
28-
popd >/dev/null
29-
pushd "$(dirname "${BASH_SOURCE[0]}")/ui" >/dev/null
30-
ln -sf "${ARTIFACTS}/ui/results" .
31-
popd >/dev/null
32-
}
33-
3424
function enable_dev_perspective() {
3525
local ocpversion
3626
ocpversion="$(oc get clusterversion/version -o jsonpath='{.status.desired.version}')"
@@ -60,25 +50,43 @@ OCP_PASSWORD="${OCP_PASSWORD:-$(echo "$OCP_USERNAME" | sha1sum - | awk '{print $
6050
OCP_LOGIN_PROVIDER="${OCP_LOGIN_PROVIDER:-my_htpasswd_provider}"
6151
CYPRESS_BASE_URL="https://$(oc get route console -n openshift-console -o jsonpath='{.status.ingress[].host}')"
6252

63-
# use dev to run test development UI
53+
# Process arguments
6454
DEFAULT_NPM_TARGET='test'
65-
if [ $# -gt 0 ] && [ "$1" = "--dev" ]; then
66-
DEFAULT_NPM_TARGET='dev'
67-
shift
68-
fi
55+
cypress_args=()
56+
while [[ $# -gt 0 ]]; do
57+
case "$1" in
58+
--dev)
59+
DEFAULT_NPM_TARGET='dev'
60+
shift
61+
;;
62+
--no-retries)
63+
cypress_args+=("--config" "retries=0")
64+
shift
65+
;;
66+
*)
67+
# Pass through any other argument to cypress
68+
cypress_args+=("$1")
69+
shift
70+
;;
71+
esac
72+
done
73+
6974

7075
if [ -n "${BUILD_ID:-}" ]; then
7176
export CYPRESS_NUM_TESTS_KEPT_IN_MEMORY=0
7277
fi
7378
export OCP_VERSION OCP_USERNAME OCP_PASSWORD OCP_LOGIN_PROVIDER CYPRESS_BASE_URL
7479

7580
add_user "$OCP_USERNAME" "$OCP_PASSWORD"
81+
7682
check_node
83+
logger.info "OCP version: $OCP_VERSION"
84+
oc version
7785
enable_dev_perspective
78-
archive_cypress_artifacts
7986
logger.success '🚀 Cluster prepared for testing.'
8087

8188
pushd "$(dirname "${BASH_SOURCE[0]}")/ui" >/dev/null
8289
npm install
8390
npm run install
84-
npm run "${NPM_TARGET:-$DEFAULT_NPM_TARGET}"
91+
npm run unit
92+
npm run "${NPM_TARGET:-$DEFAULT_NPM_TARGET}" -- "${cypress_args[@]}"

test/ui/.gitignore

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,2 @@
11
node_modules
2-
cypress/videos
3-
cypress/screenshots
42
results

test/ui/cypress.config.js

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,60 @@
1-
const {defineConfig} = require('cypress')
1+
import { defineConfig } from 'cypress'
2+
import path from 'path'
3+
import fs from 'fs'
24

3-
module.exports = defineConfig({
5+
// Determine base directory for test results
6+
const artifactsDir = path.join(process.env.ARTIFACTS || 'results', 'ui')
7+
8+
/**
9+
* Safely delete a file, ignoring errors if file doesn't exist
10+
* @param {string} filePath - Path to file to delete
11+
*/
12+
function safeUnlink(filePath) {
13+
try {
14+
fs.unlinkSync(filePath)
15+
} catch (err) {
16+
// Ignore errors (file may not exist)
17+
}
18+
}
19+
20+
export default defineConfig({
421
defaultCommandTimeout: 60_000,
522
reporter: 'cypress-multi-reporters',
623
reporterOptions: {
7-
configFile: 'reporter-config.json',
24+
reporterEnabled: 'spec, mocha-junit-reporter',
25+
mochaJunitReporterReporterOptions: {
26+
mochaFile: path.join(artifactsDir, 'junit-[hash].xml'),
27+
},
828
},
929
retries: {
1030
runMode: 2,
1131
openMode: 0,
1232
},
33+
// Configure artifact directories
34+
screenshotsFolder: path.join(artifactsDir, 'screenshots'),
35+
videosFolder: path.join(artifactsDir, 'videos'),
1336
e2e: {
37+
// Disable web security to allow cross-origin OAuth flow (console → oauth-openshift → console)
38+
chromeWebSecurity: false,
39+
video: true,
1440
setupNodeEvents(on, config) {
41+
// Delete videos for specs without failing or retried tests
42+
// This saves artifact storage space by only keeping videos of failed tests
43+
on('after:spec', (spec, results) => {
44+
if (results && results.video) {
45+
// Check if any test attempt failed
46+
const hasFailures = results.tests.some((test) =>
47+
test.attempts.some((attempt) => attempt.state === 'failed')
48+
)
49+
// Delete video if all tests passed
50+
if (!hasFailures) {
51+
safeUnlink(results.video)
52+
// Also delete compressed video if it exists
53+
safeUnlink(results.video.replace('.mp4', '-compressed.mp4'))
54+
}
55+
}
56+
})
57+
1558
config.env.TEST_NAMESPACE = process.env.TEST_NAMESPACE || 'default'
1659
config.env.OCP_LOGIN_PROVIDER = process.env.OCP_LOGIN_PROVIDER || 'kube:admin'
1760
config.env.OCP_VERSION = process.env.OCP_VERSION || '0.0.0'

test/ui/cypress/code/knative/serving/showcase.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,15 @@ class ShowcaseKservice {
3737
.should('have.attr', 'value')
3838
.and('include', 'showcase')
3939
}
40-
let selector = '.co-external-link--block a'
40+
41+
// OCP 4.20+ uses ExternalLinkWithCopy inside clipboard-copy component
42+
let selector = '.pf-v6-c-clipboard-copy a.pf-m-link'
43+
44+
// OCP 4.19 and earlier use co-external-link--block
45+
if (environment.ocpVersion().satisfies('<4.20')) {
46+
selector = '.co-external-link--block a'
47+
}
48+
4149
return cy.get(selector)
4250
.last()
4351
.scrollIntoView()

test/ui/cypress/code/openshift/openshiftConsole.js

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -63,49 +63,64 @@ class OpenshiftConsole {
6363
const selectors = this.sidebarSelectors()
6464
cy.get(selectors.drawer)
6565
.then(($drawer) => {
66-
const selector = selectors.drawer +
67-
' button[data-test-id=sidebar-close-button]'
66+
const selector = selectors.drawer + selectors.closeButtonSelector
6867
if (selectors.checkIsOpen($drawer)) {
6968
cy.log('Closing sidebar')
7069
cy.get(selector).click()
7170
}
7271
})
7372
}
7473

74+
/**
75+
* Returns CSS selectors for topology sidebar based on OCP version.
76+
*
77+
* Version Timeline:
78+
* - OCP 4.14: PatternFly v4, old drawer, data-test-id
79+
* - OCP 4.15-4.18: PatternFly v5, new drawer, data-test-id
80+
* - OCP 4.19.x: PatternFly v6, new drawer, data-test-id (migration in progress)
81+
* - OCP 4.20+: PatternFly v6, new drawer, data-test (migration complete)
82+
*
83+
* @returns {{checkIsOpen: function(JQuery<HTMLElement>): boolean, drawer: string, closeButtonSelector: string}}
84+
* Object containing sidebar selectors:
85+
* - checkIsOpen: Function to check if drawer is open
86+
* - drawer: CSS selector for the drawer panel element
87+
* - closeButtonSelector: CSS selector for the close button (appended to drawer selector)
88+
*/
7589
sidebarSelectors() {
76-
if (environment.ocpVersion().satisfies('<=4.14')) {
90+
const version = environment.ocpVersion()
91+
92+
// OCP 4.14 and earlier: PatternFly v4
93+
if (version.satisfies('<=4.14')) {
7794
return {
78-
/**
79-
* @param drawer {JQuery<HTMLElement>}
80-
* @returns {boolean}
81-
*/
82-
checkIsOpen: function (drawer) {
83-
return drawer.hasClass('pf-m-expanded')
84-
},
95+
checkIsOpen: (drawer) => drawer.hasClass('pf-m-expanded'),
8596
drawer: '.odc-topology .pf-c-drawer',
97+
closeButtonSelector: ' button[data-test-id=sidebar-close-button]',
8698
}
8799
}
88-
if (environment.ocpVersion().satisfies('<=4.18')) {
100+
101+
// OCP 4.15-4.18: PatternFly v5
102+
if (version.satisfies('<=4.18')) {
89103
return {
90-
/**
91-
* @param drawer {JQuery<HTMLElement>}
92-
* @returns {boolean}
93-
*/
94-
checkIsOpen: function (drawer) {
95-
return drawer.find('.pf-topology-resizable-side-bar').length > 0
96-
},
104+
checkIsOpen: (drawer) => drawer.find('.pf-topology-resizable-side-bar').length > 0,
97105
drawer: '.pf-v5-c-drawer__panel.ocs-sidebar-index',
106+
closeButtonSelector: ' button[data-test-id=sidebar-close-button]',
98107
}
99108
}
109+
110+
// OCP 4.19.x: PatternFly v6 with old data-test-id attribute
111+
if (version.satisfies('<4.20')) {
112+
return {
113+
checkIsOpen: (drawer) => drawer.find('.pf-topology-resizable-side-bar').length > 0,
114+
drawer: '.pf-v6-c-drawer__panel.ocs-sidebar-index',
115+
closeButtonSelector: ' button[data-test-id=sidebar-close-button]',
116+
}
117+
}
118+
119+
// OCP 4.20+: PatternFly v6 with new data-test attribute
100120
return {
101-
/**
102-
* @param drawer {JQuery<HTMLElement>}
103-
* @returns {boolean}
104-
*/
105-
checkIsOpen: function (drawer) {
106-
return drawer.find('.pf-topology-resizable-side-bar').length > 0
107-
},
121+
checkIsOpen: (drawer) => drawer.find('.pf-topology-resizable-side-bar').length > 0,
108122
drawer: '.pf-v6-c-drawer__panel.ocs-sidebar-index',
123+
closeButtonSelector: ' button[data-test=sidebar-close-button]',
109124
}
110125
}
111126
}

test/ui/cypress/code/semverResolver.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@ class SemverResolver {
77

88
satisfies(range) {
99
const r = new semver.Range(range)
10-
const result = r.test(this.version)
11-
console.log(`Version '${this.version}' matches range '${range}': ${result}`)
12-
return result
10+
return r.test(this.version)
1311
}
1412
}
1513

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { describe, it } from 'node:test'
2+
import assert from 'node:assert'
3+
import SemverResolver from './semverResolver.js'
4+
5+
describe('SemverResolver', () => {
6+
describe('satisfies with <= operator', () => {
7+
const testCases = [
8+
// Regular versions
9+
{ version: '4.19.18', range: '<=4.19', expected: true },
10+
{ version: '4.19.0', range: '<=4.19', expected: true },
11+
{ version: '4.20.0', range: '<=4.19', expected: false },
12+
{ version: '4.20.4', range: '<=4.19', expected: false },
13+
{ version: '4.18.0', range: '<=4.19', expected: true },
14+
{ version: '4.14.0', range: '<=4.19', expected: true },
15+
// Pre-release versions
16+
{ version: '4.19.0-rc', range: '<=4.19', expected: true },
17+
{ version: '4.20.0-rc', range: '<=4.19', expected: false },
18+
{ version: '4.19.0-alpha2', range: '<=4.19', expected: true },
19+
{ version: '4.20.0-alpha2', range: '<=4.19', expected: false },
20+
{ version: '4.21.0-rc', range: '<=4.19', expected: false },
21+
]
22+
23+
testCases.forEach(({ version, range, expected }) => {
24+
const verb = expected ? 'satisfies' : 'does NOT satisfy'
25+
it(`${version} ${verb} ${range}`, () => {
26+
assert.strictEqual(
27+
new SemverResolver(version).satisfies(range),
28+
expected
29+
)
30+
})
31+
})
32+
})
33+
})

test/ui/cypress/e2e/serving/multiple-revisions.cy.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,16 @@ describe('OCP UI for Serverless Serving', () => {
4343
cy.get('input[name="trafficSplitting.1.tag"]')
4444
.type('v1')
4545
cy.contains('Select a Revision', {matchCase: false}).click()
46-
let selector = `.pf-v6-c-dropdown.pf-m-expanded .pf-v6-c-menu button`
46+
47+
// PatternFly dropdown selectors vary by OCP version:
48+
// - OCP 4.20+: PatternFly v6 (without .pf-m-expanded)
49+
// - OCP 4.19: PatternFly v6 (requires .pf-m-expanded state class)
50+
// - OCP 4.15-4.18: PatternFly v5
51+
// - OCP ≤4.14: PatternFly v4
52+
let selector = `.pf-v6-c-menu button`
53+
if (environment.ocpVersion().satisfies('<=4.19')) {
54+
selector = `.pf-v6-c-dropdown.pf-m-expanded .pf-v6-c-menu button`
55+
}
4756
if (environment.ocpVersion().satisfies('<=4.18')) {
4857
selector = `ul.pf-v5-c-dropdown__menu button`
4958
}

test/ui/cypress/patches/17288-check-socket-open.patch

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# TODO: Workaround for cypress-io/cypress#17288, remove after fixed on upstream.
2-
--- Cypress/resources/app/node_modules/http-proxy-middleware/node_modules/http-proxy/lib/http-proxy/passes/ws-incoming.js
3-
+++ Cypress/resources/app/node_modules/http-proxy-middleware/node_modules/http-proxy/lib/http-proxy/passes/ws-incoming.js
2+
--- Cypress/resources/app/node_modules/http-proxy/lib/http-proxy/passes/ws-incoming.js
3+
+++ Cypress/resources/app/node_modules/http-proxy/lib/http-proxy/passes/ws-incoming.js
44
@@ -111,7 +111,7 @@ module.exports = {
55
proxyReq.on('error', onOutgoingError);
66
proxyReq.on('response', function (res) {

0 commit comments

Comments
 (0)