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
45 changes: 32 additions & 13 deletions test/ui-e2e/.auth/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -13,35 +16,35 @@ 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';

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();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} 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)
Expand All @@ -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;
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

//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 });
});
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. |

### 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
22 changes: 22 additions & 0 deletions test/ui-e2e/global.setup.ts
Original file line number Diff line number Diff line change
@@ -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;
37 changes: 17 additions & 20 deletions test/ui-e2e/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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
Expand All @@ -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,
// },
});
});
69 changes: 61 additions & 8 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

#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"}

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,45 @@ rm -f .auth/storageState.json || true

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

echo " "
#get the installed gitops version
GITOPS_VERSION=$(oc get csv -n openshift-gitops -o jsonpath='{.items[?(@.spec.displayName=="Red Hat OpenShift GitOps")].spec.version}' 2>/dev/null)
if [ -z "$GITOPS_VERSION" ]; then
GITOPS_VERSION="Unknown"
fi

#get Argo CD version
ARGO_API_VERSION=$(curl -s -k "$ARGOCD_URL/api/version" | grep -o '"Version":"[^"]*"' | cut -d'"' -f4)
if [ -z "$ARGO_API_VERSION" ]; then
ARGO_API_VERSION="Unknown"
fi

echo " TARGETING GITOPS VERSION: v$GITOPS_VERSION"
echo " TARGETING ARGO CD VERSION: $ARGO_API_VERSION"
echo " "

# 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
Loading
Loading