Skip to content

[CONSOLE-5237] Migrate OLM Cypress tests to Playwright - #16899

Draft
trgeiger wants to merge 1 commit into
openshift:mainfrom
trgeiger:CONSOLE-5237
Draft

[CONSOLE-5237] Migrate OLM Cypress tests to Playwright#16899
trgeiger wants to merge 1 commit into
openshift:mainfrom
trgeiger:CONSOLE-5237

Conversation

@trgeiger

@trgeiger trgeiger commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Analysis / Root cause:

Solution description:

Screenshots / screen recording:

Test setup:

Test cases:

Browser conformance:

  • Chrome
  • Firefox
  • Safari (or Epiphany on Linux)

Additional info:

Reviewers and assignees:

Summary by CodeRabbit

  • New Features

    • Expanded Software Catalog navigation, filtering, category selection, and empty-state handling.
    • Added OperatorHub source details, package manifests, registry poll interval editing, and default-source controls.
    • Added coverage for operator installation, operand creation, uninstall workflows, descriptors, and PackageManifest tabs.
  • Bug Fixes

    • Improved workload loading reliability and route validation.
    • Strengthened cleanup handling so test teardown continues despite resource deletion errors.
  • Tests

    • Added extensive end-to-end coverage for catalog, operator lifecycle, operand forms, and resource details.

@openshift-merge-bot

Copy link
Copy Markdown
Contributor

Pipeline controller notification
This repo is configured to use the pipeline controller. Second-stage tests will be triggered either automatically or after lgtm label is added, depending on the repository configuration. The pipeline controller will automatically detect which contexts are required and will utilize /test Prow commands to trigger the second stage.

For optional jobs, comment /test ? to see a list of all defined jobs. To trigger manually all jobs from second stage use /pipeline required command.

This repository is configured in: LGTM mode

@openshift-ci openshift-ci Bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Jul 31, 2026
@openshift-ci

openshift-ci Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@openshift-ci openshift-ci Bot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Jul 31, 2026
@openshift-ci

openshift-ci Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

PR needs rebase.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@openshift-ci openshift-ci Bot added the component/core Related to console core functionality label Jul 31, 2026
@openshift-ci

openshift-ci Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: trgeiger
Once this PR has been reviewed and has the lgtm label, please assign spadgett for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-ci openshift-ci Bot added component/olm Related to OLM component/shared Related to console-shared labels Jul 31, 2026
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Adds Kubernetes custom-resource helpers, reusable OLM cleanup workflows, Playwright page objects, broad OLM end-to-end coverage, console test stabilization, and dedicated test selectors.

Changes

OLM E2E coverage

