diff --git a/Dockerfile b/Dockerfile index e4b39a8..9277094 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM grafana/k6:0.47.0-with-browser +FROM grafana/k6:0.56.0-with-browser ENV TERM=xterm-256color ENV PROJECT_DIR=/home/k6 diff --git a/docker-compose.local.yml b/docker-compose.local.yml deleted file mode 100644 index aa90d9d..0000000 --- a/docker-compose.local.yml +++ /dev/null @@ -1,85 +0,0 @@ -version: '3.4' - -services: - k6: - container_name: "loadtesting_environment" - build: - context: . - dockerfile: Dockerfile - networks: - - loadtesting - # - spryker_demo_private - # - spryker_demo_public - # - spryker_b2b_marketplace_dev_private - # - spryker_b2c_marketplace_dev_private - # - spryker_b2b_dev_private - # - spryker_b2c_dev_private - # - spryker_b2b_marketplace_private - # - spryker_b2c_marketplace_private - # - spryker_b2b_private - # - spryker_b2c_private - - environment: - - K6_SCRIPT=${K6_SCRIPT} - - K6_HOSTENV=${K6_HOSTENV:-local} - - K6_INSECURE_SKIP_TLS_VERIFY=true - - K6_STATSD_ENABLE_TAGS=true - - K6_OUT= - - K6_STATSD_ADDR=${K6_STATSD_ADDR} - - K6_STATSD_NAMESPACE=${K6_STATSD_NAMESPACE} - - K6_HTTP_DEBUG=${K6_HTTP_DEBUG} - - K6_NO_THRESHOLDS=${K6_NO_THRESHOLDS:-true} - - DATA_EXCHANGE_PAYLOAD_PUT_CHUNK_SIZE=${DATA_EXCHANGE_PAYLOAD_PUT_CHUNK_SIZE:-100} - - DATA_EXCHANGE_PAYLOAD_PATCH_CHUNK_SIZE=${DATA_EXCHANGE_PAYLOAD_PATCH_CHUNK_SIZE:-100} - - DATA_EXCHANGE_PAYLOAD_CHUNK_SIZE=${DATA_EXCHANGE_PAYLOAD_CHUNK_SIZE:-100} - - DATA_EXCHANGE_TARGET_CATALOG_SIZE_POST=${DATA_EXCHANGE_TARGET_CATALOG_SIZE_POST:-3000} - - DATA_EXCHANGE_TARGET_CATALOG_SIZE_PUT_PATCH=${DATA_EXCHANGE_TARGET_CATALOG_SIZE_PUT_PATCH:-1000} - - DATA_EXCHANGE_THREADS_PATCH=${DATA_EXCHANGE_THREADS_PATCH:-1} - - DATA_EXCHANGE_THREADS_PUT=${DATA_EXCHANGE_THREADS_PUT:-1} - - DATA_EXCHANGE_THREADS_POST=${DATA_EXCHANGE_THREADS_POST:-1} - - DATA_EXCHANGE_TWO_LOCALES=${DATA_EXCHANGE_TWO_LOCALES:-1} - - DATA_EXCHANGE_CONCRETE_MAX_AMOUNT=${DATA_EXCHANGE_CONCRETE_MAX_AMOUNT:-1} - - DATA_EXCHANGE_DEBUG=${DATA_EXCHANGE_DEBUG:-0} - - GIT_HASH=${GIT_HASH:-'-'} - - GIT_BRANCH=${GIT_BRANCH:-'-'} - - GIT_REPO=${GIT_REPO:-'-'} - - GIT_TAG=${GIT_TAG:-'-'} - - SPRYKER_TEST_RUN_ID=${SPRYKER_TEST_RUN_ID:-'-'} - - SPRYKER_TEST_RUNNER_HOSTNAME=${SPRYKER_TEST_RUNNER_HOSTNAME:-'-'} - - BASIC_AUTH_USERNAME=${BASIC_AUTH_USERNAME} - - BASIC_AUTH_PASSWORD=${BASIC_AUTH_PASSWORD} - volumes: - - .:/home/k6 - ports: - - "6565:6565" - -networks: - loadtesting: - spryker-cloud_private: - external: true - spryker-cloud_public: - external: true - spryker_public: - external: true - spryker_private: - external: true - spryker_demo_public: - external: true - spryker_demo_private: - external: true - spryker_b2b_marketplace_private: - external: true - spryker_b2b_marketplace_dev_private: - external: true - spryker_b2c_marketplace_dev_private: - external: true - spryker_b2c_dev_private: - external: true - spryker_b2b_dev_private: - external: true - spryker_b2c_marketplace_private: - external: true - spryker_b2c_private: - external: true - spryker_b2b_private: - external: true diff --git a/docker-compose.yml b/docker-compose.yml index a710f6d..d22e17a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.4' - services: k6: container_name: "loadtesting_environment" @@ -8,30 +6,30 @@ services: dockerfile: Dockerfile networks: - loadtesting -# - spryker_b2b_marketplace_private -# - spryker_b2b_marketplace_dev_private -# - spryker_b2c_marketplace_dev_private -# - spryker_b2b_dev_private -# - spryker_b2c_dev_private -# - spryker_b2b_marketplace_private -# - spryker_b2c_marketplace_private -# - spryker_b2b_private -# - spryker_b2c_private + - spryker_b2b_dev_private + - spryker_b2b_dev_public + # - spryker_demo_private + # - spryker_demo_public + # - spryker_b2b_marketplace_dev_private + # - spryker_b2c_marketplace_dev_private + # - spryker_b2b_dev_private + # - spryker_b2c_dev_private + # - spryker_b2b_marketplace_private + # - spryker_b2c_marketplace_private + # - spryker_b2b_private + # - spryker_b2c_private + environment: - K6_SCRIPT=${K6_SCRIPT} - - K6_HOSTENV=${K6_HOSTENV} + - K6_HOSTENV=${K6_HOSTENV:-local} + - K6_INSECURE_SKIP_TLS_VERIFY=true - K6_STATSD_ENABLE_TAGS=true - - K6_OUT=${K6_OUT} + - K6_OUT= - K6_STATSD_ADDR=${K6_STATSD_ADDR} - K6_STATSD_NAMESPACE=${K6_STATSD_NAMESPACE} - K6_HTTP_DEBUG=${K6_HTTP_DEBUG} - - K6_NO_THRESHOLDS=${K6_NO_THRESHOLDS} - - GIT_HASH=${GIT_HASH} - - GIT_BRANCH=${GIT_BRANCH} - - GIT_REPO=${GIT_REPO} - - GIT_TAG=${GIT_TAG} - - BASIC_AUTH_USERNAME=${BASIC_AUTH_USERNAME} - - BASIC_AUTH_PASSWORD=${BASIC_AUTH_PASSWORD} + - K6_BROWSER_DEBUG=${K6_BROWSER_DEBUG} + - K6_NO_THRESHOLDS=${K6_NO_THRESHOLDS:-true} - DATA_EXCHANGE_PAYLOAD_PUT_CHUNK_SIZE=${DATA_EXCHANGE_PAYLOAD_PUT_CHUNK_SIZE:-100} - DATA_EXCHANGE_PAYLOAD_PATCH_CHUNK_SIZE=${DATA_EXCHANGE_PAYLOAD_PATCH_CHUNK_SIZE:-100} - DATA_EXCHANGE_PAYLOAD_CHUNK_SIZE=${DATA_EXCHANGE_PAYLOAD_CHUNK_SIZE:-100} @@ -43,52 +41,38 @@ services: - DATA_EXCHANGE_TWO_LOCALES=${DATA_EXCHANGE_TWO_LOCALES:-1} - DATA_EXCHANGE_CONCRETE_MAX_AMOUNT=${DATA_EXCHANGE_CONCRETE_MAX_AMOUNT:-1} - DATA_EXCHANGE_DEBUG=${DATA_EXCHANGE_DEBUG:-0} + - GIT_HASH=${GIT_HASH:-'-'} + - GIT_BRANCH=${GIT_BRANCH:-'-'} + - GIT_REPO=${GIT_REPO:-'-'} + - GIT_TAG=${GIT_TAG:-'-'} + - SPRYKER_TEST_RUN_ID=${SPRYKER_TEST_RUN_ID:-'-'} + - SPRYKER_TEST_RUNNER_HOSTNAME=${SPRYKER_TEST_RUNNER_HOSTNAME:-'-'} + - BASIC_AUTH_USERNAME=${BASIC_AUTH_USERNAME} + - BASIC_AUTH_PASSWORD=${BASIC_AUTH_PASSWORD} + - K6_BROWSER_ENABLED=true volumes: - .:/home/k6 ports: - "6565:6565" - links: - - "nrstatsd" - - "newrelic" - - newrelic-php-daemon: - image: newrelic/php-daemon - networks: - - loadtesting - - nrstatsd: - container_name: "new_relic_statsd" - image: newrelic/nri-statsd:latest - environment: - - NR_ACCOUNT_ID=${NR_ACCOUNT_ID} - # It appears to be a bug in the statsd image. Despite calling the API key, it only works with a New Relic license key. - - NR_API_KEY=${NRIA_LICENSE_KEY} - - NR_EU_REGION=${NR_EU_REGION} - - NR_LOG_METRICS=true - hostname: "localpocmachine" - ports: - - 8125:8125/udp - networks: - - loadtesting - restart: unless-stopped - - newrelic: - container_name: newrelic-infra - image: newrelic/infrastructure:latest - cap_add: - - SYS_PTRACE - network_mode: host - pid: host - privileged: true - volumes: - - "/:/host:ro" - - "/var/run/docker.sock:/var/run/docker.sock" - environment: - - NRIA_LICENSE_KEY=${NRIA_LICENSE_KEY} - restart: unless-stopped - + networks: loadtesting: + spryker_b2b_dev_private: + external: true + spryker_b2b_dev_public: + external: true +# spryker-cloud_private: +# external: true +# spryker-cloud_public: +# external: true +# spryker_public: +# external: true +# spryker_private: +# external: true +# spryker_demo_public: +# external: true +# spryker_demo_private: +# external: true # spryker_b2b_marketplace_private: # external: true # spryker_b2b_marketplace_dev_private: @@ -99,8 +83,6 @@ networks: # external: true # spryker_b2b_dev_private: # external: true -# spryker_b2b_marketplace_private: -# external: true # spryker_b2c_marketplace_private: # external: true # spryker_b2c_private: diff --git a/docker-compose_main.yml b/docker-compose_main.yml new file mode 100644 index 0000000..a937fb8 --- /dev/null +++ b/docker-compose_main.yml @@ -0,0 +1,115 @@ +version: '3.4' + +services: + k6: + container_name: "loadtesting_environment" + build: + context: . + dockerfile: Dockerfile + networks: + - loadtesting + - spryker_b2b_dev_private + - spryker_b2b_dev_public +# - spryker_b2b_marketplace_private +# - spryker_b2b_marketplace_dev_private +# - spryker_b2c_marketplace_dev_private +# - spryker_b2b_dev_private +# - spryker_b2c_dev_private +# - spryker_b2b_marketplace_private +# - spryker_b2c_marketplace_private +# - spryker_b2b_private +# - spryker_b2c_private + environment: + - K6_SCRIPT=${K6_SCRIPT} + - K6_HOSTENV=${K6_HOSTENV} + - K6_STATSD_ENABLE_TAGS=true + - K6_OUT=${K6_OUT} + - K6_STATSD_ADDR=${K6_STATSD_ADDR} + - K6_STATSD_NAMESPACE=${K6_STATSD_NAMESPACE} + - K6_HTTP_DEBUG=${K6_HTTP_DEBUG} + - K6_NO_THRESHOLDS=${K6_NO_THRESHOLDS} + - GIT_HASH=${GIT_HASH} + - GIT_BRANCH=${GIT_BRANCH} + - GIT_REPO=${GIT_REPO} + - GIT_TAG=${GIT_TAG} + - BASIC_AUTH_USERNAME=${BASIC_AUTH_USERNAME} + - BASIC_AUTH_PASSWORD=${BASIC_AUTH_PASSWORD} + - DATA_EXCHANGE_PAYLOAD_PUT_CHUNK_SIZE=${DATA_EXCHANGE_PAYLOAD_PUT_CHUNK_SIZE:-100} + - DATA_EXCHANGE_PAYLOAD_PATCH_CHUNK_SIZE=${DATA_EXCHANGE_PAYLOAD_PATCH_CHUNK_SIZE:-100} + - DATA_EXCHANGE_PAYLOAD_CHUNK_SIZE=${DATA_EXCHANGE_PAYLOAD_CHUNK_SIZE:-100} + - DATA_EXCHANGE_TARGET_CATALOG_SIZE_POST=${DATA_EXCHANGE_TARGET_CATALOG_SIZE_POST:-3000} + - DATA_EXCHANGE_TARGET_CATALOG_SIZE_PUT_PATCH=${DATA_EXCHANGE_TARGET_CATALOG_SIZE_PUT_PATCH:-1000} + - DATA_EXCHANGE_THREADS_PATCH=${DATA_EXCHANGE_THREADS_PATCH:-1} + - DATA_EXCHANGE_THREADS_PUT=${DATA_EXCHANGE_THREADS_PUT:-1} + - DATA_EXCHANGE_THREADS_POST=${DATA_EXCHANGE_THREADS_POST:-1} + - DATA_EXCHANGE_TWO_LOCALES=${DATA_EXCHANGE_TWO_LOCALES:-1} + - DATA_EXCHANGE_CONCRETE_MAX_AMOUNT=${DATA_EXCHANGE_CONCRETE_MAX_AMOUNT:-1} + - DATA_EXCHANGE_DEBUG=${DATA_EXCHANGE_DEBUG:-0} + volumes: + - .:/home/k6 + ports: + - "6565:6565" + links: + - "nrstatsd" + - "newrelic" + + newrelic-php-daemon: + image: newrelic/php-daemon + networks: + - loadtesting + + nrstatsd: + container_name: "new_relic_statsd" + image: newrelic/nri-statsd:latest + environment: + - NR_ACCOUNT_ID=${NR_ACCOUNT_ID} + # It appears to be a bug in the statsd image. Despite calling the API key, it only works with a New Relic license key. + - NR_API_KEY=${NRIA_LICENSE_KEY} + - NR_EU_REGION=${NR_EU_REGION} + - NR_LOG_METRICS=true + hostname: "localpocmachine" + ports: + - 8125:8125/udp + networks: + - loadtesting + restart: unless-stopped + + newrelic: + container_name: newrelic-infra + image: newrelic/infrastructure:latest + cap_add: + - SYS_PTRACE + network_mode: host + pid: host + privileged: true + volumes: + - "/:/host:ro" + - "/var/run/docker.sock:/var/run/docker.sock" + environment: + - NRIA_LICENSE_KEY=${NRIA_LICENSE_KEY} + restart: unless-stopped + +networks: + loadtesting: + spryker_b2b_dev_private: + external: true + spryker_b2b_dev_public: + external: true +# spryker_b2b_marketplace_private: +# external: true +# spryker_b2b_marketplace_dev_private: +# external: true +# spryker_b2c_marketplace_dev_private: +# external: true +# spryker_b2c_dev_private: +# external: true +# spryker_b2b_dev_private: +# external: true +# spryker_b2b_marketplace_private: +# external: true +# spryker_b2c_marketplace_private: +# external: true +# spryker_b2c_private: +# external: true +# spryker_b2b_private: +# external: true diff --git a/environments/B2B.json b/environments/B2B.json index a0f9940..a59a659 100644 --- a/environments/B2B.json +++ b/environments/B2B.json @@ -4,7 +4,8 @@ "storefrontApiUrl": "http://glue.%store%.spryker.local", "backofficeUrl": "http://backoffice.%store%.spryker.local", "backofficeApiUrl": "http://backend-api.%store%.spryker.local", - "stores": ["eu", "us"] + "stores": ["eu", "us"], + "backofficeSessionKey": "backoffice-%store%-spryker-local" }, "testing": { "storefrontUrl": "https://yves.%store%.spryker-b2bperformance.cloud.spryker.toys", diff --git a/helpers/admin-helper.js b/helpers/admin-helper.js index bc59fe3..595badc 100644 --- a/helpers/admin-helper.js +++ b/helpers/admin-helper.js @@ -1,4 +1,69 @@ -export default class AdminHelper { +import { sleep } from 'k6'; +import { browser } from 'k6/browser'; + +const formSelector = 'form[name="auth"]'; +const usernameInputSelector = `${formSelector} input[name="auth[username]"]`; +const passwordInputSelector = `${formSelector} input[name="auth[password]"]`; +const authSubmitSelector = `${formSelector} button[type="submit"]`; + +const firstSalesOrderViewButtonSelector = '.dataTable tbody tr:nth-child(1) .btn-view'; +const omsFormSubmitSelector = 'button#oms_trigger_form_submit'; + +class ContextStorage { + #context; + #page; + + constructor(context, page) { + this.#context = context; + this.#page = page; + } + + setContext(context) { + this.#context = context; + } + + getContext() { + return this.#context; + } + + setPage(page) { + this.#page = page; + } + + getPage() { + return this.#page; + } +} + +export class AdminHelper { + constructor(urlHelper, http, assertionsHelper, browserHelper) { + this.urlHelper = urlHelper; + this.http = http; + this.assertionsHelper = assertionsHelper; + this.browserHelper = browserHelper; + + this.session = null; + this.contextStorage = new ContextStorage(null, null); + + this.backofficeSessionKey = 'backoffice-eu-spryker-local'; + + const backofficeBaseUrl = this.urlHelper.getBackofficeBaseUrl(); + + const loginUrl = backofficeBaseUrl + '/security-gui/login'; + const loginCheckUrl = backofficeBaseUrl + '/login_check'; + const salesUrl = backofficeBaseUrl + '/sales'; + const salesTableUrl = backofficeBaseUrl + '/sales/index/table'; + const salesDetailUrl = backofficeBaseUrl + '/sales/detail'; + const omsTriggerUrl = backofficeBaseUrl + '/oms/trigger/submit-trigger-event-for-order'; + + this.loginUrl = loginUrl; + this.loginCheckUrl = loginCheckUrl; + this.salesUrl = salesUrl; + this.salesTableUrl = salesTableUrl; + this.salesDetailUrl = salesDetailUrl; + this.omsTriggerUrl = omsTriggerUrl; + } + getDefaultAdminEmail() { return __ENV.DEFAULT_ADMIN_EMAIL ? __ENV.DEFAULT_ADMIN_EMAIL : 'admin@spryker.com'; } @@ -6,4 +71,130 @@ export default class AdminHelper { getDefaultAdminPassword() { return __ENV.DEFAULT_ADMIN_PASSWORD ? __ENV.DEFAULT_ADMIN_PASSWORD : 'change123'; } + + async loginBackoffice() { + const context = await browser.newContext(); + this.contextStorage.setContext(context); + const page = await context.newPage(); + this.contextStorage.setPage(page); + + await page.goto(this.loginUrl); + await page.waitForSelector(formSelector, {timeout: 5000}); + + await page.locator(usernameInputSelector).type(this.getDefaultAdminEmail()); + await page.locator(passwordInputSelector).type(this.getDefaultAdminPassword()); + await page.locator(authSubmitSelector).click(); + + await page.waitForLoadState('networkidle', {timeout: 5000}); + } + + async goToSalesPage() { + const page = this.contextStorage.getPage(); + + page.on('metric', (metric) => { + metric.tag({ + name: this.salesTableUrl, + matches: [ + { + url: new RegExp(this.salesTableUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), + method: 'GET', + }, + ], + }); + }); + + await page.goto(this.salesUrl, { timeout: 5000 }); + await page.waitForLoadState('networkidle', { timeout: 2000 }); + } + + async openFirstSalesOrder() { + let self = this; + + const page = this.contextStorage.getPage(); + const buttonLocator = await page.locator(firstSalesOrderViewButtonSelector); + + page.on('metric', (metric) => { + metric.tag({ + name: self.salesDetailUrl, + matches: [ + { + url: new RegExp(self.salesDetailUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), + method: 'GET', + }, + ], + }); + }); + + await buttonLocator.waitFor({state: 'visible'}); + await buttonLocator.click(firstSalesOrderViewButtonSelector); + + await page.waitForLoadState('networkidle', { timeout: 2000 }); + } + + async waitForOrderHasOmsTriggerButton() { + const page = this.contextStorage.getPage(); + + await page.locator(omsFormSubmitSelector).waitFor({state: 'visible'}); + + await this.recursiveWaitForSelectorWithText(omsFormSubmitSelector, 'Pay', 60 ); + } + + async payForTheOrder() { + const page = this.contextStorage.getPage(); + + await page.waitForLoadState('networkidle', { timeout: 5000 }); + + page.on('metric', (metric) => { + metric.tag({ + name: this.omsTriggerUrl, + matches: [ + {url: new RegExp(this.omsTriggerUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), method: 'POST'}, + ], + }); + }); + + // const submitButtons = await page.$$(omsFormSubmitSelector); + // const submitButton = await this.findButtonByText(submitButtons, 'Pay'); + + const payButton = await page.locator('#order-overview > .row .ibox-content > .row > .col-md-12 .ibox-content form:nth-child(2) button'); + + await payButton.click({ + force: true, + noWaitAfter: true, + }); + + await page.waitForLoadState('networkidle', { timeout: 5000 }); + } + + async recursiveWaitForSelectorWithText(selector, text, maxRetries = 10) { + const page = this.contextStorage.getPage(); + + for (let attempt = 0; attempt < maxRetries; attempt++) { + const submitButtons = await page.$$(selector); + for (let i = 0; i < submitButtons.length; i++) { + if ((await submitButtons[i].innerText()) === text) { + return; + } + } + + sleep(1); + await page.reload(); + } + } + + async findButtonByText(buttons, text) { + for (let i = 0; i < buttons.length; i++) { + if ((await buttons[i].innerText()) === text) { + return buttons[i]; + } + } + + return null; + } + + async takeScreenshot(fileName = new Date().toString() + 'screenshot.png') { + const page = this.contextStorage.getPage(); + await page.screenshot({ path: 'results/' + fileName }); + console.log(`Screenshot saved to ${fileName}`); + } } diff --git a/helpers/browser-helper.js b/helpers/browser-helper.js index 2415cea..0fc0c29 100644 --- a/helpers/browser-helper.js +++ b/helpers/browser-helper.js @@ -1,10 +1,14 @@ -import {browser} from 'k6/experimental/browser'; +import { browser } from 'k6/browser'; export class BrowserHelper { constructor(urlHelper, customerHelper, assertionsHelper) { this.urlHelper = urlHelper; this.customerHelper = customerHelper; this.assertionsHelper = assertionsHelper; + + this.url = ''; + this.context = null; + this.page = null; } async getLoggedInUserContext() { diff --git a/package-lock.json b/package-lock.json index b4d1cbe..df446ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,7 @@ "packages": { "": { "devDependencies": { + "@types/k6": "^0.54.2", "eslint": "^8.39.0" } }, @@ -132,6 +133,13 @@ "node": ">= 8" } }, + "node_modules/@types/k6": { + "version": "0.54.2", + "resolved": "https://registry.npmjs.org/@types/k6/-/k6-0.54.2.tgz", + "integrity": "sha512-B5LPxeQm97JnUTpoKNE1UX9jFp+JiJCAXgZOa2P7aChxVoPQXKfWMzK+739xHq3lPkKj1aV+HeOxkP56g/oWBg==", + "dev": true, + "license": "MIT" + }, "node_modules/acorn": { "version": "8.8.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", @@ -265,10 +273,11 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -894,10 +903,11 @@ } }, "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -1091,10 +1101,11 @@ } }, "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } diff --git a/package.json b/package.json index c84445b..418cdbf 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "devDependencies": { + "@types/k6": "^0.54.2", "eslint": "^8.39.0" } } diff --git a/page-objects/abstract-page.js b/page-objects/abstract-page.js new file mode 100644 index 0000000..4aecde5 --- /dev/null +++ b/page-objects/abstract-page.js @@ -0,0 +1,10 @@ +export class AbstractPage { + constructor(page, baseUrl) { + this.page = page; + this.baseUrl = baseUrl; + } + + setPage(page) { + this.page = page; + } +} \ No newline at end of file diff --git a/page-objects/backoffice/login-page.js b/page-objects/backoffice/login-page.js new file mode 100644 index 0000000..c8dc615 --- /dev/null +++ b/page-objects/backoffice/login-page.js @@ -0,0 +1,26 @@ +import { AbstractPage } from '../abstract-page'; + +const url = '/security-gui/login'; + +const formSelector = 'form[name="auth"]'; +const inputEmailSelector = `${formSelector} input[name="auth[username]"]`; +const inputPasswordSelector = `${formSelector} input[name="auth[password]"]`; +const submitButtonSelector = `${formSelector} button[type="submit"]`; + +export class LoginPage extends AbstractPage { + async open() { + await this.page.goto(`${this.baseUrl}${url}`); + } + + async fillEmail(email) { + await this.page.locator(inputEmailSelector).type(email); + } + + async fillPassword(password) { + await this.page.locator(inputPasswordSelector).type(password); + } + + async submitForm() { + await this.page.locator(submitButtonSelector).click(); + } +} \ No newline at end of file diff --git a/shell/functions.sh b/shell/functions.sh index ffaab42..6353fec 100644 --- a/shell/functions.sh +++ b/shell/functions.sh @@ -87,11 +87,11 @@ build_k6_docker_command() { command="docker-compose run --rm \ -v $(pwd):/scripts \ - -u $(id -u):$(id -g) \ -e 'SPRYKER_TEST_RUN_ID=$testRunId' \ -e 'SPRYKER_TEST_RUNNER_HOSTNAME=$(hostname)' \ -e 'SPRYKER_TEST_PATH=$relativePath' \ -e 'K6_BROWSER_ENABLED=true' \ + -e 'K6_BROWSER_HEADLESS=true' \ k6 run $relativePath \ --summary-trend-stats='avg,min,med,max,p(90),p(95),count' \ --out json='$reportFile'" @@ -99,6 +99,44 @@ build_k6_docker_command() { echo "$command" } +build_k6_local_command() { + local relativePath="$1" + local reportFile="$2" + local testRunId="$3" + + # Check if 'k6' executable is available + if ! command -v k6 &>/dev/null; then + echo >&2 + echo -e "\e[33mError: 'k6' is not installed or not in PATH. Please install 'k6' and try again.\e[0m" >&2 + return 1 + fi + + if [ -z "$testRunId" ]; then + testRunId=$(generate_uuid) # Call generate_uuid to get a UUID + echo >&2 + echo -e "\e[33m--------------------------------------------------------------------------------\e[0m" >&2 + echo -e "\e[33mYou did not specify the test run id.\e[0m" >&2 + echo -e "\e[33mA UUId will be generated and used.\e[0m" >&2 + echo -e "\e[33m--------------------------------------------------------------------------------\e[0m" >&2 + return 1; + fi + + command="K6_HOSTENV=local \ + K6_BROWSER_HEADLESS=false \ + KK6_BROWSER_ARGS=\"--enable-automation\" \ + PROJECT_DIR=$(pwd) \ + GIT_REPO=B2B \ + GIT_BRANCH=B2B \ + GIT_HASH=B2B \ + SPRYKER_TEST_RUN_ID=$testRunId \ + SPRYKER_TEST_RUNNER_HOSTNAME=$(hostname) \ + k6 run $relativePath \ + --summary-trend-stats='avg,min,med,max,p(90),p(95),count' \ + --out json='$reportFile'" + + echo "$command" +} + # Helper method to create a folder if it does not exist create_folder_if_not_existant() { local folder="$1" diff --git a/shell/run-a-single-test.sh b/shell/run-a-single-test.sh index f82313b..3f5b63e 100755 --- a/shell/run-a-single-test.sh +++ b/shell/run-a-single-test.sh @@ -2,12 +2,25 @@ source shell/functions.sh -# Check if an argument was provided to the script -if [ $# -eq 1 ]; then - # Use the argument as the "file" variable - file="$1" -else - # Prompt the user for input and store it in the "file" variable +# Initialize default values +local_mode=false + +# Parse command-line arguments +while [[ "$#" -gt 0 ]]; do + case "$1" in + --local) + local_mode=true + shift + ;; + *) + file="$1" + shift + ;; + esac +done + +# Prompt the user for input if no file argument was provided +if [ -z "$file" ]; then echo -n "Enter the path to the test file (e.g., /tests/b2b/sapi/tests/cart/B2B-SAPI4-carts.js): " read file fi @@ -23,12 +36,21 @@ $(create_folder_if_not_existant "$outputFolder") # Get the path of the current file relative to the original directory testFile=${file#$(pwd)/} -# Construct the docker command -if ! command=$(build_k6_docker_command "$testFile" "$reportFile" "$testRunId"); then - exit 1; -fi +# Check if --local flag is set +if $local_mode; then + if ! command=$(build_k6_local_command "$testFile" "$reportFile" "$testRunId"); then + exit 1 + fi -echo "Running command: '$command'" + echo "Running command in local mode: '$command'" +else + # Construct the docker command + if ! command=$(build_k6_docker_command "$testFile" "$reportFile" "$testRunId"); then + exit 1; + fi + + echo "Running command: '$command'" +fi # Record the start time start_timer diff --git a/tests/abstract-scenario.js b/tests/abstract-scenario.js index cec01cb..7ed9247 100644 --- a/tests/abstract-scenario.js +++ b/tests/abstract-scenario.js @@ -8,7 +8,7 @@ import { Trend } from 'k6/metrics'; import CustomerHelper from '../helpers/customer-helper.js'; import { AssertionsHelper } from '../helpers/assertions-helper.js'; import { BapiHelper } from '../helpers/bapi-helper.js'; -import AdminHelper from '../helpers/admin-helper.js'; +import { AdminHelper } from '../helpers/admin-helper.js'; export class AbstractScenario { // eslint-disable-next-line no-unused-vars @@ -27,12 +27,13 @@ export class AbstractScenario { this.environmentConfig = loadEnvironmentConfig(this.environment); this.urlHelper = new UrlHelper(this.environmentConfig); this.customerHelper = new CustomerHelper(); - this.adminHelper = new AdminHelper(); this.assertionsHelper = new AssertionsHelper(); + this.browserHelper = new BrowserHelper(this.urlHelper, this.customerHelper, this.assertionsHelper); + this.adminHelper = new AdminHelper(this.urlHelper, this.http, this.assertionsHelper, this.browserHelper); this.cartHelper = new CartHelper(this.urlHelper, this.http, this.customerHelper, this.assertionsHelper); this.bapiHelper = new BapiHelper(this.urlHelper, this.http, this.adminHelper, this.assertionsHelper); this.storefrontHelper = new StorefrontHelper(this.urlHelper, this.http, this.customerHelper, this.assertionsHelper); - this.browserHelper = new BrowserHelper(this.urlHelper, this.customerHelper, this.assertionsHelper); + } createTrendMetric(name) { diff --git a/tests/b2b/backoffice/tests/order-management/tests/order-management-list.js b/tests/b2b/backoffice/tests/order-management/tests/order-management-list.js new file mode 100644 index 0000000..bb0934a --- /dev/null +++ b/tests/b2b/backoffice/tests/order-management/tests/order-management-list.js @@ -0,0 +1,32 @@ +import { SharedOrderManagementListScenario } from '../../../../../cross-product/backoffice/scenarios/order-management/shared-order-management-list-scenario.js'; +import { CheckoutScenario } from '../../../../sapi/scenarios/checkout/checkout-scenario.js'; +import { loadDefaultOptions } from '../../../../../../lib/utils.js'; +export { handleSummary } from '../../../../../../helpers/summary-helper.js'; + +const sharedCheckoutScenario = new CheckoutScenario('B2B'); +const sharedOrderManagementListScenario = new SharedOrderManagementListScenario('B2B', sharedCheckoutScenario); +const backofficeUrl = sharedOrderManagementListScenario.urlHelper.getBackofficeBaseUrl(); + +export const options = loadDefaultOptions(); +options.scenarios = { + Order_Management_List: { + exec: 'executeSharedOrderManagementListScenario', + executor: 'shared-iterations', + vus: 1, + iterations: 10, + options: { + browser: { + type: 'chromium', + }, + }, + }, +}; + +options.thresholds = { + [`browser_http_req_duration{url:${backofficeUrl}/sales}`]: ['avg<600'], + [`browser_http_req_duration{url:${backofficeUrl}/sales/index/table}`]: ['avg<600'], +} + +export async function executeSharedOrderManagementListScenario() { + await sharedOrderManagementListScenario.execute(); +} \ No newline at end of file diff --git a/tests/b2b/backoffice/tests/order-management/tests/order-management-pay.js b/tests/b2b/backoffice/tests/order-management/tests/order-management-pay.js new file mode 100644 index 0000000..295e207 --- /dev/null +++ b/tests/b2b/backoffice/tests/order-management/tests/order-management-pay.js @@ -0,0 +1,32 @@ +import { SharedOrderManagementPayScenario } from '../../../../../cross-product/backoffice/scenarios/order-management/shared-order-management-pay-scenario.js'; +import { CheckoutScenario } from '../../../../sapi/scenarios/checkout/checkout-scenario.js'; +import { loadDefaultOptions } from '../../../../../../lib/utils.js'; +export { handleSummary } from '../../../../../../helpers/summary-helper.js'; + +const sharedCheckoutScenario = new CheckoutScenario('B2B'); +const sharedOrderManagementPayScenario = new SharedOrderManagementPayScenario('B2B', sharedCheckoutScenario); +const backofficeUrl = sharedOrderManagementPayScenario.urlHelper.getBackofficeBaseUrl(); + +export const options = loadDefaultOptions(); +options.scenarios = { + Order_Management_Pay: { + exec: 'executeSharedOrderManagementPayScenario', + executor: 'shared-iterations', + vus: 1, + iterations: 1, + options: { + browser: { + type: 'chromium', + }, + }, + }, +}; + +options.thresholds = { + [`browser_http_req_duration{url:${backofficeUrl}/sales/detail}`]: ['avg<600'], + [`browser_http_req_duration{url:${backofficeUrl}/oms/trigger/submit-trigger-event-for-order}`]: ['avg<600'], +} + +export async function executeSharedOrderManagementPayScenario() { + await sharedOrderManagementPayScenario.execute(); +} \ No newline at end of file diff --git a/tests/b2b/backoffice/tests/order-management/tests/test.js b/tests/b2b/backoffice/tests/order-management/tests/test.js new file mode 100644 index 0000000..0fb735b --- /dev/null +++ b/tests/b2b/backoffice/tests/order-management/tests/test.js @@ -0,0 +1,103 @@ +// this file is only for testing custom scripts +import { browser } from 'k6/browser'; + +export const options = { + scenarios: { + default: { + executor: 'per-vu-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + console.log('Opening a new browser context'); + const context = await browser.newContext(); + console.log('Opening a new page'); + const page = await context.newPage(); + + // Listen to browser console messages + page.on('console', (msg) => console.log(`[Browser Console]: ${msg.type()}: ${msg.text()}`)); + + try { + // Navigate to login page + await page.goto('http://backoffice.eu.spryker.local/security-gui/login'); + await page.waitForLoadState('domcontentloaded', { timeout: 5000 }); + + // Login actions + await page.locator('input[name="auth[username]"]').fill('admin@spryker.com'); + await page.locator('input[name="auth[password]"]').fill('change123'); + await page.locator('form[name="auth"] button[type="submit"]').click(); + await page.screenshot({ path: 'results/screenshot.png' }); + + // Navigate to sales page + await page.goto('http://backoffice.eu.spryker.local/sales'); + await page.waitForLoadState('networkidle', { timeout: 5000 }); + await page.screenshot({ path: 'results/sales.png' }); + + // Navigate to sales details page + await page.locator('.dataTable tbody tr:nth-child(1) .btn-view').click(); + await page.waitForLoadState('networkidle', { timeout: 5000 }); + await page.screenshot({ path: 'results/sales-detail.png' }); + + console.log('Triggering the ship event'); + + // Custom retry logic for waiting and clicking the button + const buttonLocator = page.locator('#oms_trigger_form_submit'); + let retries = 0; + let maxRetries = 5; + + while (retries < maxRetries) { + try { + console.log(`Checking for the button (attempt ${retries + 1})...`); + + // Wait for the button to be present in the DOM + await buttonLocator.waitFor({ state: 'attached', timeout: 5000 }); + console.log('Button is present in the DOM.'); + + // Check if button is visible and interactable + const isVisible = await buttonLocator.isVisible(); + if (isVisible) { + console.log('Button is visible, attempting to click...'); + await buttonLocator.click(); + console.log('Button clicked!'); + break; // Exit retry loop if successful + } else { + console.log('Button is not visible. Retrying...'); + } + } catch (error) { + console.log('Error interacting with the button:', error.message); + } + + // Increment retry count and delay between retries + retries++; + if (retries < maxRetries) { + console.log('Retrying in 2 seconds...'); + await page.waitForTimeout(2000); + } else { + throw new Error('Max retries reached, button not clickable'); + } + } + + // Wait for network to be idle after the action + console.log('Waiting for the network to be idle'); + await page.waitForLoadState('networkidle', { timeout: 10000 }); + await page.screenshot({ path: 'results/sales-detail-oms.png' }); + } catch (error) { + console.error( + 'Error during page operations:', + error.stack || error.message || JSON.stringify(error, null, 2) + ); + } finally { + if (page) { + await page.close(); + } + if (context) { + await context.close(); + } + } +} \ No newline at end of file diff --git a/tests/cross-product/backoffice/scenarios/order-management/shared-order-management-list-scenario.js b/tests/cross-product/backoffice/scenarios/order-management/shared-order-management-list-scenario.js new file mode 100644 index 0000000..f3cce82 --- /dev/null +++ b/tests/cross-product/backoffice/scenarios/order-management/shared-order-management-list-scenario.js @@ -0,0 +1,42 @@ +import { AbstractScenario } from '../../../../abstract-scenario.js'; +import { group } from 'k6'; + +const numberOfOrders = 1; + +export class SharedOrderManagementListScenario extends AbstractScenario { + constructor(environment, checkoutScenario) { + super(environment); + this.checkoutScenario = checkoutScenario; + this.ordersCreated = false; + } + + async createOrders() { + let self = this; + + group('Create Orders', function () { + for (let i = 0; i < numberOfOrders; i++) { + self.checkoutScenario.execute(1); + } + }); + + this.ordersCreated = true; + } + + async execute() { + let self = this; + + if (!this.ordersCreated) { + await this.createOrders(); + } + + try { + await self.adminHelper.loginBackoffice(); + await self.adminHelper.goToSalesPage(); + } finally { + let context = self.adminHelper.contextStorage.getContext(); + await self.adminHelper.contextStorage.getPage().close(); + await context.close(); + } + + } +} \ No newline at end of file diff --git a/tests/cross-product/backoffice/scenarios/order-management/shared-order-management-pay-scenario.js b/tests/cross-product/backoffice/scenarios/order-management/shared-order-management-pay-scenario.js new file mode 100644 index 0000000..0c1dcad --- /dev/null +++ b/tests/cross-product/backoffice/scenarios/order-management/shared-order-management-pay-scenario.js @@ -0,0 +1,44 @@ +import { AbstractScenario } from '../../../../abstract-scenario.js'; +import { group, sleep } from 'k6'; + +export class SharedOrderManagementPayScenario extends AbstractScenario { + constructor(environment, checkoutScenario) { + super(environment); + this.checkoutScenario = checkoutScenario; + } + + createOrders() { + let self = this; + + group('Create Order', function () { + self.checkoutScenario.execute(1); + }); + } + + async execute() { + let self = this; + + this.createOrders(); + + try { + await self.adminHelper.loginBackoffice(); + sleep(1); + await self.adminHelper.goToSalesPage(); + sleep(1); + await self.adminHelper.openFirstSalesOrder(); + sleep(1); + await self.adminHelper.waitForOrderHasOmsTriggerButton(); + sleep(1); + await self.adminHelper.payForTheOrder(); + sleep(1); + + } catch (error) { + console.log(error); + self.adminHelper.takeScreenshot('error.png'); + } finally { + const context = self.adminHelper.contextStorage.getContext(); + await self.adminHelper.contextStorage.getPage().close(); + await context.close(); + } + } +} \ No newline at end of file