Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions test/ui-e2e/.auth/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,20 @@ setup('authenticate to OpenShift Cluster', async ({ page, baseURL }) => {
await passwordInput.fill(process.env.CLUSTER_PASSWORD);
await page.getByRole('button', { name: /Log in/i }).click();

// Handle the OpenShift 4.x Welcome Tour modal if it appears
try {
const skipTourButton = page.getByRole('button', { name: /skip tour/i });
// Wait up to 5 seconds for the modal to pop up
await skipTourButton.waitFor({ state: 'visible', timeout: 5000 });
await skipTourButton.click();
console.log('Dismissed the OpenShift Welcome Tour modal.');
} catch (error) {
// If it doesn't appear within 5 seconds, it's an older cluster or already dismissed.
// Safely ignore the error and move on
}

// Save the auth state
await expect(page.getByRole('navigation').first()).toBeVisible({ timeout: 15000 });
await expect(page.getByRole('navigation').first()).toBeVisible({ timeout: 20000 });
await expect(page).toHaveURL(/(console|k8s|overview|dashboards)/i, { timeout: 15000 });
await page.context().storageState({ path: authFile });

});
13 changes: 8 additions & 5 deletions test/ui-e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,10 @@ All executions are driven via the ./run-ui-tests.sh wrapper script. This wrapper

| Target | Command |
| --- | --- |
| **Run All Tests (Headless/CI Mode)** | `./run-ui-tests.sh --project=chromium` |
| **Run All Tests (Headed + Visual Tracing)** | `./run-ui-tests.sh --project=chromium --headed --trace on` |
| **Run a Specific Spec File** | `./run-ui-tests.sh tests/create-application.spec.ts --project=chromium --headed --trace on` |
| **Run All Tests (Local Headless)** | `./run-ui-tests.sh --project=chromium` |
| **Run All Tests (Local Headed + Trace)** | `./run-ui-tests.sh --project=chromium --headed --trace on` |
| **Run All Tests (Simulate CI)** | `./run-ui-tests.sh --env=ci --project=chromium` |
| **Run a Specific Spec File** | `./run-ui-tests.sh tests/resource-tree.spec.ts --project=chromium --headed` |

### Playwright Flags Reference

Expand All @@ -67,6 +68,7 @@ All executions are driven via the ./run-ui-tests.sh wrapper script. This wrapper
| `--headed` | Launches the visible Chromium browser UI. Excellent for local debugging. |
| `--trace on` | Records a granular execution trace (DOM snapshots, network calls, actions) for visual triage. |
| `--reporter=list` | Switches stdout to a clean line-by-line format, ideal for monitoring real-time execution steps. |
| `--env=<ci|pipeline>` | Overrides the local setup to simulate automation. It forces headless execution, performs a clean `npm ci`, and installs required browser binaries dynamically. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Escape | in the table cell to prevent broken rendering

Line 71 currently creates an extra Markdown table column (MD056). Escape the pipe in the inline flag so the table stays 2-column.

Suggested fix
-| `--env=<ci|pipeline>` | Overrides the local setup to simulate automation. It forces headless execution, performs a clean `npm ci`, and installs required browser binaries dynamically. |
+| `--env=<ci\|pipeline>` | Overrides the local setup to simulate automation. It forces headless execution, performs a clean `npm ci`, and installs required browser binaries dynamically. |
📝 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
| `--env=<ci|pipeline>` | Overrides the local setup to simulate automation. It forces headless execution, performs a clean `npm ci`, and installs required browser binaries dynamically. |
| `--env=<ci\|pipeline>` | Overrides the local setup to simulate automation. It forces headless execution, performs a clean `npm ci`, and installs required browser binaries dynamically. |
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 71-71: Table column count
Expected: 2; Actual: 3; Too many cells, extra data will be missing

(MD056, table-column-count)

🤖 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 `@test/ui-e2e/README.md` at line 71, The unescaped pipe character in the inline
flag `--env=<ci|pipeline>` on line 71 of the table is being interpreted as a
Markdown table column separator, causing the table structure to break (MD056
error). Escape the pipe character by adding a backslash before it in the flag
value so that Markdown treats it as literal text rather than a column delimiter,
maintaining the intended 2-column table layout.

Source: Linters/SAST tools


### Visual Debugging (Trace Viewer)

Expand All @@ -91,8 +93,9 @@ npx playwright show-trace test-results/create-application-chromium/trace.zip
│ └── pages/ # Page Object Models (POM) isolating UI selectors from spec logic
│ └── ApplicationsPage.ts
├── tests/ # Test specs organized by feature epic
│ ├── login.spec.ts
│ └── create-application.spec.ts
│ ├── admin-login.spec.ts
│ ├── create-application.spec.ts
│ └── resource-tree.spec.ts
├── .env # Local runtime environment overrides (Git ignored)
└── run-ui-tests.sh # Context-aware orchestrator & URL discovery engine

Expand Down
40 changes: 35 additions & 5 deletions test/ui-e2e/run-ui-tests.sh
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
#!/bin/bash

# use arguments to extract --env and keep the rest for Playwright
ENV="local"
TEST_ARGS=()

while [[ "$#" -gt 0 ]]; do
case $1 in
--env=*) ENV="${1#*=}" ;;
*) TEST_ARGS+=("$1") ;; # Save all other args (files, --headed, etc.)
esac
shift
done

if [ -f .env ]; then
echo "Loading variables from .env file..."
set -a #export all variables
source .env
set +a # stop automatically exporting
set +a #stop auto export
fi

#making sure we are in the correct dir
cd "$(dirname "$0")" || exit 1

Comment on lines 15 to 24

Copy link
Copy Markdown

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

Load .env after switching to the script directory

source .env runs before cd "$(dirname "$0")", so Line 15 checks the caller’s CWD, not test/ui-e2e. That breaks the documented setup when the script is launched from repo root or CI wrappers.

Suggested fix
- if [ -f .env ]; then
-   echo "Loading variables from .env file..."
-   set -a  `#export` all variables
-   source .env
-   set +a  `#stop` auto export
- fi
-
- `#making` sure we are in the correct dir
- cd "$(dirname "$0")" || exit 1
+ `#making` sure we are in the correct dir
+ cd "$(dirname "$0")" || exit 1
+
+ if [ -f .env ]; then
+   echo "Loading variables from .env file..."
+   set -a  `#export` all variables
+   source .env
+   set +a  `#stop` auto export
+ fi
📝 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
if [ -f .env ]; then
echo "Loading variables from .env file..."
set -a #export all variables
source .env
set +a # stop automatically exporting
set +a #stop auto export
fi
#making sure we are in the correct dir
cd "$(dirname "$0")" || exit 1
`#making` sure we are in the correct dir
cd "$(dirname "$0")" || exit 1
if [ -f .env ]; then
echo "Loading variables from .env file..."
set -a `#export` all variables
source .env
set +a `#stop` auto export
fi
🧰 Tools
🪛 Shellcheck (0.11.0)

[info] 18-18: Not following: .env was not specified as input (see shellcheck -x).

(SC1091)

🤖 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 `@test/ui-e2e/run-ui-tests.sh` around lines 15 - 24, The .env file is being
sourced before the script changes to its own directory, causing the check for
.env and the source command to look in the caller's current working directory
instead of the test/ui-e2e directory. Move the cd "$(dirname "$0")" || exit 1
command to execute before the if [ -f .env ] block so that the .env file is
correctly loaded from the script's own directory, not from wherever the script
was invoked.

# username (might be something different for rosa - can be overwritten with export CLUSTER_USER)
#username (might be something different for rosa - can be overwritten with export CLUSTER_USER)
export CLUSTER_USER=${CLUSTER_USER:-"kubeadmin"}
export IDP=${IDP:-"kube:admin"}

Expand All @@ -26,11 +38,11 @@ if [ -n "$OC_API_URL" ] && [ -n "$CLUSTER_PASSWORD" ]; then
exit 1
fi
elif ! oc whoami > /dev/null 2>&1; then
# If variables don't exist AND we aren't logged in, fail out
#if variables don't exist AND we aren't logged in fail out
echo "Error: Not logged in. Missing OC_API_URL or CLUSTER_PASSWORD."
exit 1
else
# If variables don't exist but we ARE logged in locally, just use the current session
#if variables don't exist but we ARE logged in locally just use the current session
echo "No .env credentials found. Using existing oc CLI session..."
fi

Expand All @@ -53,4 +65,22 @@ rm -f .auth/storageState.json || true

#run Playwright
echo " Starting Playwright tests..."
npx playwright test "$@"

# 2. Execute based on the environment
if [[ "$ENV" == "ci" ]] || [[ "$ENV" == "pipeline" ]]; then
echo "Running headlessly in automation ($ENV)..."
npm ci

# Prevent sudo jump-scares for local Mac users simulating CI
if [[ "$(uname -s)" == "Darwin" ]]; then
npx playwright install chromium
else
npx playwright install chromium --with-deps
fi

npx playwright test "${TEST_ARGS[@]}" --reporter=list

Comment on lines +70 to +82

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

--env=ci|pipeline does not actually force headless

In CI/pipeline mode, Line 81 still passes user args verbatim. If --headed is provided, Playwright can run headed, which conflicts with the intended automation behavior and can fail in headless-only environments.

Suggested fix
 if [[ "$ENV" == "ci" ]] || [[ "$ENV" == "pipeline" ]]; then
     echo "Running headlessly in automation ($ENV)..."
     npm ci
@@
-    npx playwright test "${TEST_ARGS[@]}" --reporter=list
+    FILTERED_ARGS=()
+    for arg in "${TEST_ARGS[@]}"; do
+      [[ "$arg" == "--headed" ]] && continue
+      FILTERED_ARGS+=("$arg")
+    done
+    npx playwright test "${FILTERED_ARGS[@]}" --reporter=list
📝 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
if [[ "$ENV" == "ci" ]] || [[ "$ENV" == "pipeline" ]]; then
echo "Running headlessly in automation ($ENV)..."
npm ci
# Prevent sudo jump-scares for local Mac users simulating CI
if [[ "$(uname -s)" == "Darwin" ]]; then
npx playwright install chromium
else
npx playwright install chromium --with-deps
fi
npx playwright test "${TEST_ARGS[@]}" --reporter=list
if [[ "$ENV" == "ci" ]] || [[ "$ENV" == "pipeline" ]]; then
echo "Running headlessly in automation ($ENV)..."
npm ci
# Prevent sudo jump-scares for local Mac users simulating CI
if [[ "$(uname -s)" == "Darwin" ]]; then
npx playwright install chromium
else
npx playwright install chromium --with-deps
fi
FILTERED_ARGS=()
for arg in "${TEST_ARGS[@]}"; do
[[ "$arg" == "--headed" ]] && continue
FILTERED_ARGS+=("$arg")
done
npx playwright test "${FILTERED_ARGS[@]}" --reporter=list
🤖 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 `@test/ui-e2e/run-ui-tests.sh` around lines 70 - 82, In the CI/pipeline
automation block where ENV is "ci" or "pipeline", the npx playwright test
command on line 81 passes TEST_ARGS verbatim without enforcing headless mode. If
TEST_ARGS contains a --headed flag, it will override the intended automation
behavior and can fail in headless-only environments. Either filter out any
--headed flags from TEST_ARGS before passing them to the playwright test
command, or explicitly add a --headed=false flag to the npx playwright test
invocation within the CI/pipeline conditional block to ensure headless execution
is enforced regardless of user-provided arguments.

else
echo "Running Locally..."
npx playwright test "${TEST_ARGS[@]}"
fi
64 changes: 64 additions & 0 deletions test/ui-e2e/src/pages/ApplicationDetailsPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Page, expect, Locator } from '@playwright/test';

