Skip to content

Commit 5d99606

Browse files
author
Triona Doyle
committed
test: add UI E2E tests for Argo CD Resource Tree and Pod logs
1 parent ca8a012 commit 5d99606

8 files changed

Lines changed: 194 additions & 27 deletions

File tree

test/ui-e2e/.auth/setup.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,20 @@ setup('authenticate to OpenShift Cluster', async ({ page, baseURL }) => {
5555
await passwordInput.fill(process.env.CLUSTER_PASSWORD);
5656
await page.getByRole('button', { name: /Log in/i }).click();
5757

58+
// Handle the OpenShift 4.x Welcome Tour modal if it appears
59+
try {
60+
const skipTourButton = page.getByRole('button', { name: /skip tour/i });
61+
// Wait up to 5 seconds for the modal to pop up
62+
await skipTourButton.waitFor({ state: 'visible', timeout: 5000 });
63+
await skipTourButton.click();
64+
console.log('Dismissed the OpenShift Welcome Tour modal.');
65+
} catch (error) {
66+
// If it doesn't appear within 5 seconds, it's an older cluster or already dismissed.
67+
// Safely ignore the error and move on
68+
}
69+
5870
// Save the auth state
59-
await expect(page.getByRole('navigation').first()).toBeVisible({ timeout: 15000 });
71+
await expect(page.getByRole('navigation').first()).toBeVisible({ timeout: 20000 });
6072
await expect(page).toHaveURL(/(console|k8s|overview|dashboards)/i, { timeout: 15000 });
6173
await page.context().storageState({ path: authFile });
62-
6374
});

test/ui-e2e/README.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,10 @@ All executions are driven via the ./run-ui-tests.sh wrapper script. This wrapper
5656

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

6364
### Playwright Flags Reference
6465