Layer / File(s) Summary
Kubernetes custom-resource access
frontend/e2e/clients/kubernetes-client.ts
Adds user fallback, custom-resource patching, retrieval, and listing methods.
Reusable cleanup workflows
frontend/e2e/test-utils/*
Adds configurable cluster, namespace, operator, and lifecycle cleanup utilities.
Catalog and operator page objects
frontend/e2e/pages/catalog*.ts, frontend/e2e/pages/*operator*.ts, frontend/e2e/pages/operand-page.ts, frontend/e2e/pages/details-page.ts, frontend/e2e/pages/yaml-editor-page.ts
Adds navigation, filtering, installation, operand, uninstall, details, YAML, and status helpers.
OLM end-to-end suites
frontend/e2e/tests/olm/*
Adds coverage for catalog sources, OperatorHub filters, descriptors, PackageManifest tabs, operator installation, operand lifecycle, and uninstall handling.
Console test stabilization
frontend/e2e/tests/console/crud/*
Improves namespace readiness, fixture validity, route matching, annotation deletion, and teardown handling.
E2E selector coverage
frontend/packages/*, frontend/public/components/utils/*
Adds data-test selectors for catalog, operator, modal, resource, detail, and heading elements.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PlaywrightTest
  participant OperatorInstallPage
  participant InstalledOperatorsPage
  participant OperatorDetailsPage
  participant KubernetesClient
  PlaywrightTest->>OperatorInstallPage: install operator
  OperatorInstallPage->>InstalledOperatorsPage: verify installation
  InstalledOperatorsPage-->>PlaywrightTest: return status
  PlaywrightTest->>OperatorDetailsPage: create or delete operand
  OperatorDetailsPage->>KubernetesClient: verify custom-resource state
  PlaywrightTest->>OperatorDetailsPage: uninstall operator
  OperatorDetailsPage-->>PlaywrightTest: return uninstall result
Loading

Possibly related PRs

  • openshift/console#16881: Uses KubernetesClient.patchClusterCustomResource for related Playwright E2E customization and cleanup.

Suggested labels: jira/valid-reference


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 3 warnings)

Check name Status Explanation Resolution
Stable And Deterministic Test Names ❌ Error New Playwright titles interpolate testOperator.name, testOperand.name, and globalNamespace at lines 36/97 and 36/109; these titles are not static and can change with test configuration. Replace interpolated describe/test titles with fixed descriptive strings. Keep operator, operand, and namespace values in test bodies or assertions.
Description check ⚠️ Warning The description contains only the template and does not provide analysis, solution details, test information, browser coverage, or reviewer details. Complete each required section with the migration rationale, implementation details, test setup and cases, browser coverage, and reviewer information.
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Ipv6 And Disconnected Network Test Compatibility ⚠️ Warning The new descriptor Playwright test creates a CSV deployment with unqualified image nginx, which can require pulling from a public registry in disconnected CI. IPv6/disconnected notice: replace nginx with a mirrored/internal image or skip the test in disconnected runs, then run /payload-job periodic-ci-openshift-release-master-nightly-4.22-e2e-metal-ipi-ovn-ipv6.
✅ Passed checks (11 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the migration of OLM Cypress tests to Playwright and includes the required Jira issue prefix.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Test Structure And Quality ✅ Passed The PR contains TypeScript Playwright/Cypress tests, not Ginkgo tests; repository searches found no Ginkgo test files or Ginkgo primitives in the changed area.
Microshift Test Compatibility ✅ Passed The PR adds Playwright TypeScript tests under frontend/e2e, not Ginkgo Go tests; no new It/Describe/Context/When Ginkgo tests require MicroShift API checks.
Single Node Openshift (Sno) Test Compatibility ✅ Passed The PR adds Playwright .spec.ts tests under frontend/e2e; no Ginkgo It/Describe/Context/When tests or SNO-relevant multi-node assumptions apply.
Topology-Aware Scheduling Compatibility ✅ Passed The PR adds E2E helpers, tests, cleanup utilities, and UI selectors; searches found no topology constraints, node selectors, affinities, spread constraints, PDBs, or scheduling manifests.
Ote Binary Stdout Contract ✅ Passed The PR changes frontend TypeScript/TSX only. The repository has no openshift-tests OTE binary or Ginkgo suite, so no applicable process-level JSON stdout violation exists.
No-Weak-Crypto ✅ Passed Scans of all summarized changed paths found no MD5, SHA1, DES, RC4, Blowfish, ECB, crypto APIs, custom crypto, or secret/token comparisons; Math.random only names test namespaces.
Container-Privileges ✅ Passed PR files contain no privileged:true, hostPID, hostNetwork, hostIPC, SYS_ADMIN, allowPrivilegeEscalation:true, or explicit root settings; no manifest-like files are changed.
No-Sensitive-Data-In-Logs ✅ Passed Changed-file review found logs limited to test operator/resource names, namespaces, status text, timestamps, and error messages; no passwords, tokens, API keys, PII, hostnames, or customer data are...
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

🧹 Nitpick comments (28)
frontend/e2e/clients/kubernetes-client.ts (3)

661-685: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Log the swallowed errors.

Both methods return [] for every failure, including RBAC denials and network errors. The callers in frontend/e2e/test-utils/operator-cleanup.ts and frontend/e2e/test-utils/olm-test-cleanup.ts then treat the empty result as "nothing to clean up", so cleanup silently becomes a no-op. The callers already wrap these calls in try/catch, so logging the error keeps the current control flow and makes the failure visible in the test output.

♻️ Proposed refactor
       return (response as any)?.items || [];
-    } catch {
+    } catch (err) {
+      console.log(`listClusterCustomResources(${group}/${version}/${plural}) failed: ${err}`);
       return [];
     }
   }
 
   async listNamespaces(): Promise<unknown[]> {
     try {
       const response = await this.k8sApi.listNamespace();
       return (response?.items || []);
-    } catch {
+    } catch (err) {
+      console.log(`listNamespaces failed: ${err}`);
       return [];
     }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/clients/kubernetes-client.ts` around lines 661 - 685, Update
listClusterCustomResources and listNamespaces to log the caught error before
returning the existing empty-array fallback. Preserve the current return
behavior and use the client’s established logging mechanism so RBAC, network,
and other failures are visible to test callers.

282-288: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace the placeholder fallback value.

getCurrentUser() returns { name: 'idk' } when kubeconfig retrieval fails. A caller cannot distinguish this fake user from a real one. Return undefined instead, and use the typed k8s.User return type for consistency with getCurrentUserToken().

♻️ Proposed refactor
-  getCurrentUser(): any {
+  getCurrentUser(): k8s.User {
     try {
       return this.kubeConfig.getCurrentUser();
     } catch {
-      return { name: 'idk' };
+      return undefined;
     }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/clients/kubernetes-client.ts` around lines 282 - 288, Update
getCurrentUser() to return undefined when kubeConfig.getCurrentUser() throws
instead of the placeholder user object, and change its return type from any to
the typed k8s.User-compatible optional return type used consistently with
getCurrentUserToken().

Source: Learnings


571-586: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use k8s.PatchStrategy.JsonPatch in patchClusterCustomResource.

patchCustomResource already uses the client enum for the JSON Patch content type. Use the same value here to avoid duplicating the literal 'application/json-patch+json'.

♻️ Proposed refactor
       body: patch,
-      contentType: 'application/json-patch+json',
+      contentType: k8s.PatchStrategy.JsonPatch,
     } as any);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/clients/kubernetes-client.ts` around lines 571 - 586, Update
patchClusterCustomResource to use k8s.PatchStrategy.JsonPatch for the
contentType value, matching the existing patchCustomResource implementation and
removing the duplicated literal.

Source: Linters/SAST tools

frontend/e2e/test-utils/cluster-cleanup.ts (2)

154-185: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the operand types configurable and log the swallowed error.

Two points in this block:

  1. The operand types are hardcoded to infinispan.org. The function accepts a configurable targetOperator, so the operand types should also come from ClusterCleanupOptions. Otherwise this "comprehensive" utility only cleans one operator's operands.
  2. The catch on Lines 182-184 discards every error. A missing CRD and an RBAC denial produce the same silent result. Log the error so a failed cleanup is visible in the test output.
♻️ Proposed refactor
 export interface ClusterCleanupOptions {
   dryRun?: boolean;
   targetOperator?: string;
   olderThanMinutes?: number;
+  operandTypes?: { group: string; version: string; plural: string }[];
 }
-      const operandTypes = [
-        { group: 'infinispan.org', version: 'v1', plural: 'infinispans' },
-        { group: 'infinispan.org', version: 'v1', plural: 'backups' },
-      ];
+      const operandTypes = options.operandTypes ?? [
+        { group: 'infinispan.org', version: 'v1', plural: 'infinispans' },
+        { group: 'infinispan.org', version: 'v1', plural: 'backups' },
+      ];
-        } catch (error) {
-          // Ignore - operand type may not exist
-        }
+        } catch (error) {
+          console.log(
+            `  Skipping ${operandType.plural} in ${namespaceName}: ${error.message}`,
+          );
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/test-utils/cluster-cleanup.ts` around lines 154 - 185, Update
the operand cleanup flow around operandTypes to derive the operand group,
version, and plurals from the configurable targetOperator in
ClusterCleanupOptions instead of hardcoding infinispan.org. In the catch block
surrounding listCustomResources and deleteCustomResource, log the caught error
with enough context to identify the operand type and namespace while preserving
the existing cleanup continuation behavior.

59-151: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the repeated resource cleanup block into a helper.

The CSV, Subscription, and InstallPlan blocks are identical except for the plural name and the match predicate. A single helper removes about 70 duplicated lines and gives one place to fix the logging and error handling.

♻️ Proposed refactor
+    const cleanupResourceType = async (
+      plural: string,
+      version: string,
+      matches: (item: any) => boolean,
+    ): Promise<void> => {
+      try {
+        const items = await k8sClient.listCustomResources(
+          'operators.coreos.com',
+          version,
+          namespaceName,
+          plural,
+        );
+        const targets = (items || []).filter(matches);
+        console.log(`Found ${targets.length} ${targetOperator} ${plural} in ${namespaceName}`);
+        for (const item of targets) {
+          console.log(`  ${dryRun ? 'Would delete' : 'Deleting'} ${plural}: ${item.metadata.name}`);
+          if (!dryRun) {
+            await k8sClient.deleteCustomResource(
+              'operators.coreos.com',
+              version,
+              namespaceName,
+              plural,
+              item.metadata.name,
+            );
+          }
+        }
+      } catch (error) {
+        console.log(`  Error checking ${plural} in ${namespaceName}: ${error.message}`);
+      }
+    };

Then call it for each type:

await cleanupResourceType('clusterserviceversions', 'v1alpha1', (csv) =>
  Boolean(csv.metadata.name?.includes(targetOperator)),
);
await cleanupResourceType(
  'subscriptions',
  'v1alpha1',
  (sub) =>
    Boolean(sub.metadata.name?.includes(targetOperator)) ||
    Boolean(sub.spec?.name?.includes(targetOperator)),
);
await cleanupResourceType('installplans', 'v1alpha1', (ip) =>
  (ip.spec?.clusterServiceVersionNames || []).some((n: string) => n.includes(targetOperator)),
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/test-utils/cluster-cleanup.ts` around lines 59 - 151, Extract
the repeated CSV, Subscription, and InstallPlan cleanup logic into a shared
cleanupResourceType helper that accepts the resource plural, API version, and
match predicate, while preserving dry-run behavior, logging, listing, deletion,
and error handling. Replace the three inline try/catch blocks with calls to this
helper using the existing resource-specific predicates and targetOperator
matching.
frontend/e2e/test-utils/operator-cleanup.ts (3)

98-100: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Match predicate differs from the other cleanup modules.

This module matches subscriptions with ===. frontend/e2e/test-utils/olm-test-cleanup.ts (Line 119) and frontend/e2e/test-utils/olm-cleanup.ts (Line 40) match with includes and startsWith. The three modules therefore select different resource sets for the same packageName.

The exact match used here is the safest of the three. Align the other modules to it, or document why each module needs a different matching rule.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/test-utils/operator-cleanup.ts` around lines 98 - 100, Align the
subscription matching predicates in the cleanup functions of olm-test-cleanup.ts
and olm-cleanup.ts with the exact-match behavior used by operator-cleanup.ts,
replacing broader includes/startsWith checks where appropriate. If either module
must retain broader matching, document the specific reason and intended
resource-selection difference.

13-13: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Type the k8sClient parameter as KubernetesClient in all four cleanup modules. Every new cleanup function accepts k8sClient: any. The shared root cause is that no module imports the concrete client type, so the compiler cannot check any method name, argument order, or return type. This matters here because the modules call positional helpers such as deleteCustomResource(group, version, namespace, plural, name); a swapped namespace and plural argument compiles today and silently deletes nothing at runtime. KubernetesClient is a default export from frontend/e2e/clients/kubernetes-client.ts and already declares every method these modules use.

  • frontend/e2e/test-utils/operator-cleanup.ts#L13-L13: add import type KubernetesClient from '../clients/kubernetes-client'; and change both cleanupAllOperatorsByPackageName and cleanupOperatorResources to accept k8sClient: KubernetesClient.
  • frontend/e2e/test-utils/cluster-cleanup.ts#L15-L15: import the same type and change cleanupClusterTestResources to accept k8sClient: KubernetesClient.
  • frontend/e2e/test-utils/olm-cleanup.ts#L17-L20: import the same type and change both cleanupOLMOperatorCompletely and cleanupOperatorWithOLMResources (Line 96) to accept k8sClient: KubernetesClient.
  • frontend/e2e/test-utils/olm-test-cleanup.ts#L15-L15: import the same type and change performOperatorCleanup, performAggressiveOperatorCleanup (Line 47), cleanupTestNamespaces (Line 75), and verifyAndForceCleanup (Line 115) to accept k8sClient: KubernetesClient. Also type the hook fixtures at Lines 149-171 instead of any.

The list methods return unknown[], so each filter callback keeps its (item: any) annotation or gains a narrow local interface.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/test-utils/operator-cleanup.ts` at line 13, Replace the
any-typed Kubernetes client parameters with the default-imported
KubernetesClient type across
frontend/e2e/test-utils/operator-cleanup.ts#L13-L13,
frontend/e2e/test-utils/cluster-cleanup.ts#L15-L15,
frontend/e2e/test-utils/olm-cleanup.ts#L17-L20, and
frontend/e2e/test-utils/olm-test-cleanup.ts#L15-L15: update all named cleanup
functions in those files, and type the olm-test-cleanup hook fixtures at Lines
149-171 instead of any. Preserve (item: any) in filter callbacks or use a narrow
local interface because KubernetesClient list methods return unknown[].

17-52: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Replace the per-namespace subscription list with the cluster-wide method.

This function currently calls listCustomResources(..., nsName, ...) for every namespace returned by listNamespaces(), then silently swallows per-namespace errors. listClusterCustomResources('operators.coreos.com', 'v1alpha1', 'subscriptions') makes one call, and each result includes its namespace, so delete it against that metadata.namespace.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/test-utils/operator-cleanup.ts` around lines 17 - 52, Update the
operator cleanup flow to replace the per-namespace loop and listCustomResources
calls with a single listClusterCustomResources call for subscriptions. Use each
subscription’s metadata.namespace when invoking deleteCustomResource, while
preserving matching by operatorPackageName and deletion of all matching
subscriptions.
frontend/e2e/pages/catalog-page.ts (1)

72-78: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the duplicate search-input getter.

getSearchInput and getSearchInputElement return the same locator. Keep one accessor to avoid two names for one concept.

♻️ Proposed cleanup
   getSearchInput(): Locator {
     return this.searchCatalogInput;
   }
-
-  getSearchInputElement(): Locator {
-    return this.searchCatalogInput;
-  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/pages/catalog-page.ts` around lines 72 - 78, Remove the
duplicate accessor between getSearchInput and getSearchInputElement in the
catalog page object, retaining a single search-input getter and updating any
callers to use the retained method.
frontend/e2e/pages/operator-install-page.ts (1)

24-38: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the shared catalog-to-install-form prologue.

All three install methods repeat the same ten steps: navigate to /catalog/all-namespaces, select the Operator tab, search, verify and click the card, then verify and click the install button. Extract one private helper and call it from each method. This keeps selector and timeout changes in one place.

♻️ Proposed refactor
+  private async openInstallForm(operatorName: string, operatorCardTestID: string): Promise<void> {
+    await this.goTo('/catalog/all-namespaces');
+    await this.catalogPage.clickOperatorTab();
+    await this.catalogPage.searchOperators(operatorName);
+
+    const operatorCard = this.page.getByTestId(operatorCardTestID);
+    await expect(operatorCard).toBeVisible({ timeout: 30_000 });
+    await this.robustClick(operatorCard);
+
+    await expect(this.installButton).toBeVisible();
+    await expect(this.installButton).toHaveAttribute('href');
+    await this.robustClick(this.installButton);
+  }
+
   async installOperatorGlobally(operatorName: string, operatorCardTestID: string): Promise<void> {
-    await this.goTo('/catalog/all-namespaces');
-
-    await this.catalogPage.clickOperatorTab();
-    await this.catalogPage.searchOperators(operatorName);
-
-    // Verify operator exists before clicking
-    const operatorCard = this.page.getByTestId(operatorCardTestID);
-    await expect(operatorCard).toBeVisible({ timeout: 30_000 });
-    await this.robustClick(operatorCard);
-
-    // Wait for install button and verify it has href
-    await expect(this.installButton).toBeVisible();
-    await expect(this.installButton).toHaveAttribute('href');
-    await this.robustClick(this.installButton);
+    await this.openInstallForm(operatorName, operatorCardTestID);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/pages/operator-install-page.ts` around lines 24 - 38, Extract
the repeated catalog-to-install-form flow from installOperatorGlobally and the
other two install methods into a private helper: navigate to
/catalog/all-namespaces, select the Operator tab, search by operator name,
verify and click the operator card, then verify and click the install button.
Update all three methods to call this helper, preserving the existing selectors,
timeout, and robustClick behavior.
frontend/e2e/pages/operator-hub-details-page.ts (1)

78-99: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move the assertions in toggleSourceAndVerify to the spec.

toggleSourceAndVerify performs the toggle flow and also asserts the modal title and both status values. Keep page objects action-only and assert in the spec file, so failures point at the test intent. Expose the toggle steps and let the spec call expect on getSourceStatus.

Based on learnings: real expectations should be asserted in the spec files (e.g., expect(...).toBeVisible()), not in page objects.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/pages/operator-hub-details-page.ts` around lines 78 - 99,
Refactor toggleSourceAndVerify in the operator hub details page object to
perform only the source-toggle actions, removing its modal-title and
source-status assertions and exposing the necessary steps for callers. Move
those expectations into the consuming spec, including both status checks via
getSourceStatus and the modal-title checks, so test intent and failures remain
in the spec.

Source: Learnings

frontend/e2e/pages/operator-details-page.ts (4)

293-299: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove commented-out code.

Lines 294 and 296 contain commented-out selector definitions no longer used. Remove them for clarity.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/pages/operator-details-page.ts` around lines 293 - 299, Remove
the unused commented-out selector declarations from verifyUninstallAlert,
including the alert and modal-title comments, while leaving the dialog
visibility and expected-text assertions unchanged.

101-124: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove commented-out code.

Lines 107-118 contain commented-out navigation logic. Remove it, since it adds no value and clutters the method.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/pages/operator-details-page.ts` around lines 101 - 124, Remove
the commented-out destructuring and navigation blocks from deleteOperand,
including the unused breadcrumb, tab-navigation, and operand-link code, while
preserving the URL assertion and deletion flow.

198-228: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract shared uninstall-modal setup logic.

uninstallOperator and uninstallOperatorWithOperands duplicate the click-page-action, modal-open, title-check, and skeleton-wait sequence. Extract this into a private helper to reduce duplication and the risk of the two methods diverging.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/pages/operator-details-page.ts` around lines 198 - 228, Extract
the shared page-action click, modal-open wait, title assertion, and
loading-skeleton wait from uninstallOperator and uninstallOperatorWithOperands
into a private helper on the page object. Have both methods call this helper,
while preserving their existing submit and delete-all-operands behavior.

51-82: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract shared logic between createOperand and createOperandFromTab.

createOperand and createOperandFromTab duplicate the same create-form logic. createOperand only adds a preceding navigateToOperandTab call. Have createOperand call navigateToOperandTab and then delegate to createOperandFromTab to avoid future divergence.

♻️ Proposed refactor
   async createOperand(testOperand: TestOperandProps, isGlobal: boolean = true): Promise<void> {
-    const { exampleName, createActionID } = testOperand;
-
     await this.navigateToOperandTab(testOperand.name, isGlobal);
-
-    // Verify operand doesn't already exist
-    await expect(this.getOperandLink(exampleName)).not.toBeAttached();
-
-    // Click create button
-    await this.robustClick(this.createItemButton);
-
-    // If specific create action ID is provided, wait for dropdown and click it
-    if (createActionID) {
-      // Wait for the dropdown item to be visible before clicking
-      await expect(this.page.getByTestId(createActionID)).toBeVisible({ timeout: 30_000 });
-      await this.robustClick(this.page.getByTestId(createActionID));
-    }
-
-    // Verify we're on the create form
-    await expect(this.page).toHaveURL(/~new/);
-
-    // Fill in the name
-    await expect(this.nameInput).toBeEnabled();
-    await this.nameInput.clear();
-    await this.nameInput.fill(exampleName);
-
-    // Submit the form
-    await this.clickSubmitButton();
-
-    // Wait for form submission and redirect
-    await expect(this.page).not.toHaveURL(/~new/, { timeout: 60_000 });
+    await this.createOperandFromTab(testOperand);
   }

Also applies to: 139-168

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/pages/operator-details-page.ts` around lines 51 - 82, Refactor
createOperand to only call navigateToOperandTab with the operand name and
isGlobal, then delegate the remaining creation flow to createOperandFromTab
using the same testOperand argument. Move or reuse the shared create-form logic
through createOperandFromTab so both methods cannot diverge.
frontend/e2e/tests/olm/operator-install-global.spec.ts (2)

41-51: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract duplicated cluster-operator cleanup logic.

The beforeEach and afterEach hooks both list operators.coreos.com resources, filter by operatorPackageName substring, and delete each match. Extract this into a shared helper function (for example alongside cleanupDataGridOperatorResources) to avoid the two copies diverging over time.

Also applies to: 91-101

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/tests/olm/operator-install-global.spec.ts` around lines 41 - 51,
Extract the duplicated cluster-operator cleanup flow from the beforeEach and
afterEach hooks into a shared helper near cleanupDataGridOperatorResources. Have
the helper list operators.coreos.com resources, filter names by
operatorPackageName, delete each matching resource, and preserve the existing
error logging; replace both hook implementations with calls to this helper.

80-80: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Replace fixed sleeps with a deterministic wait.

Line 80 uses page.waitForTimeout(5000) and line 88 uses a raw setTimeout promise for 10 seconds to let cleanup propagate. Fixed sleeps make the suite slower than necessary when cleanup finishes early, and flaky when cleanup takes longer than the fixed duration. Poll for the actual absence of the operator/operand resources instead of waiting a fixed duration.

Also applies to: 88-88

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/tests/olm/operator-install-global.spec.ts` at line 80, Replace
the fixed delays in the operator cleanup flow around the waitForTimeout call and
raw setTimeout promise with deterministic polling that repeatedly checks until
the operator and operand resources are absent. Preserve the cleanup sequencing,
but allow the wait to finish immediately when resources disappear and continue
until the configured polling timeout when propagation is slow.
frontend/e2e/tests/olm/catalog-source-details.spec.ts (1)

72-73: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Compute the shared suffix once.

Line 72 and Line 73 call Date.now() separately. The two names diverge when the calls straddle a millisecond boundary. Compute the timestamp once and reuse it.

♻️ Proposed refactor
-    const testNs = `test-catsrc-${Date.now()}`;
-    const catalogSourceName = `test-catsrc-${Date.now()}`;
+    const suffix = Date.now();
+    const testNs = `test-catsrc-${suffix}`;
+    const catalogSourceName = `test-catsrc-${suffix}`;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/tests/olm/catalog-source-details.spec.ts` around lines 72 - 73,
Compute the timestamp once before the testNs and catalogSourceName declarations,
then reuse that shared value in both template strings so the names always have
the same suffix.
frontend/e2e/tests/olm/create-namespace.spec.ts (3)

27-42: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename the shadowing loop variable.

Line 28 declares nsName inside the loop. It shadows the suite-level nsName that Line 17 assigns and that afterEach uses at Line 58 and Line 66. The shadowing is easy to misread during later edits. Rename the loop variable to staleNsName.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/tests/olm/create-namespace.spec.ts` around lines 27 - 42, Rename
the loop-local variable in the test namespace cleanup loop from nsName to
staleNsName, and update all references within that loop, including logging,
cleanupOperatorResources, deleteNamespace, and the error message. Preserve the
suite-level nsName used by afterEach unchanged.

47-48: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Replace the fixed 10 s sleep with a readiness poll.

cleanupAllOperatorsByPackageName already sleeps 5 s internally. This adds 10 s more to every test in the suite. The hook also takes the page fixture only to call waitForTimeout.

Poll the API for the absence of the subscriptions and ClusterServiceVersions instead. Then drop page from the hook signature.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/tests/olm/create-namespace.spec.ts` around lines 47 - 48,
Replace the fixed waitForTimeout call in the cleanup hook with polling against
the API until the targeted subscriptions and ClusterServiceVersions are absent,
reusing the existing cleanupAllOperatorsByPackageName context and allowing the
poll to time out appropriately. Remove the unused page fixture from the hook
signature and its callers while preserving the cleanup behavior.

52-59: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Take k8sClient from the fixture in afterEach.

The hook uses the suite-level k8sClient variable that beforeEach assigns. operator-install-single-namespace.spec.ts destructures the fixture directly in afterEach. Follow that pattern here and delete the suite-level variable. The hook then works even when beforeEach fails early.

♻️ Proposed refactor
-  test.afterEach(async () => {
+  test.afterEach(async ({ k8sClient }) => {
     console.log('=== CREATE NAMESPACE AFTER EACH: Starting cleanup ===');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/tests/olm/create-namespace.spec.ts` around lines 52 - 59, Update
the create-namespace test cleanup hook to destructure k8sClient directly from
the afterEach fixture, matching the pattern in
operator-install-single-namespace.spec.ts. Remove the suite-level k8sClient
variable and its beforeEach assignment, while continuing to pass the fixture
client to cleanupOperatorResources.
frontend/e2e/tests/olm/edit-default-sources.spec.ts (1)

26-36: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add an explicit timeout to the status assertions.

After the modal submits, the console patches the OperatorHub resource and waits for the watch update. The default expect timeout can expire before the status text changes. Set an explicit timeout on both assertions.

♻️ Proposed change
-      await expect(operatorHubPage.getSourceStatus(defaultSourceToBeToggled)).toHaveText('Disabled');
+      await expect(operatorHubPage.getSourceStatus(defaultSourceToBeToggled)).toHaveText(
+        'Disabled',
+        { timeout: 60_000 },
+      );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/tests/olm/edit-default-sources.spec.ts` around lines 26 - 36,
Update both getSourceStatus(defaultSourceToBeToggled) status assertions in the
toggle flow to use an explicit timeout long enough for the OperatorHub watch
update after modal submission, preserving the existing Disabled and Enabled
expectations.
frontend/e2e/pages/installed-operators-page.ts (3)

111-136: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Delete the two waitForFunction blocks. They do not wait for what the comments claim.

The first block waits for !input.disabled. The filter input is never disabled, so the predicate is true on the first evaluation. It does not wait for the 250 ms debounce. The following expect(...).toBeVisible() performs the real wait.

The second block re-checks opacity and visibility. toBeVisible() already covers that, and aria-busy is checked on the row element only.

The comment at Line 111 says the namespace is selected only when it is not openshift-operators, but selectNamespace always runs. Update the comment or the logic.

♻️ Proposed simplification
-    // Select namespace if not openshift-operators
+    // Scope the list to the target namespace
     await this.selectNamespace(namespace);
 
     await this.filterByName(operatorName);
 
-    // Wait for debounce to complete before clicking (filter-toolbar.tsx uses 250ms debounce)
-    await this.page.waitForFunction(() => {
-      const input = document.querySelector('[data-test="name-filter-input"]') as HTMLInputElement;
-      return input && !input.disabled;
-    });
-
     // Wait for the operator row to be visible
     await expect(this.getOperatorRow(operatorName)).toBeVisible({ timeout: 30_000 });
-
-    // Additional wait to ensure the table row is stable and ready for interaction
-    await this.page.waitForFunction(
-      (name) => {
-        const row = document.querySelector(`[data-test="operator-row-${name}"]`);
-        if (!row) return false;
-        // Check that row is fully rendered and stable
-        const style = window.getComputedStyle(row);
-        return style.opacity === '1' && style.visibility === 'visible' && !row.hasAttribute('aria-busy');
-      },
-      operatorName,
-      { timeout: 10_000 }
-    );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/pages/installed-operators-page.ts` around lines 111 - 136,
Remove both waitForFunction blocks surrounding getOperatorRow(operatorName),
along with their misleading comments, because toBeVisible already provides the
necessary wait and visibility check. Also reconcile the namespace comment with
the behavior of selectNamespace: either update the comment to state that it
always runs or conditionally call selectNamespace only when namespace is not
openshift-operators.

63-103: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace the manual polling loop with a web-first assertion.

The loop reimplements Playwright retry logic. It also re-creates the status-text locator at Line 72, although the class already holds statusText at Line 11. expect.poll keeps the fail-fast behavior on Failed and produces better trace output.

If the filtered table can render more than one row, statusElement.textContent() raises a strict-mode error. The current code swallows that error and only fails after 3 minutes. Scope the locator to the operator row to avoid this.

♻️ Proposed refactor
-    // Debug the status element by polling its text content
-    const statusElement = this.page.getByTestId('status-text');
-
-    console.log(`Waiting for ${operatorName} operator status to be 'Succeeded'...`);
-
-    // Poll the status text every 5 seconds and log what we see
-    let attempts = 0;
-    const maxAttempts = 36; // 3 minutes worth of 5-second polls
-
-    while (attempts < maxAttempts) {
-      let currentText: string | null = null;
-      try {
-        currentText = await statusElement.textContent({ timeout: 5000 });
-        console.log(`Attempt ${attempts + 1}: Status text is "${currentText}"`);
-      } catch (error) {
-        console.log(`Attempt ${attempts + 1}: Could not read status text: ${error.message}`);
-      }
-
-      if (currentText?.includes('Succeeded')) {
-        console.log('✅ Found "Succeeded" in status text!');
-        return; // Success!
-      }
-
-      if (currentText?.includes('Failed')) {
-        throw new Error(`Operator installation failed. Status: ${currentText}`);
-      }
-
-      attempts++;
-      await this.page.waitForTimeout(5000); // Wait 5 seconds between polls
-    }
-
-    throw new Error(`Timeout waiting for operator status to be 'Succeeded' after ${maxAttempts * 5} seconds`);
+    const statusElement = operatorRow.getByTestId('status-text');
+
+    await expect
+      .poll(
+        async () => {
+          const text = await statusElement.textContent().catch(() => null);
+          if (text?.includes('Failed')) {
+            throw new Error(`Operator installation failed. Status: ${text}`);
+          }
+          return text ?? '';
+        },
+        { intervals: [5_000], timeout: 180_000 },
+      )
+      .toContain('Succeeded');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/pages/installed-operators-page.ts` around lines 63 - 103,
Replace the manual polling in verifyOperatorInstallationSucceeded with
Playwright expect.poll, reusing the class-level statusText locator where
applicable. Scope statusText to the located operatorRow so multiple table rows
cannot cause a strict-mode error, preserve immediate failure when the status
contains “Failed,” and assert successful completion when it contains
“Succeeded.”

47-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

clickOperatorRow ignores operatorURLName, which makes the pinned operator version dead data. The parameter is threaded from the spec through navigateToOperatorDetails into clickOperatorRow and is never read. The pinned version therefore implies a coupling that does not exist.

  • frontend/e2e/pages/installed-operators-page.ts#L47-L58: remove the operatorURLName parameter from clickOperatorRow, and remove it from navigateToOperatorDetails.
  • frontend/e2e/tests/olm/operator-install-single-namespace.spec.ts#L8-L12: remove the urlName field from testOperator and drop the argument from the navigateToOperatorDetails calls at Lines 129, 157, and 177.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/pages/installed-operators-page.ts` around lines 47 - 58, Remove
the unused operatorURLName parameter from clickOperatorRow and
navigateToOperatorDetails in frontend/e2e/pages/installed-operators-page.ts,
updating their call chain accordingly. In
frontend/e2e/tests/olm/operator-install-single-namespace.spec.ts, remove
testOperator.urlName and omit that argument from navigateToOperatorDetails calls
at lines 129, 157, and 177.
frontend/e2e/tests/olm/packageserver-tabs.spec.ts (1)

12-53: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Construct the page objects once per test.

Each test.step creates a new DetailsPage or YamlEditorPage for the same page. The test at Line 67 already builds them once outside the steps. Follow that pattern here.

♻️ Proposed refactor for the Details tab test
   test('renders Details tab correctly', async ({ page }) => {
+    const detailsPage = new DetailsPage(page);
+
     await test.step('Navigate to PackageManifest Details tab', async () => {
-      const detailsPage = new DetailsPage(page);
       await detailsPage.navigateToDetailsUrl(baseUrl);
     });
 
     await test.step('Verify page title shows package name', async () => {
-      const detailsPage = new DetailsPage(page);
       await expect(detailsPage.title).toContainText(packageManifestName);
     });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/tests/olm/packageserver-tabs.spec.ts` around lines 12 - 53, The
Details and YAML tests recreate page objects inside each test.step; instantiate
each test’s DetailsPage or YamlEditorPage once near the start of the test and
reuse it across all steps, matching the existing pattern used by the later test.
frontend/e2e/tests/olm/operator-uninstall.spec.ts (1)

9-34: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the duplicated Data Grid test data.

testOperator and testOperand here repeat the definitions in frontend/e2e/tests/olm/operator-install-single-namespace.spec.ts at Lines 8-21. Only createActionID differs. Move the shared constants into a helper module under frontend/e2e/test-utils/ and import them in both specs. The pinned urlName then has one owner.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/tests/olm/operator-uninstall.spec.ts` around lines 9 - 34,
Extract the shared Data Grid definitions from testOperator and testOperand in
both operator-uninstall.spec.ts and operator-install-single-namespace.spec.ts
into a helper module under frontend/e2e/test-utils/. Export and reuse the common
operator and operand constants in both specs, while allowing each spec to
provide its distinct createActionID; keep the pinned urlName defined only in the
helper.
frontend/e2e/tests/olm/operator-install-single-namespace.spec.ts (1)

76-92: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Resolve the deferred cleanup decision in afterEach.

The hook only logs when operators remain. The comment at Line 86 leaves the decision open. When the test fails before the UI uninstall step, the Subscription in openshift-operators and the cluster-scoped Operator object survive the run. The cleanup fixture removes only the tracked namespace.

Call cleanupAllOperatorsByPackageName when stillThere.length > 0, and keep the log line for diagnostics. I can prepare that change or open a tracking issue.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/tests/olm/operator-install-single-namespace.spec.ts` around
lines 76 - 92, The afterEach verification must perform deferred cleanup when
operators remain after UI uninstall. In the stillThere.length > 0 branch, retain
the diagnostic log and call cleanupAllOperatorsByPackageName with the relevant
operator package name so both the Subscription and cluster-scoped Operator are
removed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/e2e/pages/installed-operators-page.ts`:
- Around line 153-156: Update frontend/e2e/pages/installed-operators-page.ts at
lines 153-156 in verifyOperatorNotExists to wait for .loading-skeleton--table to
detach, then call filterByName before asserting the operator row is not
attached; apply the same loading-skeleton wait after selectNamespace and
filterByName in verifyOperatorNotInstalledInNamespace at lines 175-181 before
its negative assertion.
- Around line 197-205: Update the namespaceOption locator in the
installed-operators page flow to use an anchored exact-text match for namespace
rather than substring filtering and first-item selection. Ensure the final
namespace-bar-dropdown assertion also verifies the complete namespace value,
preventing similarly prefixed generated namespaces from being treated as
matches.

In `@frontend/e2e/pages/operator-install-page.ts`:
- Around line 131-134: Update the namespace-radio branch in the operator install
page to use the same bounded wait as installOperatorInNamespace instead of an
immediate selectNamespaceRadio.count() probe. Preserve the conditional
check-and-check behavior while allowing the radio to appear asynchronously.
- Around line 85-88: Update the conditional around selectNamespaceRadio in the
operator-install page to wait for the radio element with a bounded timeout
instead of using count(). Preserve the existing check behavior when the element
becomes available, while allowing the flow to continue if it does not appear
within the timeout.

In `@frontend/e2e/pages/yaml-editor-page.ts`:
- Around line 121-126: Update getEditorContent so the browser evaluation safely
checks that window.monaco and its editor API exist before calling getModels,
returning an empty string when Monaco is not yet initialized; preserve the
existing model-content behavior when the editor is available.

In `@frontend/e2e/test-utils/cluster-cleanup.ts`:
- Around line 26-40: Update the namespace discovery in the cleanup function to
call KubernetesClient.listNamespaces() and use its returned namespace
collection, removing the kubectl child_process imports, execution, JSON parsing,
and obsolete limitation comments. Keep filtering to names beginning with “test-”
and remove the redundant test-operator- predicate before continuing with the
existing cleanup flow.

In `@frontend/e2e/test-utils/olm-cleanup.ts`:
- Around line 112-115: Update the cleanupOLMOperatorCompletely call to always
scope namespacePattern to the caller’s namespace instead of passing undefined
for non-test namespaces. Preserve the existing test-namespace behavior while
ensuring cleanup cannot match resources across the entire cluster.
- Around line 63-71: Update the forceDelete branch in the cleanup deletion catch
block to perform the force deletion using
KubernetesClient.patchClusterCustomResource to strip finalizers, then retry
deletion; otherwise remove or disable forceDelete and log the original error
instead of claiming a retry occurred. Ensure failures in the force path also
surface the relevant error.
- Around line 76-82: Replace the namespacePattern no-op in the cleanup flow with
k8sClient.listNamespaces(), filter results to names starting with test-, and
delete each matching namespace through the client. Remove the stale limitation
note and manual kubectl command, while preserving the existing cleanup
completion behavior.

In `@frontend/e2e/test-utils/olm-test-cleanup.ts`:
- Around line 79-83: Update the testNamespaces predicate in
performOperatorCleanup to match only namespaces owned by this helper, removing
the broad name.includes(packageName) condition while preserving the test- prefix
matching; continue relying on the existing explicit cleanup of globalNamespace
and openshift-marketplace.
- Line 61: Replace the fixed cleanup sleeps in the hooks around
verifyAndForceCleanup and the waits at the referenced cleanup points with
timeout-backed polling using the existing listClusterCustomResources(...,
'operators') call. Continue polling until matching Operator resources disappear,
then force-clean only remaining stragglers, while preserving the existing
timeout behavior and cleanup flow.

In `@frontend/e2e/test-utils/operator-cleanup.ts`:
- Around line 136-141: Update the cleanup flow around the try/catch in the
operator cleanup function so the “✅ Cleanup complete” message is emitted only
after successful cleanup. Move that log into the try block or otherwise prevent
it from running after the catch handles an error, while preserving the existing
error logging.

In `@frontend/e2e/tests/console/crud/other-routes.spec.ts`:
- Around line 141-142: Update the URL assertion around expectedPath to prevent
substring matches by anchoring the regular expression with ^ and $, while
allowing an optional query suffix; alternatively compare the parsed URL pathname
exactly. Preserve the existing route.path query stripping and escaping behavior.

In `@frontend/e2e/tests/olm/create-namespace.spec.ts`:
- Around line 22-45: Update the stale namespace filter in the cleanup block to
match the prefix produced by generateTestNamespace(), such as test-, so leftover
namespaces are discovered and deleted; keep the existing
cleanupOperatorResources and deleteNamespace flow unchanged.

In `@frontend/e2e/tests/olm/edit-default-sources.spec.ts`:
- Around line 19-37: Add an afterEach teardown for the test that uses k8sClient
to patch the OperatorHub cluster resource, ensuring the redhat-operators default
source is enabled even when assertions or toggling fail. Keep the existing
toggle verification flow unchanged and make the cleanup unconditional.

In `@frontend/e2e/tests/olm/operator-hub.spec.ts`:
- Around line 39-53: Validate that originalTileText from
getFirstCatalogTileTitle().textContent() is non-empty before passing it to
verifyTileTextChanged; do not use an empty-string fallback that can make the
comparison vacuously succeed. Preserve the existing Community-to-Certified
filter flow and changed-title assertion once the captured title is confirmed.
- Around line 77-97: Update the empty-search verification in the operator hub
test to use toHaveCount(0) for catalogPage.getCatalogTiles(), avoiding
strict-mode issues with the multi-element locator. Remove the clearButton count
conditional and assert the clear-filters button is visible directly, then click
it and verify the search input is empty and catalog tiles return.

In `@frontend/e2e/tests/olm/operator-install-single-namespace.spec.ts`:
- Around line 48-66: Scope the namespace cleanup around generateTestNamespace to
only namespaces created by this test worker, rather than every name beginning
with test-. Track generated namespace names in the suite (or include and filter
by a unique per-run identifier), then use that owned set when invoking
cleanupOperatorResources; preserve cleanup of all resources within those owned
namespaces.
- Around line 133-144: Replace the empty-state assertion in the isolation
verification step with
InstalledOperatorsPage.verifyOperatorNotInstalledInNamespace, passing the
operator under test and globalNamespace. Keep the existing navigation and
namespace selection, and assert only that this specific operator is absent from
the global namespace.

In `@frontend/e2e/tests/olm/operator-uninstall.spec.ts`:
- Around line 116-137: The uninstall flow in the “Successfully uninstall
operator (without operands)” step conflicts with the created example-backup and
subsequent deletion assertion. Update the step to call
operatorDetailsPage.uninstallOperatorWithOperands() so the operand is explicitly
deleted, and revise the step title/comment to reflect that an operand exists and
is removed.

---

Nitpick comments:
In `@frontend/e2e/clients/kubernetes-client.ts`:
- Around line 661-685: Update listClusterCustomResources and listNamespaces to
log the caught error before returning the existing empty-array fallback.
Preserve the current return behavior and use the client’s established logging
mechanism so RBAC, network, and other failures are visible to test callers.
- Around line 282-288: Update getCurrentUser() to return undefined when
kubeConfig.getCurrentUser() throws instead of the placeholder user object, and
change its return type from any to the typed k8s.User-compatible optional return
type used consistently with getCurrentUserToken().
- Around line 571-586: Update patchClusterCustomResource to use
k8s.PatchStrategy.JsonPatch for the contentType value, matching the existing
patchCustomResource implementation and removing the duplicated literal.

In `@frontend/e2e/pages/catalog-page.ts`:
- Around line 72-78: Remove the duplicate accessor between getSearchInput and
getSearchInputElement in the catalog page object, retaining a single
search-input getter and updating any callers to use the retained method.

In `@frontend/e2e/pages/installed-operators-page.ts`:
- Around line 111-136: Remove both waitForFunction blocks surrounding
getOperatorRow(operatorName), along with their misleading comments, because
toBeVisible already provides the necessary wait and visibility check. Also
reconcile the namespace comment with the behavior of selectNamespace: either
update the comment to state that it always runs or conditionally call
selectNamespace only when namespace is not openshift-operators.
- Around line 63-103: Replace the manual polling in
verifyOperatorInstallationSucceeded with Playwright expect.poll, reusing the
class-level statusText locator where applicable. Scope statusText to the located
operatorRow so multiple table rows cannot cause a strict-mode error, preserve
immediate failure when the status contains “Failed,” and assert successful
completion when it contains “Succeeded.”
- Around line 47-58: Remove the unused operatorURLName parameter from
clickOperatorRow and navigateToOperatorDetails in
frontend/e2e/pages/installed-operators-page.ts, updating their call chain
accordingly. In
frontend/e2e/tests/olm/operator-install-single-namespace.spec.ts, remove
testOperator.urlName and omit that argument from navigateToOperatorDetails calls
at lines 129, 157, and 177.

In `@frontend/e2e/pages/operator-details-page.ts`:
- Around line 293-299: Remove the unused commented-out selector declarations
from verifyUninstallAlert, including the alert and modal-title comments, while
leaving the dialog visibility and expected-text assertions unchanged.
- Around line 101-124: Remove the commented-out destructuring and navigation
blocks from deleteOperand, including the unused breadcrumb, tab-navigation, and
operand-link code, while preserving the URL assertion and deletion flow.
- Around line 198-228: Extract the shared page-action click, modal-open wait,
title assertion, and loading-skeleton wait from uninstallOperator and
uninstallOperatorWithOperands into a private helper on the page object. Have
both methods call this helper, while preserving their existing submit and
delete-all-operands behavior.
- Around line 51-82: Refactor createOperand to only call navigateToOperandTab
with the operand name and isGlobal, then delegate the remaining creation flow to
createOperandFromTab using the same testOperand argument. Move or reuse the
shared create-form logic through createOperandFromTab so both methods cannot
diverge.

In `@frontend/e2e/pages/operator-hub-details-page.ts`:
- Around line 78-99: Refactor toggleSourceAndVerify in the operator hub details
page object to perform only the source-toggle actions, removing its modal-title
and source-status assertions and exposing the necessary steps for callers. Move
those expectations into the consuming spec, including both status checks via
getSourceStatus and the modal-title checks, so test intent and failures remain
in the spec.

In `@frontend/e2e/pages/operator-install-page.ts`:
- Around line 24-38: Extract the repeated catalog-to-install-form flow from
installOperatorGlobally and the other two install methods into a private helper:
navigate to /catalog/all-namespaces, select the Operator tab, search by operator
name, verify and click the operator card, then verify and click the install
button. Update all three methods to call this helper, preserving the existing
selectors, timeout, and robustClick behavior.

In `@frontend/e2e/test-utils/cluster-cleanup.ts`:
- Around line 154-185: Update the operand cleanup flow around operandTypes to
derive the operand group, version, and plurals from the configurable
targetOperator in ClusterCleanupOptions instead of hardcoding infinispan.org. In
the catch block surrounding listCustomResources and deleteCustomResource, log
the caught error with enough context to identify the operand type and namespace
while preserving the existing cleanup continuation behavior.
- Around line 59-151: Extract the repeated CSV, Subscription, and InstallPlan
cleanup logic into a shared cleanupResourceType helper that accepts the resource
plural, API version, and match predicate, while preserving dry-run behavior,
logging, listing, deletion, and error handling. Replace the three inline
try/catch blocks with calls to this helper using the existing resource-specific
predicates and targetOperator matching.

In `@frontend/e2e/test-utils/operator-cleanup.ts`:
- Around line 98-100: Align the subscription matching predicates in the cleanup
functions of olm-test-cleanup.ts and olm-cleanup.ts with the exact-match
behavior used by operator-cleanup.ts, replacing broader includes/startsWith
checks where appropriate. If either module must retain broader matching,
document the specific reason and intended resource-selection difference.
- Line 13: Replace the any-typed Kubernetes client parameters with the
default-imported KubernetesClient type across
frontend/e2e/test-utils/operator-cleanup.ts#L13-L13,
frontend/e2e/test-utils/cluster-cleanup.ts#L15-L15,
frontend/e2e/test-utils/olm-cleanup.ts#L17-L20, and
frontend/e2e/test-utils/olm-test-cleanup.ts#L15-L15: update all named cleanup
functions in those files, and type the olm-test-cleanup hook fixtures at Lines
149-171 instead of any. Preserve (item: any) in filter callbacks or use a narrow
local interface because KubernetesClient list methods return unknown[].
- Around line 17-52: Update the operator cleanup flow to replace the
per-namespace loop and listCustomResources calls with a single
listClusterCustomResources call for subscriptions. Use each subscription’s
metadata.namespace when invoking deleteCustomResource, while preserving matching
by operatorPackageName and deletion of all matching subscriptions.

In `@frontend/e2e/tests/olm/catalog-source-details.spec.ts`:
- Around line 72-73: Compute the timestamp once before the testNs and
catalogSourceName declarations, then reuse that shared value in both template
strings so the names always have the same suffix.

In `@frontend/e2e/tests/olm/create-namespace.spec.ts`:
- Around line 27-42: Rename the loop-local variable in the test namespace
cleanup loop from nsName to staleNsName, and update all references within that
loop, including logging, cleanupOperatorResources, deleteNamespace, and the
error message. Preserve the suite-level nsName used by afterEach unchanged.
- Around line 47-48: Replace the fixed waitForTimeout call in the cleanup hook
with polling against the API until the targeted subscriptions and
ClusterServiceVersions are absent, reusing the existing
cleanupAllOperatorsByPackageName context and allowing the poll to time out
appropriately. Remove the unused page fixture from the hook signature and its
callers while preserving the cleanup behavior.
- Around line 52-59: Update the create-namespace test cleanup hook to
destructure k8sClient directly from the afterEach fixture, matching the pattern
in operator-install-single-namespace.spec.ts. Remove the suite-level k8sClient
variable and its beforeEach assignment, while continuing to pass the fixture
client to cleanupOperatorResources.

In `@frontend/e2e/tests/olm/edit-default-sources.spec.ts`:
- Around line 26-36: Update both getSourceStatus(defaultSourceToBeToggled)
status assertions in the toggle flow to use an explicit timeout long enough for
the OperatorHub watch update after modal submission, preserving the existing
Disabled and Enabled expectations.

In `@frontend/e2e/tests/olm/operator-install-global.spec.ts`:
- Around line 41-51: Extract the duplicated cluster-operator cleanup flow from
the beforeEach and afterEach hooks into a shared helper near
cleanupDataGridOperatorResources. Have the helper list operators.coreos.com
resources, filter names by operatorPackageName, delete each matching resource,
and preserve the existing error logging; replace both hook implementations with
calls to this helper.
- Line 80: Replace the fixed delays in the operator cleanup flow around the
waitForTimeout call and raw setTimeout promise with deterministic polling that
repeatedly checks until the operator and operand resources are absent. Preserve
the cleanup sequencing, but allow the wait to finish immediately when resources
disappear and continue until the configured polling timeout when propagation is
slow.

In `@frontend/e2e/tests/olm/operator-install-single-namespace.spec.ts`:
- Around line 76-92: The afterEach verification must perform deferred cleanup
when operators remain after UI uninstall. In the stillThere.length > 0 branch,
retain the diagnostic log and call cleanupAllOperatorsByPackageName with the
relevant operator package name so both the Subscription and cluster-scoped
Operator are removed.

In `@frontend/e2e/tests/olm/operator-uninstall.spec.ts`:
- Around line 9-34: Extract the shared Data Grid definitions from testOperator
and testOperand in both operator-uninstall.spec.ts and
operator-install-single-namespace.spec.ts into a helper module under
frontend/e2e/test-utils/. Export and reuse the common operator and operand
constants in both specs, while allowing each spec to provide its distinct
createActionID; keep the pinned urlName defined only in the helper.

In `@frontend/e2e/tests/olm/packageserver-tabs.spec.ts`:
- Around line 12-53: The Details and YAML tests recreate page objects inside
each test.step; instantiate each test’s DetailsPage or YamlEditorPage once near
the start of the test and reuse it across all steps, matching the existing
pattern used by the later test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

Comment on lines +153 to +156
async verifyOperatorNotExists(operatorName: string): Promise<void> {
await this.navigateToInstalledOperators();
await expect(this.getOperatorRow(operatorName)).not.toBeAttached();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Negative operator-row assertions run before the operator list loads. Both methods navigate and then assert not.toBeAttached(). Right after navigation the table renders .loading-skeleton--table, so the row is absent and the assertion passes even when the operator is still installed. verifyNoOperatorsInstalled at Line 165 already shows the correct wait.

  • frontend/e2e/pages/installed-operators-page.ts#L153-L156: wait for .loading-skeleton--table to detach, then apply filterByName, before the not.toBeAttached() assertion in verifyOperatorNotExists.
  • frontend/e2e/pages/installed-operators-page.ts#L175-L181: wait for .loading-skeleton--table to detach after selectNamespace and filterByName, before the not.toBeAttached() assertion in verifyOperatorNotInstalledInNamespace.
📍 Affects 1 file
  • frontend/e2e/pages/installed-operators-page.ts#L153-L156 (this comment)
  • frontend/e2e/pages/installed-operators-page.ts#L175-L181
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/pages/installed-operators-page.ts` around lines 153 - 156,
Update frontend/e2e/pages/installed-operators-page.ts at lines 153-156 in
verifyOperatorNotExists to wait for .loading-skeleton--table to detach, then
call filterByName before asserting the operator row is not attached; apply the
same loading-skeleton wait after selectNamespace and filterByName in
verifyOperatorNotInstalledInNamespace at lines 175-181 before its negative
assertion.

Comment on lines +197 to +205
// Filter the namespace list to make the target namespace visible
const textFilter = this.page.getByTestId('dropdown-text-filter');
await textFilter.fill(namespace);

// Select the dropdown menu item that contains our namespace text
const namespaceOption = this.page.getByTestId('dropdown-menu-item-link').filter({ hasText: namespace }).first();
await this.robustClick(namespaceOption);

await expect(this.page.getByTestId('namespace-bar-dropdown')).toContainText(namespace);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Match the namespace option exactly.

filter({ hasText: namespace }) performs a substring match, and .first() then takes whichever item appears first. If the dropdown contains a namespace whose name starts with the target name, the wrong namespace is selected. Generated namespaces share the test- prefix, so several candidates can coexist. The final toContainText(namespace) assertion is also substring-based, so it does not catch the mistake.

Use an anchored match instead.

🐛 Proposed fix
+    const escaped = namespace.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
     const namespaceOption = this.page
       .getByTestId('dropdown-menu-item-link')
-      .filter({ hasText: namespace })
+      .filter({ hasText: new RegExp(`^${escaped}$`) })
       .first();
     await this.robustClick(namespaceOption);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Filter the namespace list to make the target namespace visible
const textFilter = this.page.getByTestId('dropdown-text-filter');
await textFilter.fill(namespace);
// Select the dropdown menu item that contains our namespace text
const namespaceOption = this.page.getByTestId('dropdown-menu-item-link').filter({ hasText: namespace }).first();
await this.robustClick(namespaceOption);
await expect(this.page.getByTestId('namespace-bar-dropdown')).toContainText(namespace);
// Filter the namespace list to make the target namespace visible
const textFilter = this.page.getByTestId('dropdown-text-filter');
await textFilter.fill(namespace);
const escaped = namespace.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// Select the dropdown menu item that contains our namespace text
const namespaceOption = this.page.getByTestId('dropdown-menu-item-link').filter({ hasText: new RegExp(`^${escaped}$`) }).first();
await this.robustClick(namespaceOption);
await expect(this.page.getByTestId('namespace-bar-dropdown')).toContainText(namespace);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/pages/installed-operators-page.ts` around lines 197 - 205,
Update the namespaceOption locator in the installed-operators page flow to use
an anchored exact-text match for namespace rather than substring filtering and
first-item selection. Ensure the final namespace-bar-dropdown assertion also
verifies the complete namespace value, preventing similarly prefixed generated
namespaces from being treated as matches.

Comment on lines +85 to +88
// Check if select namespace radio exists
if (await this.selectNamespaceRadio.count() > 0) {
await this.selectNamespaceRadio.check();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

count() does not wait, so this branch can skip the radio selection.

selectNamespaceRadio.count() returns immediately. The console renders the namespace radio group after specificNamespaceRadio.check() resolves, so the count can still be 0 and the test then clicks the dropdown without selecting the radio. Wait for the element with a bounded timeout instead.

🐛 Proposed fix
-      // Check if select namespace radio exists
-      if (await this.selectNamespaceRadio.count() > 0) {
-        await this.selectNamespaceRadio.check();
-      }
+      // Wait for the optional radio to render before deciding
+      const hasSelectNamespaceRadio = await this.selectNamespaceRadio
+        .waitFor({ state: 'attached', timeout: 10_000 })
+        .then(() => true)
+        .catch(() => false);
+      if (hasSelectNamespaceRadio) {
+        await this.selectNamespaceRadio.check();
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Check if select namespace radio exists
if (await this.selectNamespaceRadio.count() > 0) {
await this.selectNamespaceRadio.check();
}
// Wait for the optional radio to render before deciding
const hasSelectNamespaceRadio = await this.selectNamespaceRadio
.waitFor({ state: 'attached', timeout: 10_000 })
.then(() => true)
.catch(() => false);
if (hasSelectNamespaceRadio) {
await this.selectNamespaceRadio.check();
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/pages/operator-install-page.ts` around lines 85 - 88, Update the
conditional around selectNamespaceRadio in the operator-install page to wait for
the radio element with a bounded timeout instead of using count(). Preserve the
existing check behavior when the element becomes available, while allowing the
flow to continue if it does not appear within the timeout.

Comment on lines +131 to +134
// Check if select namespace radio exists
if (await this.selectNamespaceRadio.count() > 0) {
await this.selectNamespaceRadio.check();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Same non-waiting count() check as in installOperatorInNamespace.

This branch repeats the immediate count() probe. Apply the same bounded wait here. See the consolidated comment for the shared fix.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/pages/operator-install-page.ts` around lines 131 - 134, Update
the namespace-radio branch in the operator install page to use the same bounded
wait as installOperatorInNamespace instead of an immediate
selectNamespaceRadio.count() probe. Preserve the conditional check-and-check
behavior while allowing the radio to appear asynchronously.

Comment on lines +121 to +126
async getEditorContent(): Promise<string> {
return this.page.evaluate(() => {
const models = (window as any).monaco.editor.getModels();
return models[0]?.getValue() || '';
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard the monaco global, not just the model.

models[0]?.getValue() || '' only guards a missing model. If window.monaco is not yet assigned, getModels() throws TypeError: Cannot read properties of undefined, and the empty-string fallback never applies. Three specs call this helper, so a slow editor load fails the test with a confusing browser error.

🐛 Proposed fix
   async getEditorContent(): Promise<string> {
+    await this.waitForEditorReady();
     return this.page.evaluate(() => {
-      const models = (window as any).monaco.editor.getModels();
-      return models[0]?.getValue() || '';
+      const models = (window as any).monaco?.editor?.getModels?.() ?? [];
+      return models[0]?.getValue() ?? '';
     });
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async getEditorContent(): Promise<string> {
return this.page.evaluate(() => {
const models = (window as any).monaco.editor.getModels();
return models[0]?.getValue() || '';
});
}
async getEditorContent(): Promise<string> {
await this.waitForEditorReady();
return this.page.evaluate(() => {
const models = (window as any).monaco?.editor?.getModels?.() ?? [];
return models[0]?.getValue() ?? '';
});
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/pages/yaml-editor-page.ts` around lines 121 - 126, Update
getEditorContent so the browser evaluation safely checks that window.monaco and
its editor API exist before calling getModels, returning an empty string when
Monaco is not yet initialized; preserve the existing model-content behavior when
the editor is available.

Comment on lines +39 to +53
// Track which tile is first with Community filter
const originalTileText = await catalogPage.getFirstCatalogTileTitle().textContent();

// Disable Community filter
await catalogPage.toggleSourceFilter('community');

// Enable Certified filter
await catalogPage.toggleSourceFilter('certified');
await expect(async () => {
const count = await catalogPage.getCatalogTiles().count();
expect(count).toBeGreaterThan(0);
}).toPass();

// Verify the first tile title is different from Community filter
await catalogPage.verifyTileTextChanged(originalTileText || '');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The empty-string fallback can make the assertion always pass.

textContent() returns string | null. If the title is not yet rendered, originalTileText is null, and line 53 asserts not.toHaveText(''), which any non-empty tile satisfies. The step then reports success without comparing the two filters. Assert the captured text is non-empty before you use it.

🐛 Proposed fix
-      const originalTileText = await catalogPage.getFirstCatalogTileTitle().textContent();
+      const originalTileText = await catalogPage.getFirstCatalogTileTitle().innerText();
+      expect(originalTileText.trim()).not.toBe('');
@@
-      await catalogPage.verifyTileTextChanged(originalTileText || '');
+      await catalogPage.verifyTileTextChanged(originalTileText);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Track which tile is first with Community filter
const originalTileText = await catalogPage.getFirstCatalogTileTitle().textContent();
// Disable Community filter
await catalogPage.toggleSourceFilter('community');
// Enable Certified filter
await catalogPage.toggleSourceFilter('certified');
await expect(async () => {
const count = await catalogPage.getCatalogTiles().count();
expect(count).toBeGreaterThan(0);
}).toPass();
// Verify the first tile title is different from Community filter
await catalogPage.verifyTileTextChanged(originalTileText || '');
// Track which tile is first with Community filter
const originalTileText = await catalogPage.getFirstCatalogTileTitle().innerText();
expect(originalTileText.trim()).not.toBe('');
// Disable Community filter
await catalogPage.toggleSourceFilter('community');
// Enable Certified filter
await catalogPage.toggleSourceFilter('certified');
await expect(async () => {
const count = await catalogPage.getCatalogTiles().count();
expect(count).toBeGreaterThan(0);
}).toPass();
// Verify the first tile title is different from Community filter
await catalogPage.verifyTileTextChanged(originalTileText);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/tests/olm/operator-hub.spec.ts` around lines 39 - 53, Validate
that originalTileText from getFirstCatalogTileTitle().textContent() is non-empty
before passing it to verifyTileTextChanged; do not use an empty-string fallback
that can make the comparison vacuously succeed. Preserve the existing
Community-to-Certified filter flow and changed-title assertion once the captured
title is confirmed.

Comment on lines +77 to +97
// Wait for search to complete and verify no tiles
await expect(catalogPage.getCatalogTiles()).not.toBeAttached({ timeout: 10_000 });

// Check if clear filters button appears
const clearButton = catalogPage.getClearFiltersButton();
if (await clearButton.count() > 0) {
await expect(clearButton).toBeVisible();
await catalogPage.clickClearAllFilters();
await expect(catalogPage.getSearchInput()).toBeEmpty();
await expect(async () => {
const count = await catalogPage.getCatalogTiles().count();
expect(count).toBeGreaterThan(0);
}).toPass();
} else {
// If clear button doesn't appear, just clear the search manually
await catalogPage.clearSearchFilter();
await expect(async () => {
const count = await catalogPage.getCatalogTiles().count();
expect(count).toBeGreaterThan(0);
}).toPass();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use toHaveCount(0) and remove the conditional branch.

Two problems in this step:

  1. getCatalogTiles() matches many elements. not.toBeAttached() on a multi-element locator is a strict-mode operation. Use toHaveCount(0) to assert an empty result set.
  2. The if (await clearButton.count() > 0) branch means the test passes whether or not the clear-filters button appears. The step then does not verify the documented behavior. The empty search result always renders the empty state, so assert the button directly.
🐛 Proposed fix
-      // Wait for search to complete and verify no tiles
-      await expect(catalogPage.getCatalogTiles()).not.toBeAttached({ timeout: 10_000 });
-
-      // Check if clear filters button appears
-      const clearButton = catalogPage.getClearFiltersButton();
-      if (await clearButton.count() > 0) {
-        await expect(clearButton).toBeVisible();
-        await catalogPage.clickClearAllFilters();
-        await expect(catalogPage.getSearchInput()).toBeEmpty();
-        await expect(async () => {
-          const count = await catalogPage.getCatalogTiles().count();
-          expect(count).toBeGreaterThan(0);
-        }).toPass();
-      } else {
-        // If clear button doesn't appear, just clear the search manually
-        await catalogPage.clearSearchFilter();
-        await expect(async () => {
-          const count = await catalogPage.getCatalogTiles().count();
-          expect(count).toBeGreaterThan(0);
-        }).toPass();
-      }
+      // Wait for search to complete and verify no tiles
+      await expect(catalogPage.getCatalogTiles()).toHaveCount(0, { timeout: 10_000 });
+
+      await expect(catalogPage.getClearFiltersButton()).toBeVisible();
+      await catalogPage.clickClearAllFilters();
+      await expect(catalogPage.getSearchInput()).toBeEmpty();
+      await expect(catalogPage.getCatalogTiles().first()).toBeVisible();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Wait for search to complete and verify no tiles
await expect(catalogPage.getCatalogTiles()).not.toBeAttached({ timeout: 10_000 });
// Check if clear filters button appears
const clearButton = catalogPage.getClearFiltersButton();
if (await clearButton.count() > 0) {
await expect(clearButton).toBeVisible();
await catalogPage.clickClearAllFilters();
await expect(catalogPage.getSearchInput()).toBeEmpty();
await expect(async () => {
const count = await catalogPage.getCatalogTiles().count();
expect(count).toBeGreaterThan(0);
}).toPass();
} else {
// If clear button doesn't appear, just clear the search manually
await catalogPage.clearSearchFilter();
await expect(async () => {
const count = await catalogPage.getCatalogTiles().count();
expect(count).toBeGreaterThan(0);
}).toPass();
}
// Wait for search to complete and verify no tiles
await expect(catalogPage.getCatalogTiles()).toHaveCount(0, { timeout: 10_000 });
await expect(catalogPage.getClearFiltersButton()).toBeVisible();
await catalogPage.clickClearAllFilters();
await expect(catalogPage.getSearchInput()).toBeEmpty();
await expect(catalogPage.getCatalogTiles().first()).toBeVisible();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/tests/olm/operator-hub.spec.ts` around lines 77 - 97, Update the
empty-search verification in the operator hub test to use toHaveCount(0) for
catalogPage.getCatalogTiles(), avoiding strict-mode issues with the
multi-element locator. Remove the clearButton count conditional and assert the
clear-filters button is visible directly, then click it and verify the search
input is empty and catalog tiles return.

Comment on lines +48 to +66
// Also clean up any test namespaces that might have conflicting OperatorGroups
try {
const namespaces = await k8sClient.listNamespaces();
const testNamespaces = namespaces.filter((ns: any) => ns.metadata.name.startsWith('test-'));

for (const testNs of testNamespaces) {
const nsName = (testNs as any)?.metadata?.name;
if (nsName) {
console.log(`Cleaning up test namespace: ${nsName}`);
await cleanupOperatorResources(k8sClient, {
operatorPackageName,
operandPlural: 'backups',
namespace: nsName,
});
}
}
} catch (error) {
console.log('Error cleaning up test namespaces:', error.message);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

The test- prefix cleanup deletes resources that belong to other running tests.

Line 51 matches every namespace whose name starts with test-. cleanupOperatorResources in frontend/e2e/test-utils/operator-cleanup.ts deletes all OperatorGroups in any namespace with that prefix. generateTestNamespace() gives every spec in this suite the same test- prefix. When Playwright runs specs in parallel, or when a shared cluster holds a user namespace named test-*, this hook deletes Subscriptions and OperatorGroups that another test owns.

Scope the cleanup to namespaces this worker created. Track the created namespaces in the suite, or add a per-run identifier to the generated names and filter on it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/tests/olm/operator-install-single-namespace.spec.ts` around
lines 48 - 66, Scope the namespace cleanup around generateTestNamespace to only
namespaces created by this test worker, rather than every name beginning with
test-. Track generated namespace names in the suite (or include and filter by a
unique per-run identifier), then use that owned set when invoking
cleanupOperatorResources; preserve cleanup of all resources within those owned
namespaces.

Comment on lines +133 to +144
await test.step('Verify operator is NOT installed globally (isolation test)', async () => {
// This is a key verification that distinguishes single namespace from global installation
await installedOperatorsPage.navigateToInstalledOperators();

// Switch to global namespace and verify operator is not there
await installedOperatorsPage.selectNamespace(globalNamespace);

const emptyState = page.getByTestId('console-empty-state');
await expect(emptyState.or(page.locator('[data-test="msg-box-title"]'))).toContainText(
/No Operators found|No results found/,
);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The isolation check asserts an empty state in a namespace that usually holds operators.

Lines 140-143 require openshift-operators to show "No Operators found" or "No results found". Most clusters already have at least one globally installed operator in openshift-operators, so the empty state never renders and the test fails for a reason unrelated to isolation.

Assert that this specific operator is absent instead. InstalledOperatorsPage.verifyOperatorNotInstalledInNamespace already does that.

🐛 Proposed fix
-      await installedOperatorsPage.navigateToInstalledOperators();
-
-      // Switch to global namespace and verify operator is not there
-      await installedOperatorsPage.selectNamespace(globalNamespace);
-
-      const emptyState = page.getByTestId('console-empty-state');
-      await expect(emptyState.or(page.locator('[data-test="msg-box-title"]'))).toContainText(
-        /No Operators found|No results found/,
-      );
+      await installedOperatorsPage.verifyOperatorNotInstalledInNamespace(
+        testOperator.name,
+        globalNamespace,
+      );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await test.step('Verify operator is NOT installed globally (isolation test)', async () => {
// This is a key verification that distinguishes single namespace from global installation
await installedOperatorsPage.navigateToInstalledOperators();
// Switch to global namespace and verify operator is not there
await installedOperatorsPage.selectNamespace(globalNamespace);
const emptyState = page.getByTestId('console-empty-state');
await expect(emptyState.or(page.locator('[data-test="msg-box-title"]'))).toContainText(
/No Operators found|No results found/,
);
});
await test.step('Verify operator is NOT installed globally (isolation test)', async () => {
// This is a key verification that distinguishes single namespace from global installation
await installedOperatorsPage.verifyOperatorNotInstalledInNamespace(
testOperator.name,
globalNamespace,
);
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/tests/olm/operator-install-single-namespace.spec.ts` around
lines 133 - 144, Replace the empty-state assertion in the isolation verification
step with InstalledOperatorsPage.verifyOperatorNotInstalledInNamespace, passing
the operator under test and globalNamespace. Keep the existing navigation and
namespace selection, and assert only that this specific operator is absent from
the global namespace.

Comment on lines +116 to +137
await test.step('Successfully uninstall operator (without operands)', async () => {
// Navigate back to operator details page to ensure clean state
await installedOperatorsPage.navigateToOperatorDetails(testOperator.name, testOperator.urlName, testNamespace);

// Uninstall operator normally (without trying to delete operands since none exist)
await operatorDetailsPage.uninstallOperator();

// Verify operator no longer exists
await installedOperatorsPage.verifyOperatorNotExists(testOperator.name);
});

await test.step('Verify operand instance is deleted', async () => {
// Verify operand is deleted via K8s API (should throw 404)
await expect(async () => {
await k8sClient.getCustomResource(
testOperand.group,
testOperand.version,
testNamespace,
'backups',
testOperand.exampleName
);
}).rejects.toThrow();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -t f 'operator-details-page.ts' frontend/e2e | xargs -r rg -n -A 40 'async uninstallOperator'
rg -n 'delete-all-operands|Delete all operand' frontend/packages/operator-lifecycle-manager/src/components/modals/uninstall-operator-modal.tsx

Repository: openshift/console

Length of output: 3326


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="$(fd -t f 'operator-uninstall.spec.ts' frontend/e2e | head -n1)"
echo "FILE=$file"
wc -l "$file"
sed -n '1,160p' "$file" | cat -n
rg -n "uninstallOperator|uninstallOperatorWithOperands|Delete all operand|delete-all-operands|verifyOperatorNotExists|getCustomResource|example-backup|backups" frontend/e2e frontend/packages/operator-lifecycle-manager/src/components/modals/uninstall-operator-modal.tsx

Repository: openshift/console

Length of output: 10478


Do not uninstall the operator while expecting the operand to be removed by default.

Lines 80-82 create example-backup, so the test state contradicts the “without operands” step title and “since none exist” comment. operatorDetailsPage.uninstallOperator() only submits the uninstall modal and does not select delete-all-operands; operatorDetailsPage.uninstallOperatorWithOperands() selects it. If the operand may remain after uninstall, use uninstallOperatorWithOperands() in this step or remove the deletion assertion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/e2e/tests/olm/operator-uninstall.spec.ts` around lines 116 - 137,
The uninstall flow in the “Successfully uninstall operator (without operands)”
step conflicts with the created example-backup and subsequent deletion
assertion. Update the step to call
operatorDetailsPage.uninstallOperatorWithOperands() so the operand is explicitly
deleted, and revise the step title/comment to reflect that an operand exists and
is removed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component/core Related to console core functionality component/olm Related to OLM component/shared Related to console-shared do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant