Skip to content

Commit 5e7ca0c

Browse files
author
Triona Doyle
committed
test updates and address coderabbit feedback
Signed-off-by: Triona Doyle <tekton@example.com>
1 parent 9ac9377 commit 5e7ca0c

5 files changed

Lines changed: 136 additions & 68 deletions

File tree

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

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ import { test as setup, expect } from '@playwright/test';
22

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

5+
//centralized timeouts to appease the linter
6+
const TIMEOUTS = { short: 5000, medium: 10000, default: 15000, long: 20000 };
7+
58
setup('authenticate to OpenShift Cluster', async ({ page, baseURL }) => {
6-
// Navigate to the OpenShift Console
9+
//navigate to the OpenShift Console
710
const targetUrl = baseURL || process.env.CONSOLE_URL || process.env.BASE_URL;
811

912
if (!targetUrl) {
@@ -13,35 +16,35 @@ setup('authenticate to OpenShift Cluster', async ({ page, baseURL }) => {
1316
console.log(`Navigating to OpenShift Console: ${targetUrl}`);
1417
await page.goto(targetUrl);
1518

16-
// Set locators
19+
//set locators
1720
const idpScreenText = page.getByText(/Log in with/i);
1821
const usernameInput = page.getByLabel(/Username/i)
1922
.or(page.locator('input[name="username"]'))
2023
.or(page.getByPlaceholder(/Username/i));
2124

22-
// Fail loudly if the page is dead so we don't get weird errors later
25+
//fail loudly if the page is dead so we don't get weird errors later
2326
await expect(
2427
idpScreenText.or(usernameInput).first(),
2528
"OpenShift login page failed to load. Check cluster health and URL."
26-
).toBeVisible({ timeout: 20000 });
29+
).toBeVisible({ timeout: TIMEOUTS.long });
2730

2831
const idpName = process.env.IDP || 'kube:admin';
2932
const user = process.env.CLUSTER_USER || 'kubeadmin';
3033

3134
if (await idpScreenText.isVisible()) {
3235
console.log(`IDP selection screen detected. Selecting provider: "${idpName}"`);
3336

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

37-
await idpLink.waitFor({ state: 'visible', timeout: 5000 });
40+
await idpLink.waitFor({ state: 'visible', timeout: TIMEOUTS.short });
3841
await idpLink.click();
3942
} else {
4043
console.log("No IDP screen detected (or already selected), proceeding to credentials...");
4144
}
4245

43-
// Fill in the credentials
44-
await usernameInput.waitFor({ state: 'visible', timeout: 10000 });
46+
//fill in the credentials
47+
await usernameInput.waitFor({ state: 'visible', timeout: TIMEOUTS.medium });
4548
await usernameInput.fill(user);
4649

4750
const passwordInput = page.getByLabel(/Password/i)
@@ -55,20 +58,21 @@ setup('authenticate to OpenShift Cluster', async ({ page, baseURL }) => {
5558
await passwordInput.fill(process.env.CLUSTER_PASSWORD);
5659
await page.getByRole('button', { name: /Log in/i }).click();
5760

58-
// Handle the OpenShift 4.x Welcome Tour modal if it appears
61+
//handle the OpenShift 4.x Welcome Tour modal if it appears
5962
try {
6063
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 });
64+
//wait up to 5 seconds for the modal to pop up
65+
await skipTourButton.waitFor({ state: 'visible', timeout: TIMEOUTS.short });
6366
await skipTourButton.click();
6467
console.log('Dismissed the OpenShift Welcome Tour modal.');
6568
} 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
69+
//if it doesn't appear within 5 seconds, it's an older cluster or already dismissed
70+
//safely ignore the error and move on
71+
console.debug('welcome tour modal did not appear, continuing...');
6872
}
6973

70-
// Save the auth state
71-
await expect(page.getByRole('navigation').first()).toBeVisible({ timeout: 20000 });
72-
await expect(page).toHaveURL(/(console|k8s|overview|dashboards)/i, { timeout: 15000 });
74+
//save the auth state
75+
await expect(page.getByRole('navigation').first()).toBeVisible({ timeout: TIMEOUTS.long });
76+
await expect(page).toHaveURL(/(console|k8s|overview|dashboards)/i, { timeout: TIMEOUTS.default });
7377
await page.context().storageState({ path: authFile });
7478
});

test/ui-e2e/global.setup.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { execSync } from 'child_process';
2+
3+
async function globalSetup() {
4+
console.log('🧹 Running pre-flight cleanup...');
5+
6+
try {
7+
console.log(' -> Sweeping ghost applications...');
8+
//no hangs on dead controllers
9+
execSync('oc delete applications.argoproj.io --all -n openshift-gitops --wait=false', { stdio: 'ignore' });
10+
11+
console.log(' -> Sweeping orphaned Spring Petclinic resources...');
12+
//no hangs on dead controllers
13+
execSync('oc delete all -l app=spring-petclinic -n openshift-gitops --wait=false', { stdio: 'ignore' });
14+
15+
console.log('✨ Cluster sanitized. Starting test suite.');
16+
} catch (error) {
17+
console.log('✨ Cluster is clean. Starting test suite.');
18+
}
19+
}
20+
21+
export default globalSetup;

test/ui-e2e/playwright.config.ts

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,40 @@
11
import { defineConfig, devices } from '@playwright/test';
2+
import dotenv from 'dotenv';
3+
import path from 'path';
24

35
/**
46
* Read environment variables from file.
57
* https://github.com/motdotla/dotenv
68
*/
7-
8-
// top of playwright.config.ts
9-
import dotenv from 'dotenv';
10-
import path from 'path';
119
dotenv.config({ path: path.resolve(__dirname, '.env') });
1210

1311
/**
1412
* See https://playwright.dev/docs/test-configuration.
1513
*/
1614
export default defineConfig({
15+
//register pre-flight script
16+
globalSetup: require.resolve('./global.setup.ts'),
17+
//global test timeout to 5 min
18+
timeout: 5 * 60 * 1000,
19+
1720
testDir: './tests',
18-
/* Run tests in files in parallel */
19-
fullyParallel: true,
21+
/* Turn off parallel execution inside files */
22+
fullyParallel: false,
2023
/* Fail the build on CI if you accidentally left test.only in the source code. */
2124
forbidOnly: !!process.env.CI,
2225
/* Retry on CI only */
2326
retries: process.env.CI ? 2 : 0,
24-
/* Opt out of parallel tests on CI. */
25-
workers: process.env.CI ? 1 : undefined,
27+
28+
//stops parallel execution so they don't fight over the openshift-gitops namespace.
29+
workers: 1,
30+
2631
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
2732
reporter: [
2833
['list'],
2934
['html', { open: process.env.CI ? 'never' : 'on-failure' }]
3035
],
3136

32-
/* GLOBAL FOUNDATION: These apply to everything */
37+
/* GLOBAL FOUNDATION: These apply to everything */
3338
use: {
3439
baseURL: process.env.ARGOCD_URL,
3540
ignoreHTTPSErrors: true,
@@ -44,7 +49,8 @@ export default defineConfig({
4449
testMatch: '**/.auth/setup.ts',
4550
/* Only changes the URL for this specific project */
4651
use: {
47-
baseURL: process.env.CONSOLE_URL, },
52+
baseURL: process.env.CONSOLE_URL,
53+
},
4854
},
4955

5056
// Update chromium project
@@ -62,16 +68,7 @@ export default defineConfig({
6268
name: 'firefox',
6369
use: {
6470
...devices['Desktop Firefox'],
65-
// storageState and dependencies here later if we want to run Firefox tests but for now just focus on Chromium
6671
},
6772
},
68-
// ... webkit etc ...
6973
],
70-
71-
/* Run your local dev server before starting the tests */
72-
// webServer: {
73-
// command: 'npm run start',
74-
// url: 'http://localhost:3000',
75-
// reuseExistingServer: !process.env.CI,
76-
// },
77-
});
74+
});

