diff --git a/test/ui-e2e/.auth/setup.ts b/test/ui-e2e/.auth/setup.ts index 6b30991d3ac..03fad54e779 100644 --- a/test/ui-e2e/.auth/setup.ts +++ b/test/ui-e2e/.auth/setup.ts @@ -2,8 +2,11 @@ import { test as setup, expect } from '@playwright/test'; const authFile = '.auth/storageState.json'; +//centralized timeouts to appease the linter +const TIMEOUTS = { short: 5000, medium: 10000, default: 15000, long: 20000 }; + setup('authenticate to OpenShift Cluster', async ({ page, baseURL }) => { - // Navigate to the OpenShift Console + //navigate to the OpenShift Console const targetUrl = baseURL || process.env.CONSOLE_URL || process.env.BASE_URL; if (!targetUrl) { @@ -13,17 +16,17 @@ setup('authenticate to OpenShift Cluster', async ({ page, baseURL }) => { console.log(`Navigating to OpenShift Console: ${targetUrl}`); await page.goto(targetUrl); - // Set locators + //set locators const idpScreenText = page.getByText(/Log in with/i); const usernameInput = page.getByLabel(/Username/i) .or(page.locator('input[name="username"]')) .or(page.getByPlaceholder(/Username/i)); - // Fail loudly if the page is dead so we don't get weird errors later + //fail loudly if the page is dead so we don't get weird errors later await expect( idpScreenText.or(usernameInput).first(), "OpenShift login page failed to load. Check cluster health and URL." - ).toBeVisible({ timeout: 20000 }); + ).toBeVisible({ timeout: TIMEOUTS.long }); const idpName = process.env.IDP || 'kube:admin'; const user = process.env.CLUSTER_USER || 'kubeadmin'; @@ -31,17 +34,17 @@ setup('authenticate to OpenShift Cluster', async ({ page, baseURL }) => { if (await idpScreenText.isVisible()) { console.log(`IDP selection screen detected. Selecting provider: "${idpName}"`); - // Look for the specific IDP - const idpLink = page.getByRole('link', { name: new RegExp(idpName, 'i') }); + //look for the specific IDP + const idpLink = page.getByRole('link', { name: idpName, exact: true }); - await idpLink.waitFor({ state: 'visible', timeout: 5000 }); + await idpLink.waitFor({ state: 'visible', timeout: TIMEOUTS.short }); await idpLink.click(); } else { console.log("No IDP screen detected (or already selected), proceeding to credentials..."); } - // Fill in the credentials - await usernameInput.waitFor({ state: 'visible', timeout: 10000 }); + //fill in the credentials + await usernameInput.waitFor({ state: 'visible', timeout: TIMEOUTS.medium }); await usernameInput.fill(user); const passwordInput = page.getByLabel(/Password/i) @@ -55,9 +58,25 @@ setup('authenticate to OpenShift Cluster', async ({ page, baseURL }) => { await passwordInput.fill(process.env.CLUSTER_PASSWORD); await page.getByRole('button', { name: /Log in/i }).click(); - // Save the auth state - await expect(page.getByRole('navigation').first()).toBeVisible({ timeout: 15000 }); - await expect(page).toHaveURL(/(console|k8s|overview|dashboards)/i, { timeout: 15000 }); - await page.context().storageState({ path: authFile }); +//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: TIMEOUTS.short }); + await skipTourButton.click(); + console.log('Dismissed the OpenShift Welcome Tour modal.'); + } catch (error) { + if (error instanceof Error && error.name === 'TimeoutError') { + //safely ignore the timeout and move on + console.log('welcome tour modal did not appear, continuing...'); + } else { + //throw any other unexpected errors + throw error; + } + } + //save the auth state + await expect(page.getByRole('navigation').first()).toBeVisible({ timeout: TIMEOUTS.long }); + await expect(page).toHaveURL(/(console|k8s|overview|dashboards)/i, { timeout: TIMEOUTS.default }); + await page.context().storageState({ path: authFile }); }); \ No newline at end of file diff --git a/test/ui-e2e/README.md b/test/ui-e2e/README.md index 69889674ee8..fae9ca41057 100644 --- a/test/ui-e2e/README.md +++ b/test/ui-e2e/README.md @@ -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 @@ -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= | Overrides the local setup to simulate automation. It forces headless execution, performs a clean `npm ci`, and installs required browser binaries dynamically. | ### Visual Debugging (Trace Viewer) @@ -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 diff --git a/test/ui-e2e/global.setup.ts b/test/ui-e2e/global.setup.ts new file mode 100644 index 00000000000..abad09b10e0 --- /dev/null +++ b/test/ui-e2e/global.setup.ts @@ -0,0 +1,22 @@ +import { execSync } from 'child_process'; + +async function globalSetup() { + console.log(' * Running pre-flight cleanup...'); + + try { + console.log(' -> Sweeping ghost applications...'); + //no hangs on dead controllers + execSync('oc delete applications.argoproj.io --all -n openshift-gitops --wait=false', { stdio: 'ignore' }); + + console.log(' -> Sweeping orphaned Spring Petclinic resources...'); + //no hangs on dead controllers + execSync('oc delete all -l app=spring-petclinic -n openshift-gitops --wait=false', { stdio: 'ignore' }); + + console.log('* Cluster sanitized. Starting test suite.'); + } catch (error) { + console.error('Pre-flight cleanup failed. Check your cluster connection.', error); + throw error; + } +} + +export default globalSetup; \ No newline at end of file diff --git a/test/ui-e2e/playwright.config.ts b/test/ui-e2e/playwright.config.ts index a1c958d6deb..c365a811b7a 100644 --- a/test/ui-e2e/playwright.config.ts +++ b/test/ui-e2e/playwright.config.ts @@ -1,35 +1,40 @@ import { defineConfig, devices } from '@playwright/test'; +import dotenv from 'dotenv'; +import path from 'path'; /** * Read environment variables from file. * https://github.com/motdotla/dotenv */ - -// top of playwright.config.ts -import dotenv from 'dotenv'; -import path from 'path'; dotenv.config({ path: path.resolve(__dirname, '.env') }); /** * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ + //register pre-flight script + globalSetup: require.resolve('./global.setup.ts'), + //global test timeout to 5 min + timeout: 5 * 60 * 1000, + testDir: './tests', - /* Run tests in files in parallel */ - fullyParallel: true, + /* Turn off parallel execution inside files */ + fullyParallel: false, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* Retry on CI only */ retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, + + //stops parallel execution so they don't fight over the openshift-gitops namespace. + workers: 1, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: [ ['list'], ['html', { open: process.env.CI ? 'never' : 'on-failure' }] ], -/* GLOBAL FOUNDATION: These apply to everything */ + /* GLOBAL FOUNDATION: These apply to everything */ use: { baseURL: process.env.ARGOCD_URL, ignoreHTTPSErrors: true, @@ -44,7 +49,8 @@ export default defineConfig({ testMatch: '**/.auth/setup.ts', /* Only changes the URL for this specific project */ use: { - baseURL: process.env.CONSOLE_URL, }, + baseURL: process.env.CONSOLE_URL, + }, }, // Update chromium project @@ -62,16 +68,7 @@ export default defineConfig({ name: 'firefox', use: { ...devices['Desktop Firefox'], - // storageState and dependencies here later if we want to run Firefox tests but for now just focus on Chromium }, }, - // ... webkit etc ... ], - - /* Run your local dev server before starting the tests */ - // webServer: { - // command: 'npm run start', - // url: 'http://localhost:3000', - // reuseExistingServer: !process.env.CI, - // }, -}); +}); \ No newline at end of file diff --git a/test/ui-e2e/run-ui-tests.sh b/test/ui-e2e/run-ui-tests.sh index 17f30f1095c..666d7b41fff 100755 --- a/test/ui-e2e/run-ui-tests.sh +++ b/test/ui-e2e/run-ui-tests.sh @@ -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 + +#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 automatically exporting + set +a #stop auto export fi -#making sure we are in the correct dir -cd "$(dirname "$0")" || exit 1 - -# 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"} @@ -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 @@ -53,4 +65,28 @@ rm -f .auth/storageState.json || true #run Playwright echo " Starting Playwright tests..." -npx playwright test "$@" \ No newline at end of file + +# 2. Execute based on the environment +if [ "$ENV" = "ci" ] || [ "$ENV" = "pipeline" ]; then + echo "Running headlessly in automation ($ENV)..." + npm ci + if [ "$(uname -s)" = "Darwin" ]; then + npx playwright install chromium + else + npx playwright install chromium --with-deps + fi + + #headed from args + FILTERED_ARGS=() + for arg in "${TEST_ARGS[@]}"; do + if [[ "$arg" != "--headed" ]]; then + FILTERED_ARGS+=("$arg") + fi + done + + npx playwright test "${FILTERED_ARGS[@]}" --reporter=list + +else + echo "Running Locally..." + npx playwright test "${TEST_ARGS[@]}" +fi \ No newline at end of file diff --git a/test/ui-e2e/src/fixtures.ts b/test/ui-e2e/src/fixtures.ts index 0eae15e8159..29cea47f0f6 100644 --- a/test/ui-e2e/src/fixtures.ts +++ b/test/ui-e2e/src/fixtures.ts @@ -30,7 +30,6 @@ export const test = base.extend({ await use(page); }, -//app setup/teardown managedApp: [ async ({ page }, use) => { const appName = `e2e-app-${Date.now()}`; const appsPage = new ApplicationsPage(page); @@ -50,17 +49,45 @@ export const test = base.extend({ //teardown console.log(`[teardown] deleting ${appName} via api`); - const response = await page.request.delete(`/api/v1/applications/${appName}?cascade=true`, { + + //page.request + const deleteResponse = await page.request.delete(`/api/v1/applications/${appName}?cascade=true`, { headers: { 'Content-Type': 'application/json' } }); - // 4. Update the teardown to only ignore 404s, treating 403s as failures - if (response.status() === 404) { + // If it's already 404 (or 403), we have nothing left to do + if (deleteResponse.status() === 404 || deleteResponse.status() === 403) { + console.log(`[teardown] ${appName} was already deleted.`); return; } else { - expect(response.status()).toBeLessThan(400); + // Ensure the delete request itself was accepted (200/202) + expect(deleteResponse.status()).toBeLessThan(400); + + console.log(`[teardown] waiting for background cleanup of ${appName} to finish...`); + await expect.poll(async () => { + try { + const checkResponse = await page.request.get(`/api/v1/applications/${appName}`); + const status = checkResponse.status(); + + //404 (Not Found) or 403 (Forbidden due to RBAC project scoping) + return status === 404 || status === 403; + } catch (error) { + //router blips or drops the socket swallow it and keep polling + if (error instanceof Error && (error.message.includes('hang up') || error.message.includes('RESET') || error.message.includes('closed'))) { + return false; + } + //fail fast + throw error; + } + }, { + message: `Waiting for ${appName} to completely delete from the cluster.`, + timeout: 60000, + intervals: [2000, 5000, 10000], + }).toBeTruthy(); + + console.log(`[teardown] ${appName} successfully removed from the cluster.`); } - }, { timeout: 120000 } ], + }, { timeout: 300000 } ], }); //export it so spec files can use it diff --git a/test/ui-e2e/src/pages/ApplicationDetailsPage.ts b/test/ui-e2e/src/pages/ApplicationDetailsPage.ts new file mode 100644 index 00000000000..fa722ee0378 --- /dev/null +++ b/test/ui-e2e/src/pages/ApplicationDetailsPage.ts @@ -0,0 +1,65 @@ +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.resourceTreeContainer.getByText('Healthy', { exact: true }).first()).toBeVisible({ timeout: 30000 }); + + } + + 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 }); + } + } +} \ No newline at end of file diff --git a/test/ui-e2e/src/pages/ApplicationsPage.ts b/test/ui-e2e/src/pages/ApplicationsPage.ts index 130c067c7f2..4f7fdecb1e8 100644 --- a/test/ui-e2e/src/pages/ApplicationsPage.ts +++ b/test/ui-e2e/src/pages/ApplicationsPage.ts @@ -1,5 +1,17 @@ import { Page, expect, Locator } from '@playwright/test'; +//timeouts +const TIMEOUTS = { + short: 3000, + modal: 5000, + panel: 10000, + default: 15000, + load: 20000, + render: 30000, + sync: 120000, + status: 180000 +}; + export class ApplicationsPage { readonly page: Page; readonly newAppButton: Locator; @@ -43,13 +55,18 @@ export class ApplicationsPage { const errorBanner = this.page.getByText('try again'); try { //wait 3 secs - await errorBanner.waitFor({ state: 'visible', timeout: 3000 }); + await errorBanner.waitFor({ state: 'visible', timeout: TIMEOUTS.short }); await errorBanner.click(); } catch (error) { - //banner didn't appear so just continue + //ignore if the banner timed out (wasn't present) + if (error instanceof Error && error.name === 'TimeoutError') { + //banner didn't appear so just continue + } else { + throw error; + } } - await expect(this.newAppButton).toBeVisible({ timeout: 15000 }); + await expect(this.newAppButton).toBeVisible({ timeout: TIMEOUTS.default }); } //helper for fields that need to have select a pre existing option @@ -57,15 +74,30 @@ export class ApplicationsPage { await locator.click(); await locator.pressSequentially(value, { delay: 50 }); - //Wait for the dropdown - await expect(locator).toHaveValue(value, { timeout: 5000 }); + //wait for the dropdown + await expect(locator).toHaveValue(value, { timeout: TIMEOUTS.modal }); await locator.press('Enter'); } async createApp(appName: string, repoUrl: string, repoPath: string) { await this.newAppButton.click(); - await this.page.getByText('Loading...').first().waitFor({ state: 'hidden', timeout: 15000 }); + + //handle the "failed to load data" banner if it appears inside the slide-out panel + const errorBanner = this.page.getByText('try again'); + try { + await errorBanner.waitFor({ state: 'visible', timeout: TIMEOUTS.short }); + await errorBanner.click(); + } catch (error) { + //ignore if the banner timed out (wasn't present) + if (error instanceof Error && error.name === 'TimeoutError') { + // banner didn't appear so just continue + } else { + throw error; + } + } + + await this.page.getByText('Loading...').first().waitFor({ state: 'hidden', timeout: TIMEOUTS.default }); await this.appNameInput.fill(appName); await this.fillDropdown(this.projectInput, 'default'); @@ -77,45 +109,80 @@ export class ApplicationsPage { //dest await this.clusterUrlInput.fill('https://kubernetes.default.svc'); - //deploy - await this.namespaceInput.fill('openshift-gitops'); + //deploy to namespace + await this.namespaceInput.fill('openshift-gitops'); + 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 appContainer.getByText('Sync', { exact: true }).click(); + await appContainer.waitFor({ state: 'visible', timeout: TIMEOUTS.load }); - //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 }); + //critical cross-version fix: wait for Argo CD to finish its initial Git clone + //if we open the Sync panel before this happens, the resources list will be empty! + await expect(appContainer.getByText(/OutOfSync|Out of Sync/i).first()).toBeVisible({ timeout: TIMEOUTS.sync }); - //click 'all' to ensure all resource checkboxes are ticked across all Argo CD versions - const allLink = this.page.getByRole('link', { name: 'all', exact: true }); + //now it is safe to open the panel + await appContainer.getByText('Sync', { exact: true }).click(); + + const slideOutPanel = this.page.locator('.sliding-panel').filter({ visible: true }); + + // 🚀 SWAPPED: this.page is now slideOutPanel + const allLink = slideOutPanel.getByRole('link', { name: 'all', exact: true }); try { - await allLink.waitFor({ state: 'visible', timeout: 3000 }); + await allLink.waitFor({ state: 'visible', timeout: TIMEOUTS.modal }); await allLink.click(); } catch (error) { - //all link didn't appear within 3 sec + //ignore if the link timed out (absent in older versions) + if (error instanceof Error && error.name === 'TimeoutError') { + //'all' link didn't appear which is normal for this version so do nothing. + } else { + throw error; + } } - //click the main sync button - await this.page.getByRole('button', { name: /^synchronize$/i }).first().click(); + + // 🚀 SWAPPED: this.page is now slideOutPanel + await expect(slideOutPanel.getByText(expectedResource).first()).toBeVisible({ timeout: TIMEOUTS.render }); - //wait for the panel to close - await expect(this.page.getByText('SYNCHRONIZE RESOURCES')).toBeHidden({ timeout: 10000 }); + // 🚀 SWAPPED: this.page is now slideOutPanel + await slideOutPanel.getByRole('button', { name: /^synchronize$/i }).first().click(); + + //wait for the panel to close + await expect(this.page.getByText('SYNCHRONIZE RESOURCES')).toBeHidden({ timeout: TIMEOUTS.panel }); } async verifyStatus(appName: string) { - //re-apply search filter just in case await this.page.getByPlaceholder(/Search applications/i).fill(appName); const appContainer = this.page.locator('.white-box, .argo-table-list__row').filter({ hasText: appName }); - //90 secs - await expect(appContainer.getByText(/synced/i)).toBeVisible({ timeout: 90000 }); - await expect(appContainer.getByText(/healthy/i)).toBeVisible({ timeout: 90000 }); + //pass the message + await expect( + appContainer.getByText(/Sync failed/i), + `Argo CD failed to sync the application manifests for ${appName}.` + ).toBeHidden({ timeout: TIMEOUTS.panel }); + + //if it didn't fail to wait for success states + await expect(appContainer.getByText(/synced/i)).toBeVisible({ timeout: TIMEOUTS.status }); + await expect(appContainer.getByText(/healthy/i)).toBeVisible({ timeout: TIMEOUTS.status }); + } + + 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({ has: this.page.getByText(appName, { exact: true }) }) + .getByRole('link', { name: appName, exact: true }); + + await appLink.waitFor({ state: 'visible', timeout: TIMEOUTS.default }); + 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: TIMEOUTS.default }); } } \ No newline at end of file diff --git a/test/ui-e2e/src/pages/LoginPage.ts b/test/ui-e2e/src/pages/LoginPage.ts index 6bf8615dd95..a1b107c0ff1 100644 --- a/test/ui-e2e/src/pages/LoginPage.ts +++ b/test/ui-e2e/src/pages/LoginPage.ts @@ -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); diff --git a/test/ui-e2e/tests/admin-login.spec.ts b/test/ui-e2e/tests/admin-login.spec.ts index 47cc90ca311..7170ba9dcaf 100644 --- a/test/ui-e2e/tests/admin-login.spec.ts +++ b/test/ui-e2e/tests/admin-login.spec.ts @@ -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 @@ -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 diff --git a/test/ui-e2e/tests/resource-tree.spec.ts b/test/ui-e2e/tests/resource-tree.spec.ts new file mode 100644 index 00000000000..4123e396f0b --- /dev/null +++ b/test/ui-e2e/tests/resource-tree.spec.ts @@ -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(); + }); + +}); \ No newline at end of file