From 68a85a27ec0b6eeaa4a818dee0a339319b062aa5 Mon Sep 17 00:00:00 2001 From: Fiete Ostkamp Date: Mon, 9 Mar 2026 12:23:16 +0100 Subject: [PATCH] Add playwright test - add everything that is required to run playwright against the same stack that is used in the pipeline for the existing ui tests - make some instable ui tests more reliable Issue-ID: SDC-4800 Change-Id: I938ada1aa98aa961e60e24f6128d7164023a1c9f Signed-off-by: Fiete Ostkamp --- .gitignore | 2 + asdctool/sdc-cassandra-init/version.sh | 1 + integration-tests/playwright-tests/README.md | 116 +++++++++++++++++++++ .../playwright-tests/package-lock.json | 78 ++++++++++++++ integration-tests/playwright-tests/package.json | 14 +++ .../playwright-tests/playwright.config.ts | 32 ++++++ .../playwright-tests/tests/sdc-sanity.spec.ts | 54 ++++++++++ integration-tests/pom.xml | 88 ++++++++++++++++ .../workspace/CompositionCanvasComponent.java | 82 ++++++++++----- .../ci/tests/utilities/NotificationComponent.java | 20 +++- pom.xml | 26 ++++- 11 files changed, 483 insertions(+), 30 deletions(-) create mode 100644 asdctool/sdc-cassandra-init/version.sh create mode 100644 integration-tests/playwright-tests/README.md create mode 100644 integration-tests/playwright-tests/package-lock.json create mode 100644 integration-tests/playwright-tests/package.json create mode 100644 integration-tests/playwright-tests/playwright.config.ts create mode 100644 integration-tests/playwright-tests/tests/sdc-sanity.spec.ts diff --git a/.gitignore b/.gitignore index 18c83d48dc..8a7712bb67 100644 --- a/.gitignore +++ b/.gitignore @@ -193,3 +193,5 @@ vagrant-asdc-all-in-one/ Vagrantfile *.xls +node_modules +node diff --git a/asdctool/sdc-cassandra-init/version.sh b/asdctool/sdc-cassandra-init/version.sh new file mode 100644 index 0000000000..d4ec29514d --- /dev/null +++ b/asdctool/sdc-cassandra-init/version.sh @@ -0,0 +1 @@ +normal['version']="1.16.0" diff --git a/integration-tests/playwright-tests/README.md b/integration-tests/playwright-tests/README.md new file mode 100644 index 0000000000..e3f0af54b4 --- /dev/null +++ b/integration-tests/playwright-tests/README.md @@ -0,0 +1,116 @@ +# SDC Playwright E2E Tests + +Playwright-based end-to-end tests for the SDC frontend. These tests run against the same +Docker stack used by the existing Selenium/TestNG integration tests (Cassandra, backend, +frontend, webseal-simulator, etc.) but use [Playwright](https://playwright.dev/) instead +of Selenium for browser automation. + +## Prerequisites + +- Node.js 18+ (Playwright ≥ 1.42 requires it) +- Docker +- Ports 8080, 8285, 8443, 9042, 9443 free on the host + +## Running locally + +The tests need the integration-test Docker stack (Cassandra, backend, frontend, +simulator). If the stack is already running, skip straight to "Run the tests". + +### 1. Build Docker images (one-time) + +The Docker images (`onap/sdc-backend-all-plugins`, `onap/sdc-frontend`, etc.) +must exist locally. Build them from the repository root with **both** the +`all` and `docker` profiles (`-P all` keeps the default module list active +when other profiles are specified): + +```bash +mvn clean install -P all,docker -DskipTests +``` + +> **Note:** `clean` is required so that the `build-helper-maven-plugin` +> `parse-version` goal (bound to `pre-clean`) runs and resolves +> `${parsedVersion.*}` variables used in Docker image tags. + +### 2. Start the Docker stack + +```bash +mvn pre-integration-test -P run-integration-tests-playwright \ + -f integration-tests/pom.xml +``` + +`pre-integration-test` starts the containers (Cassandra, backend, frontend, +simulator, etc.) but does **not** run the tests or tear them down, so the +stack stays up for iterating locally. + +Once healthy, the webseal-simulator is reachable at `http://localhost:8285`. + +Stop the containers later with: + +```bash +mvn docker:stop -f integration-tests/pom.xml +``` + +### 3. Run the tests + +```bash +cd integration-tests/playwright-tests +npm install +npx playwright install chromium +SDC_BASE_URL=http://localhost:8285 npx playwright test +``` + +### Headed mode (see the browser) + +```bash +SDC_BASE_URL=http://localhost:8285 npm run test:headed +``` + +### View the HTML report + +```bash +npm run test:report +``` + +## Running via Maven (full lifecycle) + +A single Maven command handles the entire lifecycle — spin up Docker, install +Node/npm, install Playwright browsers, run the tests, tear down Docker: + +```bash +mvn verify -P run-integration-tests-playwright \ + -f integration-tests/pom.xml +``` + +> This assumes the Docker images already exist locally (see step 1 above). + +Reports are written to: + +| Artifact | Path | +| -------------------- | --------------------------------------------------------- | +| HTML report | `integration-tests/target/playwright-report/index.html` | +| JUnit XML | `integration-tests/target/playwright-reports/results.xml` | +| Screenshots & traces | `integration-tests/target/playwright-results/` | + +## CI (Jenkins) + +The JJB definition in `ci-management` registers the job +`sdc-integration-tests-{stream}-playwright-verify-java`, which triggers on every +Gerrit patch set and archives the report artifacts listed above. + +## Writing new tests + +Add `.spec.ts` files under `tests/`. The Playwright config (`playwright.config.ts`) +sets `baseURL` from the `SDC_BASE_URL` environment variable (default +`http://localhost:8285`), so you can use relative URLs in `page.goto()`. + +The webseal-simulator login flow is straightforward: + +```ts +await page.goto("/login"); +await page.locator('input[name="userId"]').fill(""); +await page.locator('input[name="password"]').fill("123123a"); +await page.locator('input[value="Login"]').click(); +await page.waitForURL("**/sdc1**"); +``` + +See `tests/sdc-sanity.spec.ts` for a working example. diff --git a/integration-tests/playwright-tests/package-lock.json b/integration-tests/playwright-tests/package-lock.json new file mode 100644 index 0000000000..537beed6ff --- /dev/null +++ b/integration-tests/playwright-tests/package-lock.json @@ -0,0 +1,78 @@ +{ + "name": "sdc-playwright-tests", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sdc-playwright-tests", + "version": "1.0.0", + "devDependencies": { + "@playwright/test": "^1.42.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://artifactory.devops.telekom.de/artifactory/api/npm/registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@playwright/test/node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://artifactory.devops.telekom.de/artifactory/api/npm/registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://artifactory.devops.telekom.de/artifactory/api/npm/registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://artifactory.devops.telekom.de/artifactory/api/npm/registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/integration-tests/playwright-tests/package.json b/integration-tests/playwright-tests/package.json new file mode 100644 index 0000000000..021826e1a5 --- /dev/null +++ b/integration-tests/playwright-tests/package.json @@ -0,0 +1,14 @@ +{ + "name": "sdc-playwright-tests", + "version": "1.0.0", + "description": "Playwright end-to-end tests for SDC", + "private": true, + "scripts": { + "test": "npx playwright test", + "test:headed": "npx playwright test --headed", + "test:report": "npx playwright show-report" + }, + "devDependencies": { + "@playwright/test": "^1.42.0" + } +} diff --git a/integration-tests/playwright-tests/playwright.config.ts b/integration-tests/playwright-tests/playwright.config.ts new file mode 100644 index 0000000000..4be91c533e --- /dev/null +++ b/integration-tests/playwright-tests/playwright.config.ts @@ -0,0 +1,32 @@ +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests', + timeout: 60_000, + expect: { + timeout: 10_000, + }, + fullyParallel: false, + retries: 1, + reporter: [ + ['list'], + ['html', { outputFolder: '../target/playwright-report', open: 'never' }], + ['junit', { outputFile: '../target/playwright-reports/results.xml' }], + ], + use: { + baseURL: process.env.SDC_BASE_URL || 'http://localhost:8285', + trace: 'on', + screenshot: 'only-on-failure', + ignoreHTTPSErrors: true, + }, + projects: [ + { + name: 'chromium', + use: { + browserName: 'chromium', + viewport: { width: 1920, height: 1080 }, + }, + }, + ], + outputDir: '../target/playwright-results', +}); diff --git a/integration-tests/playwright-tests/tests/sdc-sanity.spec.ts b/integration-tests/playwright-tests/tests/sdc-sanity.spec.ts new file mode 100644 index 0000000000..3f5777280f --- /dev/null +++ b/integration-tests/playwright-tests/tests/sdc-sanity.spec.ts @@ -0,0 +1,54 @@ +import { test, expect } from '@playwright/test'; + +/** + * SDC Sanity Test - Demonstrates Playwright e2e testing with the integration-test Docker stack. + * + * This test uses the webseal-simulator (sdc-sim) login page to authenticate, + * then verifies that the SDC home page loads successfully. + * + * When running locally: SDC_BASE_URL=http://localhost:8285 npx playwright test + * When running in CI: The Maven profile sets the URL automatically. + */ + +const SIM_PASSWORD = '123123a'; + +test.describe('SDC Sanity', () => { + + test('should login via simulator and reach the SDC home page', async ({ page }) => { + // Navigate to the simulator login page + await page.goto('/login'); + + // Verify the login page rendered + await expect(page.locator('h1')).toContainText('Webseal simulator'); + + // Fill in credentials for the Designer role + await page.locator('input[name="userId"]').fill('cs0008'); + await page.locator('input[name="password"]').fill(SIM_PASSWORD); + + // Submit the login form + await page.locator('input[value="Login"]').click(); + + // After login the simulator redirects to /sdc1 which loads the SDC UI. + // Wait for the URL to contain /sdc1 (the redirect target). + await page.waitForURL('**/sdc1**', { timeout: 30_000 }); + + // The SDC UI should render – verify the page title or a known element. + // The SDC app sets the document title to "SDC" or similar. + await expect(page).toHaveTitle(/SDC|STARTER/i, { timeout: 30_000 }); + + // The HOME button in the main menu should be visible + await expect(page.locator('[data-tests-id="main-menu-button-home"]')).toBeVisible({ timeout: 30_000 }); + }); + + test('should display user quick-links table on the login page', async ({ page }) => { + await page.goto('/login'); + + // The simulator renders a table of preconfigured users + const table = page.locator('table'); + await expect(table).toBeVisible(); + + // At least one user row should be present + const rows = table.locator('tr'); + await expect(rows).not.toHaveCount(0); + }); +}); diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 1739b6fe59..378b1e5915 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -1483,5 +1483,93 @@ limitations under the License. + + + run-integration-tests-playwright + + true + true + true + false + http://localhost:8285 + v18.20.8 + 10.8.2 + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + true + + + + + com.github.eirslett + frontend-maven-plugin + 1.12.0 + + ${project.basedir}/playwright-tests + ${project.basedir}/playwright-tests + ${npm.registry} + + + + pw-install-node-and-npm + pre-integration-test + + install-node-and-npm + + + ${it.playwright.nodeVersion} + ${it.playwright.npmVersion} + + + + pw-npm-install + pre-integration-test + + npm + + + install + + + + pw-install-browsers + pre-integration-test + + npx + + + playwright install chromium + + + + pw-run-tests + integration-test + + npx + + + playwright test + + ${it.playwright.baseUrl} + + + + + + + + diff --git a/integration-tests/src/test/java/org/onap/sdc/frontend/ci/tests/pages/component/workspace/CompositionCanvasComponent.java b/integration-tests/src/test/java/org/onap/sdc/frontend/ci/tests/pages/component/workspace/CompositionCanvasComponent.java index e29df1583a..0e481c5b00 100644 --- a/integration-tests/src/test/java/org/onap/sdc/frontend/ci/tests/pages/component/workspace/CompositionCanvasComponent.java +++ b/integration-tests/src/test/java/org/onap/sdc/frontend/ci/tests/pages/component/workspace/CompositionCanvasComponent.java @@ -143,41 +143,69 @@ public class CompositionCanvasComponent extends AbstractPageObject { public ComponentInstance createNodeOnServiceCanvas(final String serviceName, final String serviceVersion, final String resourceName, final String resourceVersion) { - final Point freePositionInCanvas = getFreePositionInCanvas(20); - final Point pointFromCanvasCenter = calculateOffsetFromCenter(freePositionInCanvas); - try { - final Service service = - AtomicOperationUtils.getServiceObjectByNameAndVersion(DESIGNER, serviceName, serviceVersion); - final Resource resourceToAdd = - AtomicOperationUtils.getResourceObjectByNameAndVersion(DESIGNER, resourceName, resourceVersion); - final ComponentInstance componentInstance = AtomicOperationUtils - .addComponentInstanceToComponentContainer(resourceToAdd, service, DESIGNER, true, - String.valueOf(pointFromCanvasCenter.getX()), String.valueOf(pointFromCanvasCenter.getY())) - .left().value(); + final int maxRetries = 3; + Exception lastException = null; + for (int attempt = 1; attempt <= maxRetries; attempt++) { + final Point freePositionInCanvas = getFreePositionInCanvas(20); + final Point pointFromCanvasCenter = calculateOffsetFromCenter(freePositionInCanvas); + try { + final Service service = + AtomicOperationUtils.getServiceObjectByNameAndVersion(DESIGNER, serviceName, serviceVersion); + final Resource resourceToAdd = + AtomicOperationUtils.getResourceObjectByNameAndVersion(DESIGNER, resourceName, resourceVersion); + final ComponentInstance componentInstance = AtomicOperationUtils + .addComponentInstanceToComponentContainer(resourceToAdd, service, DESIGNER, true, + String.valueOf(pointFromCanvasCenter.getX()), String.valueOf(pointFromCanvasCenter.getY())) + .left().value(); - LOGGER.debug("Created instance {} in the Service {}", componentInstance.getName(), serviceName); - return componentInstance; - } catch (final Exception e) { - throw new CompositionCanvasRuntimeException("Could not create node through the API", e); + LOGGER.debug("Created instance {} in the Service {}", componentInstance.getName(), serviceName); + return componentInstance; + } catch (final Exception e) { + lastException = e; + LOGGER.warn("Attempt {}/{} to create node on service canvas failed: {}", attempt, maxRetries, e.getMessage()); + if (attempt < maxRetries) { + try { + Thread.sleep(1000L * attempt); + } catch (final InterruptedException ie) { + Thread.currentThread().interrupt(); + break; + } + } + } } + throw new CompositionCanvasRuntimeException("Could not create node through the API", lastException); } public ComponentInstance createNodeOnResourceCanvas(final String serviceName, final String serviceVersion, final String resourceName, final String resourceVersion) { - final Point freePositionInCanvas = getFreePositionInCanvas(20); - final Point pointFromCanvasCenter = calculateOffsetFromCenter(freePositionInCanvas); - try { - final Resource service = AtomicOperationUtils.getResourceObjectByNameAndVersion(DESIGNER, serviceName, serviceVersion); - final Resource resourceToAdd = AtomicOperationUtils.getResourceObjectByNameAndVersion(DESIGNER, resourceName, resourceVersion); - final ComponentInstance componentInstance = - AtomicOperationUtils.addComponentInstanceToComponentContainer(resourceToAdd, service, DESIGNER, true, - String.valueOf(pointFromCanvasCenter.getX()), String.valueOf(pointFromCanvasCenter.getY())).left().value(); + final int maxRetries = 3; + Exception lastException = null; + for (int attempt = 1; attempt <= maxRetries; attempt++) { + final Point freePositionInCanvas = getFreePositionInCanvas(20); + final Point pointFromCanvasCenter = calculateOffsetFromCenter(freePositionInCanvas); + try { + final Resource service = AtomicOperationUtils.getResourceObjectByNameAndVersion(DESIGNER, serviceName, serviceVersion); + final Resource resourceToAdd = AtomicOperationUtils.getResourceObjectByNameAndVersion(DESIGNER, resourceName, resourceVersion); + final ComponentInstance componentInstance = + AtomicOperationUtils.addComponentInstanceToComponentContainer(resourceToAdd, service, DESIGNER, true, + String.valueOf(pointFromCanvasCenter.getX()), String.valueOf(pointFromCanvasCenter.getY())).left().value(); - LOGGER.debug("Created instance {} in the Service {}", componentInstance.getName(), serviceName); - return componentInstance; - } catch (final Exception e) { - throw new CompositionCanvasRuntimeException("Could not create node through the API", e); + LOGGER.debug("Created instance {} in the Service {}", componentInstance.getName(), serviceName); + return componentInstance; + } catch (final Exception e) { + lastException = e; + LOGGER.warn("Attempt {}/{} to create node on resource canvas failed: {}", attempt, maxRetries, e.getMessage()); + if (attempt < maxRetries) { + try { + Thread.sleep(1000L * attempt); + } catch (final InterruptedException ie) { + Thread.currentThread().interrupt(); + break; + } + } + } } + throw new CompositionCanvasRuntimeException("Could not create node through the API", lastException); } private Point getFreePositionInCanvas(int maxAttempts) { diff --git a/integration-tests/src/test/java/org/onap/sdc/frontend/ci/tests/utilities/NotificationComponent.java b/integration-tests/src/test/java/org/onap/sdc/frontend/ci/tests/utilities/NotificationComponent.java index 2ffe658465..d09f103b22 100644 --- a/integration-tests/src/test/java/org/onap/sdc/frontend/ci/tests/utilities/NotificationComponent.java +++ b/integration-tests/src/test/java/org/onap/sdc/frontend/ci/tests/utilities/NotificationComponent.java @@ -24,6 +24,7 @@ import lombok.AllArgsConstructor; import lombok.Getter; import org.onap.sdc.frontend.ci.tests.pages.AbstractPageObject; import org.openqa.selenium.By; +import org.openqa.selenium.StaleElementReferenceException; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.slf4j.Logger; @@ -39,11 +40,26 @@ public class NotificationComponent extends AbstractPageObject { public void waitForNotification(final NotificationType notificationType, final int timeout) { final By messageLocator = getMessageLocator(notificationType); - final WebElement webElement = waitForElementVisibility(messageLocator, timeout); - webElement.click(); + waitForElementVisibility(messageLocator, timeout); + clickNotification(messageLocator); waitForElementInvisibility(messageLocator, 5); } + private void clickNotification(final By messageLocator) { + int attempts = 0; + while (attempts < 3) { + try { + final WebElement element = findElement(messageLocator); + element.click(); + return; + } catch (final StaleElementReferenceException e) { + LOGGER.warn("StaleElementReferenceException on notification click, attempt {}", attempts + 1); + attempts++; + } + } + LOGGER.warn("Failed to click notification after {} attempts", attempts); + } + private By getMessageLocator(final NotificationType notificationType) { return By.xpath(getMessageXpath(notificationType)); } diff --git a/pom.xml b/pom.xml index 1ca33e47d8..6057125345 100644 --- a/pom.xml +++ b/pom.xml @@ -1021,7 +1021,7 @@ Modifications copyright (c) 2018-2019 Nokia onboarding common-app-logging common-app-api - common-be-tests-utils + common-be-tests-utils common-be catalog-dao catalog-model @@ -1035,6 +1035,30 @@ Modifications copyright (c) 2018-2019 Nokia integration-tests + + run-integration-tests-playwright + + true + true + true + + + onboarding + common-app-logging + common-app-api + common-be-tests-utils + common-be + catalog-dao + catalog-model + catalog-be + catalog-be-plugins + asdctool + catalog-ui + catalog-fe + utils/webseal-simulator + integration-tests + + -- 2.16.6