test/ui-e2e/src/fixtures.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export const test = base.extend<MyFixtures>({
3030
await use(page);
3131
},
3232

33-
//app setup/teardown
33+
// 🚀 Cleaned up 'request' from the parameters, just using 'page' now
3434
managedApp: [ async ({ page }, use) => {
3535
const appName = `e2e-app-${Date.now()}`;
3636
const appsPage = new ApplicationsPage(page);
@@ -50,17 +50,42 @@ export const test = base.extend<MyFixtures>({
5050

5151
//teardown
5252
console.log(`[teardown] deleting ${appName} via api`);
53-
const response = await page.request.delete(`/api/v1/applications/${appName}?cascade=true`, {
53+
54+
// 🚀 REVERTED: Back to page.request so we keep our UI login cookies!
55+
const deleteResponse = await page.request.delete(`/api/v1/applications/${appName}?cascade=true`, {
5456
headers: { 'Content-Type': 'application/json' }
5557
});
5658

57-
// 4. Update the teardown to only ignore 404s, treating 403s as failures
58-
if (response.status() === 404) {
59+
// If it's already 404 (or 403), we have nothing left to do
60+
if (deleteResponse.status() === 404 || deleteResponse.status() === 403) {
61+
console.log(`[teardown] ${appName} was already deleted.`);
5962
return;
6063
} else {
61-
expect(response.status()).toBeLessThan(400);
64+
// Ensure the delete request itself was accepted (200/202)
65+
expect(deleteResponse.status()).toBeLessThan(400);
66+
67+
console.log(`[teardown] waiting for background cleanup of ${appName} to finish...`);
68+
await expect.poll(async () => {
69+
try {
70+
// 🚀 REVERTED: Back to page.request, but KEEPING the try/catch shield!
71+
const checkResponse = await page.request.get(`/api/v1/applications/${appName}`);
72+
const status = checkResponse.status();
73+
74+
// 🚀 ACCEPT BOTH: 404 (Not Found) or 403 (Forbidden due to RBAC project scoping)
75+
return status === 404 || status === 403;
76+
} catch (error) {
77+
// If the OpenShift router blips or drops the socket, swallow it and keep polling
78+
return false;
79+
}
80+
}, {
81+
message: `Waiting for ${appName} to completely delete from the cluster.`,
82+
timeout: 60000,
83+
intervals: [2000, 5000, 10000],
84+
}).toBeTruthy(); // 🚀 Changed to check if our boolean logic returns true
85+
86+
console.log(`[teardown] ${appName} successfully removed from the cluster.`);
6287
}
63-
}, { timeout: 120000 } ],
88+
}, { timeout: 300000 } ],
6489
});
6590

6691
//export it so spec files can use it

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

Lines changed: 47 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
import { Page, expect, Locator } from '@playwright/test';
22

3+
//timeouts
4+
const TIMEOUTS = {
5+
short: 3000,
6+
modal: 5000,
7+
panel: 10000,
8+
default: 15000,
9+
load: 20000,
10+
render: 30000,
11+
sync: 120000,
12+
status: 180000
13+
};
14+
315
export class ApplicationsPage {
416
readonly page: Page;
517
readonly newAppButton: Locator;
@@ -43,40 +55,39 @@ export class ApplicationsPage {
4355
const errorBanner = this.page.getByText('try again');
4456
try {
4557
//wait 3 secs
46-
await errorBanner.waitFor({ state: 'visible', timeout: 3000 });
58+
await errorBanner.waitFor({ state: 'visible', timeout: TIMEOUTS.short });
4759
await errorBanner.click();
4860
} catch (error) {
4961
//banner didn't appear so just continue
5062
}
5163

52-
await expect(this.newAppButton).toBeVisible({ timeout: 15000 });
64+
await expect(this.newAppButton).toBeVisible({ timeout: TIMEOUTS.default });
5365
}
5466

5567
//helper for fields that need to have select a pre existing option
5668
async fillDropdown(locator: Locator, value: string) {
5769
await locator.click();
5870
await locator.pressSequentially(value, { delay: 50 });
5971

60-
//Wait for the dropdown
61-
await expect(locator).toHaveValue(value, { timeout: 5000 });
72+
//wait for the dropdown
73+
await expect(locator).toHaveValue(value, { timeout: TIMEOUTS.modal });
6274

6375
await locator.press('Enter');
6476
}
6577

66-
async createApp(appName: string, repoUrl: string, repoPath: string) {
78+
async createApp(appName: string, repoUrl: string, repoPath: string) {
6779
await this.newAppButton.click();
6880

6981
//handle the "failed to load data" banner if it appears inside the slide-out panel
7082
const errorBanner = this.page.getByText('try again');
7183
try {
72-
//wait 3 secs
73-
await errorBanner.waitFor({ state: 'visible', timeout: 3000 });
84+
await errorBanner.waitFor({ state: 'visible', timeout: TIMEOUTS.short });
7485
await errorBanner.click();
7586
} catch (error) {
7687
//banner didn't appear so just continue
7788
}
7889

79-
await this.page.getByText('Loading...').first().waitFor({ state: 'hidden', timeout: 15000 });
90+
await this.page.getByText('Loading...').first().waitFor({ state: 'hidden', timeout: TIMEOUTS.default });
8091

8192
await this.appNameInput.fill(appName);
8293
await this.fillDropdown(this.projectInput, 'default');
@@ -88,8 +99,9 @@ async createApp(appName: string, repoUrl: string, repoPath: string) {
8899
//dest
89100
await this.clusterUrlInput.fill('https://kubernetes.default.svc');
90101

91-
//deploy
92-
await this.namespaceInput.fill('openshift-gitops');
102+
//deploy to namespace
103+
await this.namespaceInput.fill('openshift-gitops');
104+
93105
await this.createButton.click();
94106
}
95107

@@ -98,38 +110,47 @@ async createApp(appName: string, repoUrl: string, repoPath: string) {
98110
await this.page.getByPlaceholder(/Search applications/i).fill(appName);
99111

100112
const appContainer = this.page.locator('.white-box, .argo-table-list__row').filter({ hasText: appName });
101-
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
113+
await appContainer.waitFor({ state: 'visible', timeout: TIMEOUTS.load });
114+
115+
//critical cross-version fix: wait for Argo CD to finish its initial Git clone
116+
//if we open the Sync panel before this happens, the resources list will be empty!
117+
await expect(appContainer.getByText(/OutOfSync|Out of Sync/i).first()).toBeVisible({ timeout: TIMEOUTS.sync });
118+
119+
//now it is safe to open the panel
104120
await appContainer.getByText('Sync', { exact: true }).click();
105121

106-
//click 'all'
122+
//click 'all' first to ensure all resource checkboxes are ticked across newer Argo CD versions
107123
const allLink = this.page.getByRole('link', { name: 'all', exact: true });
108124
try {
109-
await allLink.waitFor({ state: 'visible', timeout: 5000 });
125+
await allLink.waitFor({ state: 'visible', timeout: TIMEOUTS.modal });
110126
await allLink.click();
111127
} catch (error) {
112-
// all link didn't appear within 5 sec
128+
//'all' link didn't appear which is normal for this version so do nothing.
113129
}
114-
115-
//wait for the manifests to render on the panel
116-
await expect(this.page.getByText(expectedResource).first()).toBeVisible({ timeout: 30000 });
130+
131+
//wait for the manifests to render on the panel (generous timeout for slower FIPS clusters)
132+
await expect(this.page.getByText(expectedResource).first()).toBeVisible({ timeout: TIMEOUTS.render });
117133

118134
//click the main sync button
119135
await this.page.getByRole('button', { name: /^synchronize$/i }).first().click();
120136

121137
//wait for the panel to close
122-
await expect(this.page.getByText('SYNCHRONIZE RESOURCES')).toBeHidden({ timeout: 15000 });
138+
await expect(this.page.getByText('SYNCHRONIZE RESOURCES')).toBeHidden({ timeout: TIMEOUTS.panel });
123139
}
124140

125141
async verifyStatus(appName: string) {
126-
//re-apply search filter just in case
127142
await this.page.getByPlaceholder(/Search applications/i).fill(appName);
128143
const appContainer = this.page.locator('.white-box, .argo-table-list__row').filter({ hasText: appName });
129144

130-
//90 secs
131-
await expect(appContainer.getByText(/synced/i)).toBeVisible({ timeout: 90000 });
132-
await expect(appContainer.getByText(/healthy/i)).toBeVisible({ timeout: 90000 });
145+
//pass the message
146+
await expect(
147+
appContainer.getByText(/Sync failed/i),
148+
`Argo CD failed to sync the application manifests for ${appName}.`
149+
).toBeHidden({ timeout: TIMEOUTS.panel });
150+
151+
//if it didn't fail to wait for success states
152+
await expect(appContainer.getByText(/synced/i)).toBeVisible({ timeout: TIMEOUTS.status });
153+
await expect(appContainer.getByText(/healthy/i)).toBeVisible({ timeout: TIMEOUTS.status });
133154
}
134155

135156
async openApplication(appName: string) {
@@ -141,10 +162,10 @@ async createApp(appName: string, repoUrl: string, repoPath: string) {
141162
.filter({ hasText: appName })
142163
.getByRole('link', { name: appName });
143164

144-
await appLink.waitFor({ state: 'visible', timeout: 15000 });
165+
await appLink.waitFor({ state: 'visible', timeout: TIMEOUTS.default });
145166
await appLink.click();
146167

147168
//wait for the URL to change to the details page to ensure the click worked
148-
await expect(this.page).toHaveURL(/.*\/applications\/.*\/.*/, { timeout: 15000 });
169+
await expect(this.page).toHaveURL(/.*\/applications\/.*\/.*/, { timeout: TIMEOUTS.default });
149170
}
150171
}

0 commit comments

Comments
 (0)