@@ -67,6 +68,7 @@ All executions are driven via the ./run-ui-tests.sh wrapper script. This wrapper
6768
| `--headed` | Launches the visible Chromium browser UI. Excellent for local debugging. |
6869
| `--trace on` | Records a granular execution trace (DOM snapshots, network calls, actions) for visual triage. |
6970
| `--reporter=list` | Switches stdout to a clean line-by-line format, ideal for monitoring real-time execution steps. |
71+
| `--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. |
7072

7173
### Visual Debugging (Trace Viewer)
7274

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

test/ui-e2e/run-ui-tests.sh

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,28 @@
11
#!/bin/bash
22

3+
# use arguments to extract --env and keep the rest for Playwright
4+
ENV="local"
5+
TEST_ARGS=()
6+
7+
while [[ "$#" -gt 0 ]]; do
8+
case $1 in
9+
--env=*) ENV="${1#*=}" ;;
10+
*) TEST_ARGS+=("$1") ;; # Save all other args (files, --headed, etc.)
11+
esac
12+
shift
13+
done
14+
315
if [ -f .env ]; then
416
echo "Loading variables from .env file..."
517
set -a #export all variables
618
source .env
7-
set +a # stop automatically exporting
19+
set +a #stop auto export
820
fi
921

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

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

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

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

5466
#run Playwright
5567
echo " Starting Playwright tests..."
56-
npx playwright test "$@"
68+
69+
# 2. Execute based on the environment
70+
if [[ "$ENV" == "ci" ]] || [[ "$ENV" == "pipeline" ]]; then
71+
echo "Running headlessly in automation ($ENV)..."
72+
npm ci
73+
74+
# Prevent sudo jump-scares for local Mac users simulating CI
75+
if [[ "$(uname -s)" == "Darwin" ]]; then
76+
npx playwright install chromium
77+
else
78+
npx playwright install chromium --with-deps
79+
fi
80+
81+
npx playwright test "${TEST_ARGS[@]}" --reporter=list
82+
83+
else
84+
echo "Running Locally..."
85+
npx playwright test "${TEST_ARGS[@]}"
86+
fi
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { Page, expect, Locator } from '@playwright/test';
2+
3+
export class ApplicationDetailsPage {
4+
readonly page: Page;
5+
readonly resourceTreeContainer: Locator;
6+
readonly slideOutPanel: Locator;
7+
readonly logsTab: Locator;
8+
9+
constructor(page: Page) {
10+
this.page = page;
11+
12+
//main container
13+
this.resourceTreeContainer = page.locator('.application-details__tree');
14+
15+
//details panel that slides out (isolate the active visible pane)
16+
this.slideOutPanel = page.locator('.sliding-panel').filter({ visible: true });
17+
18+
//logs tab inside the slide-out panel
19+
this.logsTab = this.slideOutPanel.getByRole('button', { name: /logs/i }).or(this.slideOutPanel.getByText(/logs/i, { exact: true }));
20+
}
21+
22+
async verifyResourceTreeLoaded() {
23+
//wait tree to be visible
24+
await expect(this.resourceTreeContainer).toBeVisible({ timeout: 20000 });
25+
//wait for healthy status
26+
await expect(this.page.getByText('Healthy', { exact: true }).first()).toBeVisible({ timeout: 30000 });
27+
}
28+
29+
async clickResourceNode(kind: string, name: string) {
30+
//find the innermost div representing the resource node
31+
const node = this.resourceTreeContainer
32+
.locator('div')
33+
.filter({ hasText: kind })
34+
.filter({ hasText: name })
35+
.last();
36+
37+
//scroll it into view and click it
38+
await node.scrollIntoViewIfNeeded();
39+
await node.waitFor({ state: 'visible', timeout: 15000 });
40+
await node.click();
41+
42+
//self-healing validation block to handle frontend rendering lag
43+
await expect(async () => {
44+
await expect(this.slideOutPanel).toBeVisible({ timeout: 2000 });
45+
}).toPass({ timeout: 10000 });
46+
}
47+
48+
async verifyPodLogs(expectedLogText?: string) {
49+
//click Logs
50+
await this.logsTab.waitFor({ state: 'visible', timeout: 5000 });
51+
await this.logsTab.click();
52+
53+
const logFilterInput = this.slideOutPanel.getByPlaceholder('containing');
54+
await expect(logFilterInput).toBeVisible({ timeout: 15000 });
55+
56+
if (expectedLogText) {
57+
//find log line anywhere in the slide-out panel
58+
await expect(this.slideOutPanel).toContainText(expectedLogText, { timeout: 30000 });
59+
} else {
60+
const genericLogLine = this.slideOutPanel.getByText(/\d{4}-\d{2}-\d{2}.*(INFO|Started)/).first();
61+
await expect(genericLogLine).toBeVisible({ timeout: 30000 });
62+
}
63+
}
64+
}

test/ui-e2e/src/pages/ApplicationsPage.ts

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,19 @@ export class ApplicationsPage {
6363
await locator.press('Enter');
6464
}
6565

66-
async createApp(appName: string, repoUrl: string, repoPath: string) {
66+
async createApp(appName: string, repoUrl: string, repoPath: string) {
6767
await this.newAppButton.click();
68+
69+
//handle the "failed to load data" banner if it appears inside the slide-out panel
70+
const errorBanner = this.page.getByText('try again');
71+
try {
72+
//wait 3 secs
73+
await errorBanner.waitFor({ state: 'visible', timeout: 3000 });
74+
await errorBanner.click();
75+
} catch (error) {
76+
//banner didn't appear so just continue
77+
}
78+
6879
await this.page.getByText('Loading...').first().waitFor({ state: 'hidden', timeout: 15000 });
6980

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

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

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

93-
//slideout panel
94-
// Wait for the manifests to fetch from Git and render on the panel
95-
await expect(this.page.getByText(expectedResource).first()).toBeVisible({ timeout: 15000 });
96-
97-
//click 'all' to ensure all resource checkboxes are ticked across all Argo CD versions
106+
//click 'all'
98107
const allLink = this.page.getByRole('link', { name: 'all', exact: true });
99108
try {
100-
await allLink.waitFor({ state: 'visible', timeout: 3000 });
109+
await allLink.waitFor({ state: 'visible', timeout: 5000 });
101110
await allLink.click();
102111
} catch (error) {
103-
//all link didn't appear within 3 sec
112+
// all link didn't appear within 5 sec
104113
}
114+
115+
//wait for the manifests to render on the panel
116+
await expect(this.page.getByText(expectedResource).first()).toBeVisible({ timeout: 30000 });
117+
105118
//click the main sync button
106119
await this.page.getByRole('button', { name: /^synchronize$/i }).first().click();
107120

108-
//wait for the panel to close
109-
await expect(this.page.getByText('SYNCHRONIZE RESOURCES')).toBeHidden({ timeout: 10000 });
121+
//wait for the panel to close
122+
await expect(this.page.getByText('SYNCHRONIZE RESOURCES')).toBeHidden({ timeout: 15000 });
110123
}
111124

112125
async verifyStatus(appName: string) {
@@ -118,4 +131,20 @@ async syncApplication(appName: string, expectedResource: string = 'spring-petcli
118131
await expect(appContainer.getByText(/synced/i)).toBeVisible({ timeout: 90000 });
119132
await expect(appContainer.getByText(/healthy/i)).toBeVisible({ timeout: 90000 });
120133
}
134+
135+
async openApplication(appName: string) {
136+
//re-apply search filter just in case the UI refreshed
137+
await this.page.getByPlaceholder(/Search applications/i).fill(appName);
138+
139+
//find the container, then specifically click the link of the app name
140+
const appLink = this.page.locator('.white-box, .argo-table-list__row')
141+
.filter({ hasText: appName })
142+
.getByRole('link', { name: appName });
143+
144+
await appLink.waitFor({ state: 'visible', timeout: 15000 });
145+
await appLink.click();
146+
147+
//wait for the URL to change to the details page to ensure the click worked
148+
await expect(this.page).toHaveURL(/.*\/applications\/.*\/.*/, { timeout: 15000 });
149+
}
121150
}

test/ui-e2e/src/pages/LoginPage.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,10 @@ export class LoginPage {
3737
}
3838

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

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

test/ui-e2e/tests/admin-login.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ test('Log into Argo CD as local admin', async ({ browser }) => {
1111
{ timeout: 15000, stdio: 'pipe' }
1212
).toString();
1313
} catch (error) {
14-
throw new Error("Failed to extract admin password. Please check your cluster connection and oc CLI.");
14+
throw new Error("Failed to extract admin password. Please check your cluster connection and oc CLI.", { cause: error });
1515
}
1616

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

3333
//Fresh context to avoid any cached state issues
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { test, expect } from '../src/fixtures';
2+
import { ApplicationDetailsPage } from '../src/pages/ApplicationDetailsPage';
3+
import { ApplicationsPage } from '../src/pages/ApplicationsPage';
4+
5+
test.describe('Argo CD Resource Tree and Pod Logs', () => {
6+
7+
test.use({ storageState: '.auth/storageState.json' });
8+
9+
test('Navigate to app details, open a Pod, and verify logs stream', async ({ page, managedApp }) => {
10+
test.setTimeout(120000);
11+
12+
const appsPage = new ApplicationsPage(page);
13+
const detailsPage = new ApplicationDetailsPage(page);
14+
15+
await appsPage.navigate();
16+
await page.getByPlaceholder(/Search applications/i).fill(managedApp);
17+
18+
//click the Application Name text/link
19+
const appCard = page.locator('.white-box, .argo-table-list__row').filter({ hasText: managedApp });
20+
await appCard.getByText(managedApp, { exact: true }).first().click();
21+
22+
//on details page
23+
await detailsPage.verifyResourceTreeLoaded();
24+
//Deployment node
25+
await detailsPage.clickResourceNode('deploy', 'spring-petclinic');
26+
await detailsPage.verifyPodLogs();
27+
});
28+
29+
});

0 commit comments

Comments
 (0)