export class ApplicationDetailsPage {
readonly page: Page;
readonly resourceTreeContainer: Locator;
readonly slideOutPanel: Locator;
readonly logsTab: Locator;

constructor(page: Page) {
this.page = page;

//main container
this.resourceTreeContainer = page.locator('.application-details__tree');

//details panel that slides out (isolate the active visible pane)
this.slideOutPanel = page.locator('.sliding-panel').filter({ visible: true });

//logs tab inside the slide-out panel
this.logsTab = this.slideOutPanel.getByRole('button', { name: /logs/i }).or(this.slideOutPanel.getByText(/logs/i, { exact: true }));
}

async verifyResourceTreeLoaded() {
//wait tree to be visible
await expect(this.resourceTreeContainer).toBeVisible({ timeout: 20000 });
//wait for healthy status
await expect(this.page.getByText('Healthy', { exact: true }).first()).toBeVisible({ timeout: 30000 });

Copy link
Copy Markdown

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

Scope the “Healthy” assertion to the resource tree container to avoid false positives.

this.page.getByText('Healthy') can match unrelated UI regions, so the helper may pass even when the tree state is wrong.

Suggested fix
-    await expect(this.page.getByText('Healthy', { exact: true }).first()).toBeVisible({ timeout: 30000 });
+    await expect(
+      this.resourceTreeContainer.getByText('Healthy', { exact: true }).first()
+    ).toBeVisible({ timeout: 30000 });
📝 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 expect(this.page.getByText('Healthy', { exact: true }).first()).toBeVisible({ timeout: 30000 });
await expect(
this.resourceTreeContainer.getByText('Healthy', { exact: true }).first()
).toBeVisible({ timeout: 30000 });
🤖 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 `@test/ui-e2e/src/pages/ApplicationDetailsPage.ts` at line 26, The assertion on
the "Healthy" text is currently scoped to the entire page via
this.page.getByText, which can match unrelated UI elements and cause false
positives. Instead of using this.page.getByText('Healthy', { exact: true
}).first(), first locate the resource tree container element, then apply the
getByText selector to that specific container element rather than the whole page
to ensure you're only checking the health status within the correct UI region.

}

async clickResourceNode(kind: string, name: string) {
//find the innermost div representing the resource node
const node = this.resourceTreeContainer
.locator('div')
.filter({ hasText: kind })
.filter({ hasText: name })
.last();

//scroll it into view and click it
await node.scrollIntoViewIfNeeded();
await node.waitFor({ state: 'visible', timeout: 15000 });
await node.click();

//self-healing validation block to handle frontend rendering lag
await expect(async () => {
await expect(this.slideOutPanel).toBeVisible({ timeout: 2000 });
}).toPass({ timeout: 10000 });
}

async verifyPodLogs(expectedLogText?: string) {
//click Logs
await this.logsTab.waitFor({ state: 'visible', timeout: 5000 });
await this.logsTab.click();

const logFilterInput = this.slideOutPanel.getByPlaceholder('containing');
await expect(logFilterInput).toBeVisible({ timeout: 15000 });

if (expectedLogText) {
//find log line anywhere in the slide-out panel
await expect(this.slideOutPanel).toContainText(expectedLogText, { timeout: 30000 });
} else {
const genericLogLine = this.slideOutPanel.getByText(/\d{4}-\d{2}-\d{2}.*(INFO|Started)/).first();
await expect(genericLogLine).toBeVisible({ timeout: 30000 });
}
}
}
51 changes: 40 additions & 11 deletions test/ui-e2e/src/pages/ApplicationsPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,19 @@ export class ApplicationsPage {
await locator.press('Enter');
}

