Skip to content

Commit 9a96835

Browse files
Triona DoyleTriona Doyle
authored andcommitted
Add Application Test
Signed-off-by: Triona Doyle <bot@example.com> Signed-off-by: Triona Doyle <tekton@example.com>
1 parent 69485ba commit 9a96835

7 files changed

Lines changed: 218 additions & 28 deletions

File tree

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

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { test as setup } from '@playwright/test';
1+
import { test as setup, expect } from '@playwright/test';
22

33
const authFile = '.auth/storageState.json';
44

55
setup('authenticate to OpenShift Cluster', async ({ page, baseURL }) => {
6-
// Navigate to the OpenShift Console
6+
//navigate to the OpenShift console
77
const targetUrl = baseURL || process.env.CONSOLE_URL || process.env.BASE_URL;
88

99
if (!targetUrl) {
@@ -19,23 +19,19 @@ setup('authenticate to OpenShift Cluster', async ({ page, baseURL }) => {
1919
.or(page.locator('input[name="username"]'))
2020
.or(page.getByPlaceholder(/Username/i));
2121

22-
//wait for the IDP screen OR the Username field to appear
23-
try {
24-
await Promise.race([
25-
idpScreenText.waitFor({ state: 'visible', timeout: 15000 }),
26-
usernameInput.waitFor({ state: 'visible', timeout: 15000 })
27-
]);
28-
} catch (e) {
29-
console.log("Timed out waiting for OpenShift login page to render.");
30-
}
22+
//fail loudly if the page is dead so we don't get weird errors later
23+
await expect(
24+
idpScreenText.or(usernameInput).first(),
25+
"OpenShift login page failed to load. Check cluster health and URL."
26+
).toBeVisible({ timeout: 20000 });
3127

3228
const idpName = process.env.IDP || 'kube:admin';
3329
const user = process.env.CLUSTER_USER || 'kubeadmin';
3430

3531
if (await idpScreenText.isVisible()) {
3632
console.log(`IDP selection screen detected. Selecting provider: "${idpName}"`);
3733

38-
// look for the specific IDP
34+
//look for the specific IDP
3935
const idpLink = page.getByRole('link', { name: new RegExp(idpName, 'i') });
4036

4137
await idpLink.waitFor({ state: 'visible', timeout: 5000 });
@@ -44,7 +40,7 @@ setup('authenticate to OpenShift Cluster', async ({ page, baseURL }) => {
4440
console.log("No IDP screen detected (or already selected), proceeding to credentials...");
4541
}
4642

47-
// fill in the Credentials
43+
//fill in the credentials
4844
await usernameInput.waitFor({ state: 'visible', timeout: 10000 });
4945
await usernameInput.fill(user);
5046

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

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,23 @@ export CLUSTER_USER=${CLUSTER_USER:-"kubeadmin"}
1515
export IDP=${IDP:-"kube:admin"}
1616

1717
#check auth state first
18-
echo "Checking cluster authentication..."
19-
if ! oc whoami > /dev/null 2>&1; then
20-
if [ -n "$OC_API_URL" ] && [ -n "$CLUSTER_PASSWORD" ]; then
21-
echo "Attempting automated login..."
22-
oc login "$OC_API_URL" -u "$CLUSTER_USER" -p "$CLUSTER_PASSWORD" --insecure-skip-tls-verify=true
23-
else
24-
echo "Error: Not logged in. Missing OC_API_URL or CLUSTER_PASSWORD."
18+
echo "Syncing CLI context..."
19+
if [ -n "$OC_API_URL" ] && [ -n "$CLUSTER_PASSWORD" ]; then
20+
# If variables exist, FORCE the CLI to match them so there is no cross-cluster confusion
21+
echo "Logging into $OC_API_URL..."
22+
oc login "$OC_API_URL" -u "$CLUSTER_USER" -p "$CLUSTER_PASSWORD" --insecure-skip-tls-verify=true > /dev/null 2>&1
23+
24+
if [ $? -ne 0 ]; then
25+
echo "Error: Failed to log into the cluster. Please check the credentials in your .env file."
2526
exit 1
2627
fi
28+
elif ! oc whoami > /dev/null 2>&1; then
29+
# If variables don't exist AND we aren't logged in, fail out
30+
echo "Error: Not logged in. Missing OC_API_URL or CLUSTER_PASSWORD."
31+
exit 1
32+
else
33+
# If variables don't exist but we ARE logged in locally, just use the current session
34+
echo "No .env credentials found. Using existing oc CLI session..."
2735
fi
2836

2937
#find the URLs for console and argocd

test/ui-e2e/src/fixtures.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { test as base, expect } from '@playwright/test';
2+
import { LoginPage } from './pages/LoginPage';
3+
4+
export const test = base.extend({
5+
page: async ({ page }, use) => {
6+
const loginPage = new LoginPage(page);
7+
await loginPage.goto();
8+
await loginPage.loginViaOpenShift();
9+
await use(page);
10+
},
11+
});
12+
13+
//export it so spec files can use it
14+
export { expect };
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { Page, expect, Locator } from '@playwright/test';
2+
3+
export class ApplicationsPage {
4+
readonly page: Page;
5+
readonly newAppButton: Locator;
6+
readonly appNameInput: Locator;
7+
readonly projectInput: Locator;
8+
readonly repoUrlInput: Locator;
9+
readonly pathInput: Locator;
10+
readonly clusterUrlInput: Locator;
11+
readonly namespaceInput: Locator;
12+
readonly createButton: Locator;
13+
14+
constructor(page: Page) {
15+
this.page = page;
16+
17+
//header buttons
18+
this.newAppButton = page.getByRole('button', { name: /NEW APP/i });
19+
this.createButton = page.getByRole('button', { name: 'Create', exact: true });
20+
21+
this.appNameInput = page.getByLabel('Application Name', { exact: true });
22+
this.projectInput = page.locator('[qe-id="application-create-field-project"]');
23+
this.repoUrlInput = page.locator('.argo-form-row').filter({ hasText: 'Repository URL' }).locator('input').first();
24+
this.pathInput = page.locator('.argo-form-row').filter({ hasText: 'Path' }).locator('input').first();
25+
26+
//dest
27+
this.clusterUrlInput = page.locator('.argo-form-row').filter({ hasText: 'Cluster URL' }).locator('input').first();
28+
this.namespaceInput = page.locator('.argo-form-row')
29+
.filter({ has: page.getByText('Namespace', { exact: true }) })
30+
.locator('input').first();
31+
}
32+
33+
async navigate() {
34+
await this.page.goto('/applications');
35+
36+
//ingnore the "failed to load data" banner if it appears
37+
const errorBanner = this.page.getByText('try again');
38+
try {
39+
//wait 3 secs
40+
await errorBanner.waitFor({ state: 'visible', timeout: 3000 });
41+
await errorBanner.click();
42+
} catch (error) {
43+
//banner didn't appear so just continue
44+
}
45+
46+
await expect(this.newAppButton).toBeVisible({ timeout: 15000 });
47+
}
48+
49+
//helper for fields that need to have select a pre existing option
50+
async fillDropdown(locator: Locator, value: string) {
51+
await locator.click();
52+
await locator.pressSequentially(value, { delay: 50 });
53+
await this.page.waitForTimeout(500);
54+
await locator.press('Enter');
55+
}
56+
57+
async createApp(appName: string, repoUrl: string, repoPath: string) {
58+
await this.newAppButton.click();
59+
await this.page.getByText('Loading...').first().waitFor({ state: 'hidden', timeout: 15000 });
60+
61+
await this.appNameInput.fill(appName);
62+
await this.fillDropdown(this.projectInput, 'default');
63+
64+
//src
65+
await this.repoUrlInput.fill(repoUrl);
66+
await this.pathInput.fill(repoPath);
67+
68+
//dest
69+
await this.clusterUrlInput.fill('https://kubernetes.default.svc');
70+
71+
//deploy
72+
await this.namespaceInput.fill('openshift-gitops');
73+
await this.createButton.click();
74+
}
75+
76+
async syncApplication(appName: string) {
77+
//search for app
78+
await this.page.getByPlaceholder(/Search applications/i).fill(appName);
79+
80+
const appContainer = this.page.locator('.white-box, .argo-table-list__row').filter({ hasText: appName });
81+
await appContainer.waitFor({ state: 'visible', timeout: 20000 });
82+
await appContainer.getByText('Sync', { exact: true }).click();
83+
84+
//slideout panel
85+
const resourcesSection = this.page.locator('.argo-form-row').filter({ hasText: 'SYNCHRONIZE RESOURCES' });
86+
await expect(resourcesSection).toContainText('spring-petclinic', { timeout: 15000 });
87+
88+
//click all
89+
await resourcesSection.getByText('all', { exact: true }).click();
90+
91+
//handle the validation error that appears in older versions
92+
const validationWarning = resourcesSection.getByText('Select at least one resource');
93+
await this.page.waitForTimeout(500);
94+
95+
//if the red text exists (4.14) wait for it to vanish so means boxes are checked
96+
if (await validationWarning.isVisible()) {
97+
await validationWarning.waitFor({ state: 'hidden', timeout: 10000 });
98+
}
99+
100+
//click
101+
await this.page.getByRole('button', { name: /^synchronize$/i }).click();
102+
}
103+
104+
async verifyStatus(appName: string) {
105+
//re-apply search filter just in case
106+
await this.page.getByPlaceholder(/Search applications/i).fill(appName);
107+
const appContainer = this.page.locator('.white-box, .argo-table-list__row').filter({ hasText: appName });
108+
109+
//90 secs
110+
await expect(appContainer.getByText(/synced/i)).toBeVisible({ timeout: 90000 });
111+
await expect(appContainer.getByText(/healthy/i)).toBeVisible({ timeout: 90000 });
112+
}
113+
}

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

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,19 @@ export class LoginPage {
1212
await this.page.goto('/');
1313
}
1414

15-
async loginViaOpenShift(user: string, pass: string, idp: string = 'kube:admin') {
16-
//click the SSO button on the Argo CD landing page
15+
async loginViaOpenShift(user?: string, pass?: string, idp: string = 'kube:admin') {
1716
const ssoButton = this.page.getByText(/LOG IN VIA OPENSHIFT/i);
18-
await ssoButton.waitFor({ state: 'visible', timeout: 10000 });
17+
const newAppButton = this.page.getByRole('button', { name: /NEW APP/i });
18+
19+
//wait dynamically for either the login screen OR the dashboard to render
20+
await ssoButton.or(newAppButton).first().waitFor({ state: 'visible', timeout: 20000 });
21+
22+
//if we landed straight on the dashboard, the cluster was already fully authenticated
23+
if (await newAppButton.isVisible()) {
24+
return;
25+
}
26+
27+
//otherwise, click the SSO button on the Argo CD landing page
1928
await ssoButton.click();
2029

2130
//handle the OpenShift IDP selection screen if it appears
@@ -27,11 +36,16 @@ export class LoginPage {
2736
//if it's not there then OpenShift likely defaulted to another
2837
}
2938

30-
//fil out the OpenShift credentials
31-
await this.page.getByLabel(/Username/i).waitFor({ state: 'visible' });
32-
await this.page.getByLabel(/Username/i).fill(user);
33-
await this.page.getByLabel(/Password/i).fill(pass);
34-
await this.page.getByRole('button', { name: /Log in/i }).click();
39+
//check if manual login is actually required
40+
const usernameInput = this.page.getByLabel(/Username/i);
41+
const needsLogin = await usernameInput.waitFor({ state: 'visible', timeout: 5000 }).then(() => true).catch(() => false);
42+
43+
if (needsLogin && user && pass) {
44+
//fil out the OpenShift credentials
45+
await usernameInput.fill(user);
46+
await this.page.getByLabel(/Password/i).fill(pass);
47+
await this.page.getByRole('button', { name: /Log in/i }).click();
48+
}
3549

3650
//Auth Handle the Allow Permissions screen
3751
try {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { test, expect } from '../src/fixtures';
2+
import { ApplicationsPage } from '../src/pages/ApplicationsPage';
3+
4+
test.describe('ArgoCD Create Application', () => {
5+
//declare appname
6+
let appName: string;
7+
8+
test.afterEach(async ({ page }) => {
9+
if (!appName) return;
10+
11+
console.log(`cleaning up: deleting ${appName} via api`);
12+
13+
const response = await page.request.delete(`/api/v1/applications/${appName}?cascade=true`, {
14+
headers: { 'Content-Type': 'application/json' }
15+
});
16+
17+
//ignore 404 (already gone) or 403 (no permission on this cluster)
18+
if (response.status() === 404 || response.status() === 403) {
19+
if (response.status() === 403) console.log('warning: rbac bypass on this cluster');
20+
return;
21+
}
22+
23+
//only fail for actual server errors
24+
expect(response.status()).toBeLessThan(400);
25+
});
26+
27+
test('Deploy the Spring Petclinic application via UI', async ({ page }) => {
28+
test.setTimeout(180000);
29+
30+
const appsPage = new ApplicationsPage(page);
31+
appName = `spring-petclinic-${Date.now()}`;
32+
const publicRepo = 'https://github.com/redhat-developer/openshift-gitops-getting-started.git';
33+
const repoPath = 'app';
34+
35+
await appsPage.navigate();
36+
await appsPage.createApp(appName, publicRepo, repoPath);
37+
await appsPage.syncApplication(appName);
38+
await appsPage.verifyStatus(appName);
39+
});
40+
41+
});

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import { LoginPage } from '../src/pages/LoginPage';
33

44
test.describe('Argo CD SSO Authentication', () => {
55

6+
//give the manual login flow plenty of time to finish
7+
test.setTimeout(60000);
8+
69
//clear storageState to force a full login flow for this specific test
710
test.use({ storageState: { cookies: [], origins: [] } });
811

@@ -20,4 +23,5 @@ test.describe('Argo CD SSO Authentication', () => {
2023
//Check the button is visible as proof of successful login
2124
await expect(page.getByRole('button', { name: /NEW APP/i })).toBeVisible({ timeout: 15000 });
2225
});
26+
2327
});

0 commit comments

Comments
 (0)