From 60edacfcd76b85a3b53bef1a08c9ff5ff5284c8b Mon Sep 17 00:00:00 2001 From: "Per G. da Silva" Date: Fri, 3 Jul 2026 10:28:14 +0200 Subject: [PATCH 1/2] Fix flaky e2e-gcp-console secrets tests and SecretData React bug The e2e-gcp-console CI job has had a 0% pass rate since Feb 2026, with crud/secrets test timeouts as the dominant recent failure mode. - Fix React anti-pattern in SecretData: remove setState call from inside useMemo and compute hasRevealableContent as a derived value. The previous code had a stale closure bug (hasRevealableContent not in the useMemo dependency array) and violated React's expectation that useMemo callbacks are pure. - Increase Cypress defaultCommandTimeout from 30s to 40s and retries from 1 to 2 for CI headless runs. The 30s timeout is insufficient for the CI environment where fresh GCP clusters are provisioned. - Add explicit wait for secret-data element in detailsPageIsLoaded helper to ensure the K8s API response has been received and the SecretData component has rendered before tests interact with it. - Use detailsPageIsLoaded helper consistently in key-value.cy.ts instead of duplicating the loading checks inline. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../cypress.config.js | 4 +- .../tests/crud/secrets/key-value.cy.ts | 16 ++---- .../integration-tests-cypress/views/secret.ts | 1 + .../components/configmap-and-secret-data.tsx | 54 ++++++++++--------- 4 files changed, 35 insertions(+), 40 deletions(-) diff --git a/frontend/packages/integration-tests-cypress/cypress.config.js b/frontend/packages/integration-tests-cypress/cypress.config.js index 073f349b502..e77aacf36ff 100644 --- a/frontend/packages/integration-tests-cypress/cypress.config.js +++ b/frontend/packages/integration-tests-cypress/cypress.config.js @@ -16,9 +16,9 @@ module.exports = defineConfig({ configFile: 'reporter-config.json', }, fixturesFolder: 'fixtures', - defaultCommandTimeout: 30000, + defaultCommandTimeout: 40000, retries: { - runMode: 1, + runMode: 2, openMode: 0, }, e2e: { diff --git a/frontend/packages/integration-tests-cypress/tests/crud/secrets/key-value.cy.ts b/frontend/packages/integration-tests-cypress/tests/crud/secrets/key-value.cy.ts index 864323bab9c..d9b11d94430 100644 --- a/frontend/packages/integration-tests-cypress/tests/crud/secrets/key-value.cy.ts +++ b/frontend/packages/integration-tests-cypress/tests/crud/secrets/key-value.cy.ts @@ -63,9 +63,7 @@ describe('Create key/value secrets', () => { cy.byLegacyTestID('file-input-textarea').should('not.exist'); cy.byTestID('alert-info').should('exist'); secrets.save(); - cy.byTestID('loading-indicator').should('not.exist'); - detailsPage.isLoaded(); - detailsPage.titleShouldContain(binarySecretName); + secrets.detailsPageIsLoaded(binarySecretName); cy.exec( `oc get secret -n ${testName} ${binarySecretName} --template '{{.data.${secretKey}}}' | base64 -d`, { @@ -78,9 +76,7 @@ describe('Create key/value secrets', () => { }); modifySecretForm(modifiedSecretKey); secrets.save(); - cy.byTestID('loading-indicator').should('not.exist'); - detailsPage.isLoaded(); - detailsPage.titleShouldContain(binarySecretName); + secrets.detailsPageIsLoaded(binarySecretName); cy.exec( `oc get secret -n ${testName} ${binarySecretName} --template '{{.data.${modifiedSecretKey}}}' | base64 -d`, { @@ -99,9 +95,7 @@ describe('Create key/value secrets', () => { cy.byLegacyTestID('file-input-textarea').should('contain.text', asciiSecret); cy.byTestID('alert-info').should('not.exist'); secrets.save(); - cy.byTestID('loading-indicator').should('not.exist'); - detailsPage.isLoaded(); - detailsPage.titleShouldContain(asciiSecretName); + secrets.detailsPageIsLoaded(asciiSecretName); cy.exec( `oc get secret -n ${testName} ${asciiSecretName} --template '{{.data.${secretKey}}}' | base64 -d`, { @@ -119,9 +113,7 @@ describe('Create key/value secrets', () => { cy.byLegacyTestID('file-input-textarea').should('contain.text', unicodeSecret); cy.byTestID('alert-info').should('not.exist'); secrets.save(); - cy.byTestID('loading-indicator').should('not.exist'); - detailsPage.isLoaded(); - detailsPage.titleShouldContain(unicodeSecretName); + secrets.detailsPageIsLoaded(unicodeSecretName); cy.exec( `oc get secret -n ${testName} ${unicodeSecretName} --template '{{.data.${secretKey}}}' | base64 -d`, { diff --git a/frontend/packages/integration-tests-cypress/views/secret.ts b/frontend/packages/integration-tests-cypress/views/secret.ts index 5e863f58216..fca8bb52513 100644 --- a/frontend/packages/integration-tests-cypress/views/secret.ts +++ b/frontend/packages/integration-tests-cypress/views/secret.ts @@ -56,6 +56,7 @@ export const secrets = { cy.byTestID('loading-indicator').should('not.exist'); detailsPage.isLoaded(); detailsPage.titleShouldContain(secretName); + cy.byTestID('secret-data').should('exist'); }, encode: (username, password) => Base64.encode(`${username}:${password}`), enterSecretName: (secretName: string) => cy.byTestID('secret-name').type(secretName), diff --git a/frontend/public/components/configmap-and-secret-data.tsx b/frontend/public/components/configmap-and-secret-data.tsx index cd534ffa3f4..dc935c6f29a 100644 --- a/frontend/public/components/configmap-and-secret-data.tsx +++ b/frontend/public/components/configmap-and-secret-data.tsx @@ -126,34 +126,36 @@ const SecretDataRevealButton: React.FC = ({ reveal, export const SecretData: React.FC = ({ data }) => { const [reveal, setReveal] = React.useState(false); - const [hasRevealableContent, setHasRevealableContent] = React.useState(false); const { t } = useTranslation(); - const dataDescriptionList = React.useMemo(() => { - return data - ? Object.keys(data) - .sort() - .map((k) => { - const isBinary = ITOB.isBinary(k, Buffer.from(data[k], 'base64')); - if (!isBinary && data[k]) { - setHasRevealableContent(hasRevealableContent || !isBinary); - } - return ( - -
- {k} -
-
- {isBinary ? ( - - ) : ( - - )} -
-
- ); - }) - : []; + const { items: dataDescriptionList, hasRevealableContent } = React.useMemo(() => { + if (!data) { + return { items: [], hasRevealableContent: false }; + } + let revealable = false; + const items = Object.keys(data) + .sort() + .map((k) => { + const isBinary = ITOB.isBinary(k, Buffer.from(data[k], 'base64')); + if (!isBinary && data[k]) { + revealable = true; + } + return ( + +
+ {k} +
+
+ {isBinary ? ( + + ) : ( + + )} +
+
+ ); + }); + return { items, hasRevealableContent: revealable }; }, [data, reveal]); return ( From d2d0e33f244a1d05ff445762ae1a8c4485615fae Mon Sep 17 00:00:00 2001 From: "Per G. da Silva" Date: Fri, 3 Jul 2026 10:44:06 +0200 Subject: [PATCH 2/2] Backport guided tour dismissal and test stability fixes from 4.19 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-pick the guided tour dismissal from 4.19 commit 2b60b9e6d4 ("Re-enabled guided tour for admin perspective") and add source.cy.ts stability fixes from 567b06b242. The guided tour modal was re-enabled for admin perspective but the secrets e2e tests never dismissed it. When the modal appears after login, it overlays the entire viewport and blocks all Cypress interactions — causing timeout failures on whichever element the test tries to click next. This is the primary reason e2e-gcp-console passes on 4.19 but fails on 4.18. Changes: - Add guidedTour.close() after cy.login() in all secrets tests, image-pull-secret test, and demo-dynamic-plugin test - Add project visit before navigation in source.cy.ts beforeEach to avoid wrong-project flakes - Add secret cleanup in source.cy.ts afterEach to prevent leftover state between test runs Co-Authored-By: Claude Opus 4.6 (1M context) --- .../tests/app/demo-dynamic-plugin.cy.ts | 2 ++ .../tests/crud/image-pull-secret.cy.ts | 2 ++ .../tests/crud/secrets/add-to-workload.cy.ts | 2 ++ .../tests/crud/secrets/image-pull.cy.ts | 2 ++ .../tests/crud/secrets/key-value.cy.ts | 2 ++ .../tests/crud/secrets/source.cy.ts | 7 +++++++ .../tests/crud/secrets/webhook.cy.ts | 2 ++ 7 files changed, 19 insertions(+) diff --git a/frontend/packages/integration-tests-cypress/tests/app/demo-dynamic-plugin.cy.ts b/frontend/packages/integration-tests-cypress/tests/app/demo-dynamic-plugin.cy.ts index 44f9c9cebba..9b92c4c8223 100644 --- a/frontend/packages/integration-tests-cypress/tests/app/demo-dynamic-plugin.cy.ts +++ b/frontend/packages/integration-tests-cypress/tests/app/demo-dynamic-plugin.cy.ts @@ -2,6 +2,7 @@ import { safeLoadAll } from 'js-yaml'; import { checkErrors } from '../../support'; import { isLocalDevEnvironment } from '../../views/common'; import { detailsPage } from '../../views/details-page'; +import { guidedTour } from '../../views/guided-tour'; import { listPage } from '../../views/list-page'; import { modal } from '../../views/modal'; import { nav } from '../../views/nav'; @@ -83,6 +84,7 @@ if (!Cypress.env('OPENSHIFT_CI') || Cypress.env('PLUGIN_PULL_SPEC')) { describe('Demo dynamic plugin test', () => { before(() => { cy.login(); + guidedTour.close(); cy.createProjectWithCLI(PLUGIN_NAME); cy.readFile(`${PLUGIN_PATH}/oc-manifest.yaml`).then((textManifest) => { const yamlManifest = safeLoadAll(textManifest); diff --git a/frontend/packages/integration-tests-cypress/tests/crud/image-pull-secret.cy.ts b/frontend/packages/integration-tests-cypress/tests/crud/image-pull-secret.cy.ts index 0ab624d0878..df2dcc6bbef 100644 --- a/frontend/packages/integration-tests-cypress/tests/crud/image-pull-secret.cy.ts +++ b/frontend/packages/integration-tests-cypress/tests/crud/image-pull-secret.cy.ts @@ -1,5 +1,6 @@ import { checkErrors, testName } from '../../support'; import { projectDropdown } from '../../views/common'; +import { guidedTour } from '../../views/guided-tour'; import { listPage } from '../../views/list-page'; import { nav } from '../../views/nav'; @@ -51,6 +52,7 @@ describe('Create image pull secret', () => { before(() => { cy.login(); + guidedTour.close(); cy.createProjectWithCLI(testName); }); diff --git a/frontend/packages/integration-tests-cypress/tests/crud/secrets/add-to-workload.cy.ts b/frontend/packages/integration-tests-cypress/tests/crud/secrets/add-to-workload.cy.ts index f75217b4f9e..941d4aa76a1 100644 --- a/frontend/packages/integration-tests-cypress/tests/crud/secrets/add-to-workload.cy.ts +++ b/frontend/packages/integration-tests-cypress/tests/crud/secrets/add-to-workload.cy.ts @@ -1,6 +1,7 @@ import * as _ from 'lodash'; import { DeploymentKind } from '@console/internal/module/k8s'; import { checkErrors, testName } from '../../../support'; +import { guidedTour } from '../../../views/guided-tour'; import { modal } from '../../../views/modal'; import { secrets } from '../../../views/secret'; @@ -43,6 +44,7 @@ const deployment: DeploymentKind = { describe('Add Secret to Workloads', () => { before(() => { cy.login(); + guidedTour.close(); cy.createProjectWithCLI(testName); cy.exec(`echo '${JSON.stringify(deployment)}' | kubectl create -n ${testName} -f -`); cy.exec( diff --git a/frontend/packages/integration-tests-cypress/tests/crud/secrets/image-pull.cy.ts b/frontend/packages/integration-tests-cypress/tests/crud/secrets/image-pull.cy.ts index 8ab55c1c189..f53a19742b7 100644 --- a/frontend/packages/integration-tests-cypress/tests/crud/secrets/image-pull.cy.ts +++ b/frontend/packages/integration-tests-cypress/tests/crud/secrets/image-pull.cy.ts @@ -1,5 +1,6 @@ import { checkErrors, testName } from '../../../support'; import { detailsPage } from '../../../views/details-page'; +import { guidedTour } from '../../../views/guided-tour'; import { secrets } from '../../../views/secret'; const heading = 'Create image pull secret'; @@ -7,6 +8,7 @@ const heading = 'Create image pull secret'; describe('Image pull secrets', () => { before(() => { cy.login(); + guidedTour.close(); cy.createProjectWithCLI(testName); }); diff --git a/frontend/packages/integration-tests-cypress/tests/crud/secrets/key-value.cy.ts b/frontend/packages/integration-tests-cypress/tests/crud/secrets/key-value.cy.ts index d9b11d94430..7bb1f9c4c19 100644 --- a/frontend/packages/integration-tests-cypress/tests/crud/secrets/key-value.cy.ts +++ b/frontend/packages/integration-tests-cypress/tests/crud/secrets/key-value.cy.ts @@ -2,6 +2,7 @@ import 'cypress-file-upload'; import { checkErrors, testName } from '../../../support'; import { detailsPage } from '../../../views/details-page'; +import { guidedTour } from '../../../views/guided-tour'; import { listPage } from '../../../views/list-page'; import { nav } from '../../../views/nav'; import { secrets } from '../../../views/secret'; @@ -33,6 +34,7 @@ describe('Create key/value secrets', () => { before(() => { cy.login(); + guidedTour.close(); cy.createProjectWithCLI(testName); }); diff --git a/frontend/packages/integration-tests-cypress/tests/crud/secrets/source.cy.ts b/frontend/packages/integration-tests-cypress/tests/crud/secrets/source.cy.ts index c8b8b6501c4..328976d8d9b 100644 --- a/frontend/packages/integration-tests-cypress/tests/crud/secrets/source.cy.ts +++ b/frontend/packages/integration-tests-cypress/tests/crud/secrets/source.cy.ts @@ -1,5 +1,6 @@ import { checkErrors, testName } from '../../../support'; import { detailsPage } from '../../../views/details-page'; +import { guidedTour } from '../../../views/guided-tour'; import { secrets } from '../../../views/secret'; describe('Source secrets', () => { @@ -14,15 +15,21 @@ describe('Source secrets', () => { before(() => { cy.login(); + guidedTour.close(); cy.createProjectWithCLI(testName); }); beforeEach(() => { + // ensure the test project is selected to avoid flakes + cy.visit(`/k8s/cluster/projects/${testName}`); cy.visit(`/k8s/ns/${testName}/secrets/`); secrets.clickCreateSecretDropdownButton('source'); }); afterEach(() => { + cy.exec(`oc delete secret -n ${testName} ${basicSourceSecretName} ${sshSourceSecretName}`, { + failOnNonZeroExit: false, + }); checkErrors(); }); diff --git a/frontend/packages/integration-tests-cypress/tests/crud/secrets/webhook.cy.ts b/frontend/packages/integration-tests-cypress/tests/crud/secrets/webhook.cy.ts index 5d136b312e7..66f357b6fe2 100644 --- a/frontend/packages/integration-tests-cypress/tests/crud/secrets/webhook.cy.ts +++ b/frontend/packages/integration-tests-cypress/tests/crud/secrets/webhook.cy.ts @@ -1,5 +1,6 @@ import { checkErrors, testName } from '../../../support'; import { detailsPage } from '../../../views/details-page'; +import { guidedTour } from '../../../views/guided-tour'; import { secrets } from '../../../views/secret'; describe('Webhook secret', () => { @@ -8,6 +9,7 @@ describe('Webhook secret', () => { before(() => { cy.login(); + guidedTour.close(); cy.createProjectWithCLI(testName); });