async createApp(appName: string, repoUrl: string, repoPath: string) {
async createApp(appName: string, repoUrl: string, repoPath: string) {
await this.newAppButton.click();

//handle the "failed to load data" banner if it appears inside the slide-out panel
const errorBanner = this.page.getByText('try again');
try {
//wait 3 secs
await errorBanner.waitFor({ state: 'visible', timeout: 3000 });
await errorBanner.click();
} catch (error) {
//banner didn't appear so just continue
}

await this.page.getByText('Loading...').first().waitFor({ state: 'hidden', timeout: 15000 });

await this.appNameInput.fill(appName);
Expand All @@ -82,31 +93,33 @@ export class ApplicationsPage {
await this.createButton.click();
}

async syncApplication(appName: string, expectedResource: string = 'spring-petclinic') {
async syncApplication(appName: string, expectedResource: string = 'spring-petclinic') {
//search for app
await this.page.getByPlaceholder(/Search applications/i).fill(appName);

const appContainer = this.page.locator('.white-box, .argo-table-list__row').filter({ hasText: appName });
await appContainer.waitFor({ state: 'visible', timeout: 20000 });
await expect(appContainer.getByText(/OutOfSync|Out of Sync/i).first()).toBeVisible({ timeout: 45000 });
//safe to open the panel
await appContainer.getByText('Sync', { exact: true }).click();

//slideout panel
// Wait for the manifests to fetch from Git and render on the panel
await expect(this.page.getByText(expectedResource).first()).toBeVisible({ timeout: 15000 });

//click 'all' to ensure all resource checkboxes are ticked across all Argo CD versions
//click 'all'
const allLink = this.page.getByRole('link', { name: 'all', exact: true });
try {
await allLink.waitFor({ state: 'visible', timeout: 3000 });
await allLink.waitFor({ state: 'visible', timeout: 5000 });
await allLink.click();
} catch (error) {
//all link didn't appear within 3 sec
// all link didn't appear within 5 sec
}

//wait for the manifests to render on the panel
await expect(this.page.getByText(expectedResource).first()).toBeVisible({ timeout: 30000 });

//click the main sync button
await this.page.getByRole('button', { name: /^synchronize$/i }).first().click();
Comment on lines 107 to 119

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, explore the file structure and locate it
find . -type f -name "ApplicationsPage.ts" 2>/dev/null

# Get an outline of the file to understand its structure
ast-grep outline test/ui-e2e/src/pages/ApplicationsPage.ts --view expanded | head -100

Repository: redhat-developer/gitops-operator

Length of output: 1011


🏁 Script executed:

#!/bin/bash
# Read the specific section in context (lines 95-130 to see surrounding code)
sed -n '95,130p' test/ui-e2e/src/pages/ApplicationsPage.ts

Repository: redhat-developer/gitops-operator

Length of output: 1716


🏁 Script executed:

#!/bin/bash
# Search for "synchronize panel" or similar references to understand the panel structure
rg -n "panel|modal|dialog|synchronize" test/ui-e2e/src/pages/ApplicationsPage.ts -A 2 -B 2

Repository: redhat-developer/gitops-operator

Length of output: 1141


🏁 Script executed:

#!/bin/bash
# Look for how other methods scope their selectors in this file
rg -n "getByRole|locator|within|filter" test/ui-e2e/src/pages/ApplicationsPage.ts | head -30

Repository: redhat-developer/gitops-operator

Length of output: 2152


Scope selectors for the 'all' link, expectedResource text, and synchronize button to the sync panel.

Lines 107-119 use page-wide selectors (this.page.getByRole(), this.page.getByText()) that can match unintended elements in background table content. This contradicts the scoping pattern established earlier in the method (line 100-104, where appContainer is used). After opening the sync panel with appContainer.getByText('Sync'), the subsequent operations should be scoped to that panel context rather than querying the entire page, which can introduce flaky behavior and false positives.

🤖 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 `@test/ui-e2e/src/pages/ApplicationsPage.ts` around lines 107 - 119, Scope the
selectors in the sync panel operations to prevent matching unintended elements.
Instead of using page-wide selectors (`this.page.getByRole()` and
`this.page.getByText()`), scope the allLink selector at line 107, the
expectedResource text selector at line 115, and the synchronize button selector
at line 118 to the sync panel context that was opened earlier with
`appContainer.getByText('Sync')`. Store the sync panel locator and use it to
scope all three subsequent selectors to that specific panel rather than querying
the entire page.


//wait for the panel to close
await expect(this.page.getByText('SYNCHRONIZE RESOURCES')).toBeHidden({ timeout: 10000 });
//wait for the panel to close
await expect(this.page.getByText('SYNCHRONIZE RESOURCES')).toBeHidden({ timeout: 15000 });
}

async verifyStatus(appName: string) {
Expand All @@ -118,4 +131,20 @@ async syncApplication(appName: string, expectedResource: string = 'spring-petcli
await expect(appContainer.getByText(/synced/i)).toBeVisible({ timeout: 90000 });
await expect(appContainer.getByText(/healthy/i)).toBeVisible({ timeout: 90000 });
}

async openApplication(appName: string) {
//re-apply search filter just in case the UI refreshed
await this.page.getByPlaceholder(/Search applications/i).fill(appName);

//find the container, then specifically click the link of the app name
const appLink = this.page.locator('.white-box, .argo-table-list__row')
.filter({ hasText: appName })
.getByRole('link', { name: appName });

Comment on lines +140 to +143

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, locate the file in question
find . -type f -name "ApplicationsPage.ts" 2>/dev/null | head -20

Repository: redhat-developer/gitops-operator

Length of output: 214


🏁 Script executed:

#!/bin/bash
# If found, examine the file structure and content around lines 140-143
if [ -f "test/ui-e2e/src/pages/ApplicationsPage.ts" ]; then
  echo "=== File exists, examining structure ==="
  wc -l test/ui-e2e/src/pages/ApplicationsPage.ts
  echo ""
  echo "=== AST outline to understand context ==="
  ast-grep outline test/ui-e2e/src/pages/ApplicationsPage.ts --view expanded | head -100
fi

Repository: redhat-developer/gitops-operator

Length of output: 1097


🏁 Script executed:

#!/bin/bash
# Read the specific lines with context
if [ -f "test/ui-e2e/src/pages/ApplicationsPage.ts" ]; then
  echo "=== Lines 130-160 with context ==="
  sed -n '130,160p' test/ui-e2e/src/pages/ApplicationsPage.ts | cat -n
fi

Repository: redhat-developer/gitops-operator

Length of output: 1301


🏁 Script executed:

#!/bin/bash
# Check for similar patterns with getByRole in test files
rg "getByRole.*name:" test/ui-e2e --type typescript -A 1 -B 1 | head -40

Repository: redhat-developer/gitops-operator

Length of output: 205


🏁 Script executed:

#!/bin/bash
# Check if there are test fixtures with similarly-named apps
rg "petclinic|app.*name|createApp" test/ui-e2e --type typescript | grep -E "(test|fixture|data|const)" | head -20

Repository: redhat-developer/gitops-operator

Length of output: 205


🏁 Script executed:

#!/bin/bash
# Check for similar patterns with getByRole in test files (using ts type)
rg "getByRole.*name:" test/ui-e2e -A 1 -B 1 | head -40

Repository: redhat-developer/gitops-operator

Length of output: 2988


🏁 Script executed:

#!/bin/bash
# Check test data and app creation patterns
rg "createApp|spring-petclinic|appName" test/ui-e2e/src -A 2 -B 1 | head -50

Repository: redhat-developer/gitops-operator

Length of output: 3873


🏁 Script executed:

#!/bin/bash
# Check if syncApplication and verifyStatus have similar patterns
sed -n '96,130p' test/ui-e2e/src/pages/ApplicationsPage.ts | cat -n

Repository: redhat-developer/gitops-operator

Length of output: 1960


Use exact app-link matching in openApplication.

hasText: appName combined with non-exact getByRole('link', { name: appName }) can match similarly-named apps (e.g., petclinic vs petclinic-dev), causing the wrong application to be opened. Resolve by using exact: true on both the link selector and the row filter—this pattern is already used elsewhere in the file (e.g., line 13 for the Create button).

Suggested fix
-    const appLink = this.page.locator('.white-box, .argo-table-list__row')
-                             .filter({ hasText: appName })
-                             .getByRole('link', { name: appName });
+    const appLink = this.page.locator('.white-box, .argo-table-list__row')
+                             .filter({ has: this.page.getByRole('link', { name: appName, exact: true }) })
+                             .getByRole('link', { name: appName, exact: true });
📝 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
const appLink = this.page.locator('.white-box, .argo-table-list__row')
.filter({ hasText: appName })
.getByRole('link', { name: appName });
const appLink = this.page.locator('.white-box, .argo-table-list__row')
.filter({ has: this.page.getByRole('link', { name: appName, exact: true }) })
.getByRole('link', { name: appName, exact: true });
🤖 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 `@test/ui-e2e/src/pages/ApplicationsPage.ts` around lines 140 - 143, The
appLink selector in the openApplication method uses non-exact matching on both
the filter and getByRole calls, which can incorrectly match similarly-named
applications (e.g., petclinic vs petclinic-dev). Add exact: true to both the
hasText filter and the getByRole link selector to ensure precise matching. This
pattern is already demonstrated elsewhere in the file at line 13 for the Create
button selector.

await appLink.waitFor({ state: 'visible', timeout: 15000 });
await appLink.click();

//wait for the URL to change to the details page to ensure the click worked
await expect(this.page).toHaveURL(/.*\/applications\/.*\/.*/, { timeout: 15000 });
}
}
5 changes: 3 additions & 2 deletions test/ui-e2e/src/pages/LoginPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ export class LoginPage {
}

//check if manual login is actually required
const usernameInput = this.page.getByLabel(/Username/i)
const usernameInput = this.page.getByRole('textbox', { name: /Username/i })
.or(this.page.locator('input[name="username"]'))
.or(this.page.getByPlaceholder(/Username/i));
.or(this.page.getByPlaceholder(/Username/i))
.first();

const needsLogin = await usernameInput.waitFor({ state: 'visible', timeout: 5000 }).then(() => true).catch(() => false);

Expand Down
4 changes: 2 additions & 2 deletions test/ui-e2e/tests/admin-login.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ test('Log into Argo CD as local admin', async ({ browser }) => {
{ timeout: 15000, stdio: 'pipe' }
).toString();
} catch (error) {
throw new Error("Failed to extract admin password. Please check your cluster connection and oc CLI.");
throw new Error("Failed to extract admin password. Please check your cluster connection and oc CLI.", { cause: error });
}

//get credentials
Expand All @@ -27,7 +27,7 @@ test('Log into Argo CD as local admin', async ({ browser }) => {
{ timeout: 15000, stdio: 'pipe' }
).toString().trim();
} catch (error) {
throw new Error("Failed to fetch Argo CD route. Please check your cluster connection and oc CLI.");
throw new Error("Failed to fetch Argo CD route. Please check your cluster connection and oc CLI.", { cause: error });
}

//Fresh context to avoid any cached state issues
Expand Down
29 changes: 29 additions & 0 deletions test/ui-e2e/tests/resource-tree.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { test, expect } from '../src/fixtures';
import { ApplicationDetailsPage } from '../src/pages/ApplicationDetailsPage';
import { ApplicationsPage } from '../src/pages/ApplicationsPage';

test.describe('Argo CD Resource Tree and Pod Logs', () => {

test.use({ storageState: '.auth/storageState.json' });

test('Navigate to app details, open a Pod, and verify logs stream', async ({ page, managedApp }) => {
test.setTimeout(120000);

const appsPage = new ApplicationsPage(page);
const detailsPage = new ApplicationDetailsPage(page);

await appsPage.navigate();
await page.getByPlaceholder(/Search applications/i).fill(managedApp);

//click the Application Name text/link
const appCard = page.locator('.white-box, .argo-table-list__row').filter({ hasText: managedApp });
await appCard.getByText(managedApp, { exact: true }).first().click();

//on details page
await detailsPage.verifyResourceTreeLoaded();
//Deployment node
await detailsPage.clickResourceNode('deploy', 'spring-petclinic');
await detailsPage.verifyPodLogs();
});

});
Loading