diff --git a/tests/qa/.env.example b/tests/qa/.env.example new file mode 100644 index 000000000..14534a580 --- /dev/null +++ b/tests/qa/.env.example @@ -0,0 +1,21 @@ +# JDoe test single site, PHP x.x +# playwright-utils config +WP_BASE_URL='' +WP_USERNAME=admin +WP_PASSWORD=pass +WP_BASIC_AUTH_USER=admin +WP_BASIC_AUTH_PASS=pass +STORAGE_STATE_PATH='./storage-states' +STORAGE_STATE_PATH_ADMIN='./storage-states/admin.json' + +# WooCommerce API config +WC_API_KEY=api_client_key +WC_API_SECRET=api_client_secret + +# Percy config +PERCY_TOKEN=web_abc123 + +# Xray in Jira +XRAY_CLIENT_ID='B3B1169C13144A268BD8F34290B6EB4A' +XRAY_CLIENT_SECRET='c913b13fa41b8e3053818ab20120db64f877f05b211b02c96743c0f7271879f7' +# TEST_EXEC_KEY='' \ No newline at end of file diff --git a/tests/qa/.gitignore b/tests/qa/.gitignore new file mode 100644 index 000000000..3416a593e --- /dev/null +++ b/tests/qa/.gitignore @@ -0,0 +1,13 @@ +.idea +.vscode +node_modules +playwright-utils +playwright-report +playwright/.cache +storage-states +test-results +package-lock.json + +.env* +!.env.example +.DS_Store \ No newline at end of file diff --git a/tests/qa/.npmrc b/tests/qa/.npmrc new file mode 100644 index 000000000..426867c0c --- /dev/null +++ b/tests/qa/.npmrc @@ -0,0 +1 @@ +@inpsyde:registry=https://npm.pkg.github.com \ No newline at end of file diff --git a/tests/qa/README.md b/tests/qa/README.md new file mode 100644 index 000000000..7e11f01fd --- /dev/null +++ b/tests/qa/README.md @@ -0,0 +1,175 @@ +# PCP Tests + +Folder for Playwright tests. Depends on [`@inpsyde/playwright-utils`](https://github.com/inpsyde/playwright-utils) package. + +## Folder structure + +Tests for PCP project are stored under the __tests/qa__ dir. + +### Project structure + +- `resources` - files with test-data, images, project related installation packages, types, etc. + +- `tests` - test specifications. For payment plugins contains following folders: + + - `01-plugin-foundation` - general tests for plugin installation, uninstallation, activation, deactivation, display of plugin in __WooCommerce -> Settings -> Payments__. + + __The rest of the tests will be added over time__ + + \* - folders are numerated on purpose, to force correct sequence of tests - from basic to advanced. Although each test should be independent and work separately, it is better to start testing from `plugin-foundation` and move to more complex tests. + + \*\* - folders and numeration can be different, based on project requirements. + +- `utils` - project related utility files, built on top of `@inpsyde/playwright-utils`. + + - `admin` - functionality for operating dashboard pages. + + - `frontend` - functionality for operating frontend pages, hosted checkout pages (payment system provider's pages). + + - `test.ts` - declarations of project related test fixtures. + + - other project related functionality, like helpers, APIs, urls. + +- `.env`, `playwright.config.ts`, `package.json` - see below. + +### Setup @inpsyde/playwright-utils as a node package + +1. Remove `"workspaces": [ "playwright-utils" ]` from `package.json`. + +2. In the root of the tests (which is __qa__) run following command: + +```bash +npm run setup:tests +``` + +### Setup @inpsyde/playwright-utils for local development + +1. Add `"workspaces": [ "playwright-utils" ]` to `package.json`. + +2. Delete `@inpsyde/playwright-utils` from `/node_modules`. + +3. In the root of the project (which is __qa__ in this case) run following command: + + ```bash + git clone https://github.com/inpsyde/playwright-utils.git + ``` + + [`@inpsyde/playwright-utils`](https://github.com/inpsyde/playwright-utils) repository should be cloned as `playwright-utils` right inside the root directory of project. + +4. Restart VSCode editor. This will create `playwright-utils` instance in the source control tab of VSCode editor. + +5. Run following command: + + ```bash + npm run setup:utils + ``` + +6. `@inpsyde/playwright-utils` should reappear in node_modules. Following message (coming from `tsc-watch`) should be displayed in the terminal: + + ```bash + 10:00:00 - Found 0 errors. Watching for file changes. + ``` + +7. If you plan to make changes in `playwright-utils` keep current terminal window opened and create another instance of terminal. + +## Project configuration + +Project requires a working WordPress website with WooCommmerce, `.env` file and configured Playwright. + +1. [SSE setup](https://inpsyde.atlassian.net/wiki/spaces/AT/pages/3175907370/Self+Service+WordPress+Environment) - will be deprecated in Q1 of 2025. + +2. Tested user with Administrator role should be created + +2. In the Dashboard navigate to __Settings -> Permalinks__ and select `Post name` in __Permalink structure__ for correct format of REST path. + +3. Install __Storefront__ theme. + +4. Install __WooCommerce__ plugin. + +5. In __WooCommerce -> Settings -> Advanced -> REST API__ create _Consumer Key_ and _Secret_ with Read/Write permissions and store them in `.env`. + +6. To avoid conflicts make sure any other payment plugins like are deleted. + +7. Configure `.env` file following [these steps](https://github.com/inpsyde/playwright-utils?tab=readme-ov-file#env-variables). See also `/tests/qa/.env.example`. + +8. Configure `playwright.config.ts` of the project following [these steps](https://github.com/inpsyde/playwright-utils?tab=readme-ov-file#playwright-configuration). + +9. Reporting to __Xray in Jira__ is configured [this way](https://github.com/inpsyde/playwright-utils/blob/main/docs/test-report-api/report-to-xray.md). + +## Run tests + +To execute tests, in the terminal, navigate to the __qa__ directory of the project (e.g. `cd tests/qa`) and run following command: + +```bash +npx playwright test +``` + +### Additional options to run tests from command line + +- Add scripts to `package.json` of the project (eligible for Windows, not tested on other OS): + + ```json + "scripts": { + "test:smoke": "npx playwright test --grep \"@Smoke\"", + "test:critical": "npx playwright test --grep \"@Critical\"", + "test:ui": "npx playwright test --grep \"UI\"", + "test:functional": "npx playwright test --grep \"Functional\"", + "test:all": "npm run test:ui & npm run test:functional" + }, + ``` + + Run script with the following command: + + ```bash + npm run test:critical + ``` + +- Run several tests by test ID (on Windows, Powershell): + + ```bash + npx playwright test --grep --% "WOL-123^|WOL-124^|WOL-125" + ``` + + It may be required additionally to specify the project (if tests relate to more then one project): + + ```bash + npx playwright test --project "project-name" --grep --% "WOL-123^|WOL-124^|WOL-125" + ``` + +## Autotest Execution workflow + +1. Create Test Execution ticket in Jira, named after the tested plugin version, for example "Test Execution for v2.3.4-rc1, PHP8.1". + +2. Link release ticket (via `tests: WOL-234`). + +3. Set Test Execution ticket status `In progress`. + +4. Add/update test execution ticket key in `.env` file of the project (`TEST_EXEC_KEY`). + +5. Download tested plugin `.zip` package (usually attached to release ticket) and add it to `/project//resources/files`. You may need to remove version number from the file name. + +6. Optional: delete previous version of tested plugin from the website if you don't execute __plugin foundation__ tests. + +7. Start autotest execution from command line for the defined scope of tests (e.g. all, Critical, etc.). You should see `Test execution Jira key: WOL-234` in the terminal. + +8. When finished test results should be exported to the specified test execution ticket in Jira. + +9. Analyze failed tests (if any). Restart execution for failed tests, possibly in debug mode: + + ```bash + npx playwright test --grep --% "WOL-123^|WOL-124^|WOL-125" --debug + ``` + +10. Report bugs (if any) and attach them to the test-runs of failed tests (Click "Create defect" or "Add defect" on test execution screen). + +11. If needed fix failing tests in a new branch, create a PR and assign it for review. + +12. Set Test execution ticket status to `Done`. + +## Coding standards + +Before commiting changes run following command: + +```bash +npm run lint:js:fix +``` diff --git a/tests/qa/global-setup.ts b/tests/qa/global-setup.ts new file mode 100644 index 000000000..6b53b756f --- /dev/null +++ b/tests/qa/global-setup.ts @@ -0,0 +1,29 @@ +/** + * External dependencies + */ +import { FullConfig } from '@playwright/test'; +import { restLogin, guestStorageState } from '@inpsyde/playwright-utils/build'; + +async function globalSetup( config: FullConfig ) { + const projectUse = config.projects[ 0 ].use; + + await restLogin( { + baseURL: projectUse.baseURL, + storageStatePath: String( projectUse.storageState ), + httpCredentials: projectUse.httpCredentials, + user: { + // @ts-ignore + username: process.env.WP_USERNAME, + // @ts-ignore + password: process.env.WP_PASSWORD, + }, + } ); + + await guestStorageState( { + baseURL: projectUse.baseURL, + httpCredentials: projectUse.httpCredentials, + storageStatePath: `${ process.env.STORAGE_STATE_PATH }/guest.json`, + } ); +} + +export default globalSetup; diff --git a/tests/qa/package.json b/tests/qa/package.json new file mode 100644 index 000000000..468f9514b --- /dev/null +++ b/tests/qa/package.json @@ -0,0 +1,53 @@ +{ + "name": "@inpsyde/playwright-tests", + "version": "1.0.0", + "description": "Monorepo for Playwright tests", + "main": "index.js", + "repository": { + "type": "git", + "url": "git+https://github.com/inpsyde/playwright-tests.git" + }, + "author": { + "name": "Syde GmbH", + "email": "hello@syde.com", + "url": "https://syde.com/" + }, + "license": "GPL-3.0-or-later", + "bugs": { + "url": "https://github.com/woocommerce/woocommerce-paypal-payments/issues" + }, + "homepage": "https://github.com/woocommerce/woocommerce-paypal-payments#readme", + "workspaces": [ + "playwright-utils" + ], + "dependencies": { + "dotenv": "^16.3.1", + "dotenv-cli": "^7.3.0", + "playwright": "^1.49.1", + "yarn": "^1.22.21" + }, + "devDependencies": { + "@percy/cli": "^1.28.0", + "@percy/playwright": "^1.0.4", + "@playwright/test": "^1.38.1", + "@types/node": "^20.8.4", + "@wordpress/scripts": "^25.0.0" + }, + "scripts": { + "all": "npx playwright test --workers=1 --project \"all\"", + "setup:tests": "npm install && npx playwright install", + "setup:utils": "npm run setup:tests && cd ./playwright-utils && yarn devLocal", + "lint:md": "wp-scripts lint-md-docs ./**/*.md README.md", + "lint:js": "wp-scripts lint-js projects/**/*.{ts,tsx,mjs} projects/**/.*/*.{ts,tsx,mjs}", + "lint:js:fix": "wp-scripts lint-js --resolve-plugins-relative-to ./ --fix '**/*.{ts,tsx,mjs}'" }, + "eslintConfig": { + "extends": [ + "plugin:@wordpress/eslint-plugin/recommended" + ], + "rules": { + "@wordpress/dependency-group": "error", + "@wordpress/no-unsafe-wp-apis": "off", + "no-console": "off" + } + } +} diff --git a/tests/qa/parallel.projects.config.ts b/tests/qa/parallel.projects.config.ts new file mode 100644 index 000000000..29f0e8801 --- /dev/null +++ b/tests/qa/parallel.projects.config.ts @@ -0,0 +1,142 @@ +export const parallelProjects = [ + { + name: 'parallel-transaction-block', + dependencies: [ 'setup-gateway-block-paypal-pay-later-acdc' ], + testMatch: /01-block-germany\.spec/, + fullyParallel: true, + }, + { + name: 'parallel-transaction-classic', + dependencies: [ 'setup-gateway-classic-paypal-pay-later-acdc' ], + testMatch: /02-classic-germany\.spec/, + fullyParallel: true, + }, + { + name: 'parallel-transaction-block-excluding-tax', + dependencies: [ 'setup-gateway-block-paypal-pay-later-acdc' ], + testMatch: /03-block-germany-excluding-tax\.spec/, + fullyParallel: true, + }, + { + name: 'parallel-transaction-classic-excluding-tax', + dependencies: [ 'setup-gateway-classic-paypal-pay-later-acdc' ], + testMatch: /04-classic-germany-excluding-tax\.spec/, + fullyParallel: true, + }, + { + name: 'parallel-transaction-block-authorized', + dependencies: [ 'setup-gateway-block-paypal-pay-later-acdc' ], + testMatch: /05-block-germany-authorized\.spec/, + fullyParallel: true, + }, + { + name: 'parallel-transaction-classic-authorized', + dependencies: [ 'setup-gateway-classic-paypal-pay-later-acdc' ], + testMatch: /06-classic-germany-authorized\.spec/, + fullyParallel: true, + }, + { + name: 'parallel-transaction-block-button-orientation', + dependencies: [ 'setup-gateway-block-paypal-pay-later-acdc' ], + testMatch: /07-block-germany-button-orientation\.spec/, + fullyParallel: true, + }, + { + name: 'parallel-transaction-classic-button-orientation', + dependencies: [ 'setup-gateway-classic-paypal-pay-later-acdc' ], + testMatch: /08-classic-germany-button-orientation\.spec/, + fullyParallel: true, + }, + { + name: 'parallel-transaction-wp-debugging', + dependencies: [ 'setup-gateway-block-paypal-pay-later-acdc' ], + testMatch: /09-block-germany-wp-debugging\.spec/, + fullyParallel: true, + }, + { + name: 'parallel-transaction-classic-wp-debugging', + dependencies: [ 'setup-gateway-classic-paypal-pay-later-acdc' ], + testMatch: /10-classic-germany-wp-debugging\.spec/, + fullyParallel: true, + }, + { + name: 'parallel-transaction-classic-standard-card-button', + dependencies: [ 'setup-gateway-classic-standard-card-button' ], + testMatch: /11-classic-germany-standard-card-button\.spec/, + fullyParallel: true, + }, + { + name: 'parallel-transaction-classic-standard-card-button-excluding-tax', + dependencies: [ 'setup-gateway-classic-standard-card-button' ], + testMatch: + /12-classic-germany-standard-card-button-excluding-tax\.spec/, + fullyParallel: true, + }, + { + name: 'parallel-transaction-classic-standard-card-button-authorized', + dependencies: [ 'setup-gateway-classic-standard-card-button' ], + testMatch: /13-classic-germany-standard-card-button-authorized\.spec/, + fullyParallel: true, + }, + { + name: 'parallel-transaction-classic-debit-or-credit-card', + dependencies: [ 'setup-gateway-classic-debit-or-credit-card' ], + testMatch: /14-classic-germany-debit-or-credit-card\.spec/, + fullyParallel: true, + }, + { + name: 'parallel-transaction-classic-debit-or-credit-card-excluding-tax', + dependencies: [ 'setup-gateway-classic-debit-or-credit-card' ], + testMatch: + /15-classic-germany-debit-or-credit-card-excluding-tax\.spec/, + fullyParallel: true, + }, + { + name: 'parallel-transaction-classic-germany', + dependencies: [ 'setup-gateway-classic-pay-upon-invoice' ], + testMatch: /16-classic-germany-pay-upon-invoice\.spec/, + fullyParallel: true, + }, + { + name: 'parallel-transaction-classic-germany-excluding-tax', + dependencies: [ 'setup-gateway-classic-pay-upon-invoice' ], + testMatch: /17-classic-germany-pay-upon-invoice-excluding-tax\.spec/, + fullyParallel: true, + }, + { + name: 'parallel-transaction-classic-mexico', + dependencies: [ 'setup-gateway-classic-oxxo' ], + testMatch: /18-classic-mexico-oxxo\.spec/, + fullyParallel: true, + }, + { + name: 'parallel-transaction-classic-mexico-excluding-tax', + dependencies: [ 'setup-gateway-classic-oxxo' ], + testMatch: /19-classic-mexico-oxxo-excluding-tax\.spec/, + fullyParallel: true, + }, + { + name: 'parallel-transaction-classic-usa', + dependencies: [ 'setup-gateway-classic-venmo' ], + testMatch: /20-classic-usa-venmo\.spec/, + fullyParallel: true, + }, + { + name: 'parallel-transaction-classic-usa-excluding-tax', + dependencies: [ 'setup-gateway-classic-venmo' ], + testMatch: /21-classic-usa-venmo-excluding-tax\.spec/, + fullyParallel: true, + }, + { + name: 'parallel-transaction-classic-specific-merchant', + dependencies: [ 'setup-store-classic-germany' ], + testMatch: /22-classic-germany-specific-merchant\.spec/, + fullyParallel: true, + }, + { + name: 'parallel-refund', + dependencies: [ 'setup-gateway-classic-paypal-pay-later-acdc' ], + testMatch: /parallel-refund-block-germany\.spec/, + fullyParallel: true, + }, +]; diff --git a/tests/qa/playwright.config.ts b/tests/qa/playwright.config.ts new file mode 100644 index 000000000..0d708165e --- /dev/null +++ b/tests/qa/playwright.config.ts @@ -0,0 +1,107 @@ +/** + * External dependencies + */ +import { defineConfig, devices } from '@playwright/test'; +require( 'dotenv' ).config(); + +/** + * Internal dependencies + */ +import { storeSetupProjects } from './tests/.setup/store'; +import { pcpSetupProjects } from './tests/.setup/pcp'; +import { gatewaySetupProjects } from './tests/.setup/gateways'; +import { parallelProjects } from './parallel.projects.config'; + +export default defineConfig( { + testDir: 'tests', + expect: { + timeout: 20 * 1000, + }, + timeout: 2 * 60 * 1000, + /* Run tests in files in parallel */ + 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 : 1, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: process.env.CI + ? [ + [ 'list' ], + // [ 'html', { outputFolder: 'playwright-report' } ], + [ + '@inpsyde/playwright-utils/build/integration/jira/xray-reporter.js', + { + apiClient: { + client_id: process.env.XRAY_CLIENT_ID, + client_secret: process.env.XRAY_CLIENT_SECRET, + }, + testExecutionKey: process.env.TEST_EXEC_KEY, + }, + ], + ] + : [ + [ 'list' ], + // [ 'html', { outputFolder: 'playwright-report' } ], + [ + '@inpsyde/playwright-utils/build/integration/jira/xray-reporter.js', + { + apiClient: { + client_id: process.env.XRAY_CLIENT_ID, + client_secret: process.env.XRAY_CLIENT_SECRET, + }, + testExecutionKey: process.env.TEST_EXEC_KEY, + }, + ], + ], + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + + globalSetup: require.resolve( './global-setup' ), + + use: { + baseURL: process.env.WP_BASE_URL, + + storageState: process.env.STORAGE_STATE_PATH_ADMIN, + + httpCredentials: { + username: process.env.WP_BASIC_AUTH_USER, + password: process.env.WP_BASIC_AUTH_PASS, + }, + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + + // Capture screenshot after each test failure. + screenshot: 'only-on-failure', //'off', // + + // Record video only when retrying a test for the first time. + video: 'retain-on-failure', //'on', // + + ...devices[ 'Desktop Chrome' ], + + viewport: { width: 1280, height: 850 }, + + launchOptions: { + // Put your chromium-specific args here + args: [ '--disable-web-security' ], + }, + }, + + projects: [ + { + name: 'setup-woocommerce', + testMatch: /woocommerce\.setup\.ts/, + fullyParallel: false, + }, + ...storeSetupProjects, + ...pcpSetupProjects, + ...gatewaySetupProjects, + { + name: 'all', + dependencies: [ 'setup-woocommerce' ], + testIgnore: '**/.parallel-specs/**', + }, + ], +} ); diff --git a/tests/qa/resources/cards.ts b/tests/qa/resources/cards.ts new file mode 100644 index 000000000..6a64e6d3d --- /dev/null +++ b/tests/qa/resources/cards.ts @@ -0,0 +1,39 @@ +const visa: WooCommerce.CreditCard = { + card_number: '4005519200000004', + // card_number: '4444333322221111', + // card_number: '4111111111111111', + expiration_date: '12/30', + card_cvv: '029', + card_type: 'VISA', +}; + +const visa3ds: WooCommerce.CreditCard = { + card_number: '4020024518402084', + expiration_date: '01/25', + card_cvv: '123', + card_type: 'VISA', + code_3ds: '1234', +}; + +const mastercard: WooCommerce.CreditCard = { + card_number: '2223000048400011', + expiration_date: '12/30', + card_cvv: '456', + card_type: 'Master', +}; + +const declined: WooCommerce.CreditCard = { + card_number: '4032037524607534', + expiration_date: '09/25', + card_cvv: '340', + card_type: 'VISA', +}; + +export const cards: { + [ key: string ]: WooCommerce.CreditCard; +} = { + visa, + visa3ds, + mastercard, + declined, +}; diff --git a/tests/qa/resources/disable-gutenberg-welcome-guide-plugin.json b/tests/qa/resources/disable-gutenberg-welcome-guide-plugin.json new file mode 100644 index 000000000..980cf8510 --- /dev/null +++ b/tests/qa/resources/disable-gutenberg-welcome-guide-plugin.json @@ -0,0 +1,7 @@ +{ + "name": "Disable Gutenberg Welcome Guide", + "slug": "disable-gutenberg-welcome-guide", + "zipFile": "disable-gutenberg-welcome-guide.zip", + "zipFilePath": "./resources/files/disable-gutenberg-welcome-guide.zip", + "version": "" +} \ No newline at end of file diff --git a/tests/qa/resources/disable-nonce-plugin.json b/tests/qa/resources/disable-nonce-plugin.json new file mode 100644 index 000000000..7e6963a35 --- /dev/null +++ b/tests/qa/resources/disable-nonce-plugin.json @@ -0,0 +1,7 @@ +{ + "name": "Disable nonce check", + "slug": "disable-nonce-check", + "zipFile": "disable-nonce.zip", + "zipFilePath": "./resources/files/disable-nonce.zip", + "version": "" +} \ No newline at end of file diff --git a/tests/qa/resources/disable-wc-setup-wizard-plugin.json b/tests/qa/resources/disable-wc-setup-wizard-plugin.json new file mode 100644 index 000000000..b69ec1bf3 --- /dev/null +++ b/tests/qa/resources/disable-wc-setup-wizard-plugin.json @@ -0,0 +1,7 @@ +{ + "name": "Disable WC Setup Wizard", + "slug": "disable-wc-setup-wizard", + "zipFile": "disable-wc-setup-wizard.zip", + "zipFilePath": "./resources/files/disable-wc-setup-wizard.zip", + "version": "" +} \ No newline at end of file diff --git a/tests/qa/resources/enable-vault-v2-plugin.json b/tests/qa/resources/enable-vault-v2-plugin.json new file mode 100644 index 000000000..016de46b8 --- /dev/null +++ b/tests/qa/resources/enable-vault-v2-plugin.json @@ -0,0 +1,7 @@ +{ + "name": "Enable Vault v2", + "slug": "enable-vault-v2", + "zipFile": "enable-vault-v2.zip", + "zipFilePath": "./resources/files/enable-vault-v2.zip", + "version": "" +} \ No newline at end of file diff --git a/tests/qa/resources/gateways.ts b/tests/qa/resources/gateways.ts new file mode 100644 index 000000000..92e7e67d6 --- /dev/null +++ b/tests/qa/resources/gateways.ts @@ -0,0 +1,84 @@ +/** + * Internal dependencies + */ +import { cards } from './cards'; +import { PcpPayment } from './types'; +import { payPalAccounts } from './paypal-accounts'; + +const country = 'germany'; + +export const payPal: PcpPayment = { + gatewayName: 'PayPal', + method: 'PayPal', + dataFundingSource: 'paypal', + gateway: 'ppcp-gateway', + payPalAccount: payPalAccounts[ country ], +}; + +export const payPalVaulted: PcpPayment = { + ...payPal, + isVaulted: true, +}; + +export const payLater: PcpPayment = { + gatewayName: 'PayPal Pay Later', + method: 'PayLater', + dataFundingSource: 'paylater', + gateway: 'ppcp-gateway', + payPalAccount: payPalAccounts[ country ], +}; + +export const oxxo: PcpPayment = { + gatewayName: 'OXXO', + method: 'OXXO', + dataFundingSource: 'oxxo', + gateway: 'ppcp-oxxo-gateway', +}; + +export const venmo: PcpPayment = { + gatewayName: 'Venmo', + method: 'Venmo', + dataFundingSource: 'venmo', + gateway: 'ppcp-gateway', + payPalAccount: payPalAccounts.usa, +}; + +export const acdc: PcpPayment = { + gatewayName: 'Debit & Credit Cards', + method: 'ACDC', + dataFundingSource: 'acdc', + gateway: 'ppcp-credit-card-gateway', + card: cards.visa, +}; + +export const acdc3ds: PcpPayment = { + gatewayName: 'Debit & Credit Cards', + method: 'ACDC3DS', + dataFundingSource: 'acdc', + gateway: 'ppcp-credit-card-gateway', + card: cards.visa3ds, +}; + +export const debitOrCreditCard: PcpPayment = { + gatewayName: 'Credit or debit cards (via PayPal)', //'Debit or Credit Cards',// + method: 'DebitOrCreditCard', + dataFundingSource: 'card', + gateway: 'ppcp-gateway', + card: cards.visa, +}; + +export const standardCardButton: PcpPayment = { + gatewayName: 'Debit & Credit Cards', + method: 'StandardCardButton', + dataFundingSource: 'card', + gateway: 'ppcp-card-button-gateway', + card: cards.visa, +}; + +export const payUponInvoice: PcpPayment = { + gatewayName: 'Pay upon Invoice', + method: 'PayUponInvoice', + dataFundingSource: 'pay_upon_invoice', + gateway: 'ppcp-pay-upon-invoice-gateway', + birthDate: '01.01.1991', +}; diff --git a/tests/qa/resources/index.ts b/tests/qa/resources/index.ts new file mode 100644 index 000000000..19925e9ed --- /dev/null +++ b/tests/qa/resources/index.ts @@ -0,0 +1,25 @@ +export { + shopSettings, + shippingZones, + flatRate, + freeShipping, + guests, + customers, + taxSettings, + coupons, + products, +} from '@inpsyde/playwright-utils/build/e2e/plugins/woocommerce'; +export * from './cards'; +export * from './gateways'; +export * from './merchants'; +export * from './orders'; +export * from './woocommerce-config'; +export * from './types'; + +export { default as pcpPlugin } from './pcp-plugin.json'; +export { default as disableNoncePlugin } from './disable-nonce-plugin.json'; +export { default as enableVaultV2Plugin } from './enable-vault-v2-plugin.json'; +export { default as subscriptionsPlugin } from './woocommerce-subscriptions-plugin.json'; +export { default as wpDebuggingPlugin } from './wp-debugging-plugin.json'; +export { default as disableWcSetupWizard } from './disable-wc-setup-wizard-plugin.json'; +export { default as disableGutenbergWelcomeGuide } from './disable-gutenberg-welcome-guide-plugin.json'; diff --git a/tests/qa/resources/merchants.ts b/tests/qa/resources/merchants.ts new file mode 100644 index 000000000..788760683 --- /dev/null +++ b/tests/qa/resources/merchants.ts @@ -0,0 +1,59 @@ +/** + * Internal dependencies + */ +import { PcpMerchant } from './types'; + +const invalid: PcpMerchant = { + email: '123sb-vzlb326615278@business.example.com', + password: '123-*Gv5z#X', + client_id: + '123AV7C3agl0lCTUEi4gm-5Ku9vagoqOxzKQoc9BIvirXGr5lRrbX3TyxOFzHWTTUXs74BI_XkK3C5LemHZ', + client_secret: + '123EFzI8FCerbL8qvMs0baJiVAqvA4SwXka3WM-WWE-o0c6b2acaGu_Q7a4n1nEGQf2-dnCgtmKLgm0AXmC', + account_id: '123MQEBC2LND7J3L', +}; +const germany: PcpMerchant = { + email: 'sb-vzlb326615278@business.example.com', + password: '-*Gv5z#X', + client_id: + 'AV7C3agl0lCTUEi4gm-5Ku9vagoqOxzKQoc9BIvirXGr5lRrbX3TyxOFzHWTTUXs74BI_XkK3C5LemHZ', + client_secret: + 'EFzI8FCerbL8qvMs0baJiVAqvA4SwXka3WM-WWE-o0c6b2acaGu_Q7a4n1nEGQf2-dnCgtmKLgm0AXmC', + account_id: 'MQEBC2LND7J3L', +}; +const usa: PcpMerchant = { + email: 'sb-hskbh29881597@business.example.com', + password: '', + client_id: + 'AUuFjwA2QOosiCIaE9etgEq3GN8R1EfEWUAJ1NhuZ6LW6z0-TAJRRmTyO3vJof5dFKVJNHfer8k83eDc', + client_secret: + 'EIXJLpKYTzPF4bS5ZKZYxO47t99i52OBBuQ_3JMpuV-G_SUnoWg0YHpZxa_JSxdlmzbn8AGs7_5pGB5R', + account_id: 'RRPUW6YXX22W2', +}; + +// const usa: PcpMerchant = { +// email: 'sb-1vqws26578261@business.example.com', +// password: 'xc\'#0\'Xf', +// client_id: 'ASld_H2Yc6Fpkh6Q0NZuQvckyAjAfWfm_kW-IpROgYPUhpsAxU6KCYRcEw1sf60n6xULzj4K_n_Jg_Y5', +// client_secret: 'EJ88I1fmwA7QkGvkkuYC4z1_R_M6ZfV511OpRDPI61D6TuAaCm9rULpr-wxqeVTKJfZV1Zzszj9XtDVR', +// account_id: 'QTGBXV4YCQLD6' +// }; + +const mexico: PcpMerchant = { + email: 'sb-zafdx26915775@business.example.com', + password: 'S*j$Sty5', + client_id: + 'AVDHMoK8jjBdN1aHHU1KhxBuqX60dBVDSGDxMcUDT5KsEm2ukaSoAah6B7BDQKSrUNO01tlus4aZMDqW', + client_secret: + 'ENTGiM-woqy7YtzmOTbvAjz7KXsfTkaovkIua9dUB3uVcXjMewUsY1vXovfLnIDiE3oTj2CBnpqd6nCg', + account_id: 'Q5UMB8J7HH6DN', +}; + +export const merchants: { + [ key: string ]: PcpMerchant; +} = { + invalid, + germany, + usa, + mexico, +}; diff --git a/tests/qa/resources/orders.ts b/tests/qa/resources/orders.ts new file mode 100644 index 000000000..6cbf132a8 --- /dev/null +++ b/tests/qa/resources/orders.ts @@ -0,0 +1,17 @@ +/** + * External dependencies + */ +import { orders } from '@inpsyde/playwright-utils/build/e2e/plugins/woocommerce'; +/** + * Internal dependencies + */ +import { merchants } from '.'; + +const country = 'germany'; +const merchant = merchants[ country ]; + +for ( const order in orders ) { + orders[ order ].merchant = merchant; +} + +export { orders }; diff --git a/tests/qa/resources/paypal-accounts.ts b/tests/qa/resources/paypal-accounts.ts new file mode 100644 index 000000000..5b632dba0 --- /dev/null +++ b/tests/qa/resources/paypal-accounts.ts @@ -0,0 +1,27 @@ +/** + * Internal dependencies + */ +import { PayPalAccount } from './types'; + +const germany: PayPalAccount = { + email: 'sb-pshsb27001797@personal.example.com', + password: '#D0:c!DO', +}; + +const usa: PayPalAccount = { + email: 'sb-tb1aj26722276@personal.example.com', + password: 'Z9+6Az-G', +}; + +const mexico: PayPalAccount = { + email: 'sb-z2t6h26996394@personal.example.com', + password: 'C-cg>?33', +}; + +export const payPalAccounts: { + [ key: string ]: PayPalAccount; +} = { + germany, + usa, + mexico, +}; diff --git a/tests/qa/resources/pcp-plugin.json b/tests/qa/resources/pcp-plugin.json new file mode 100644 index 000000000..1ab6e0744 --- /dev/null +++ b/tests/qa/resources/pcp-plugin.json @@ -0,0 +1,9 @@ +{ + "name": "WooCommerce PayPal Payments", + "slug": "woocommerce-paypal-payments", + "zipFile": "woocommerce-paypal-payments.zip", + "oldVersionZipFile": "old-pcp-version.zip", + "zipFilePath": "./resources/files/woocommerce-paypal-payments.zip", + "oldVerionZipFilePath": "./resources/files/old-pcp-version.zip", + "version": "2.3.0-rc2" +} \ No newline at end of file diff --git a/tests/qa/resources/types.ts b/tests/qa/resources/types.ts new file mode 100644 index 000000000..faeab29e0 --- /dev/null +++ b/tests/qa/resources/types.ts @@ -0,0 +1,95 @@ +export type PcpMerchant = { + email: string; + password: string; + client_id: string; + client_secret: string; + account_id: string; +}; + +export type PayPalAccount = { + email: string; + password: string; +}; + +export type PcpPaymentMethod = + | 'PayPal' + | 'PayLater' + | 'OXXO' + | 'Venmo' + | 'ACDC' + | 'ACDC3DS' + | 'DebitOrCreditCard' + | 'StandardCardButton' + | 'PayUponInvoice'; + +export type PcpFundingSource = + | 'paypal' + | 'paylater' + | 'oxxo' + | 'venmo' + | 'acdc' + | 'card' + | 'pay_upon_invoice'; + +export type PcpPayment = { + gatewayName: string; + method: PcpPaymentMethod; + dataFundingSource: PcpFundingSource; + gateway: string; + card?: WooCommerce.CreditCard; + payPalAccount?: PayPalAccount; + useNotVaultedAccount?: PayPalAccount; + birthDate?: string; + isAuthorized?: boolean; + isVaulted?: boolean; + saveToAccount?: boolean; +}; + +export namespace PcpSettings { + export type StandardPayments = { [ key: string ]: any }; + + export type PayLater = { [ key: string ]: any }; + + export type AdvancedCardProcessing = { [ key: string ]: any }; + + export type StandardCardButton = { [ key: string ]: any }; + + export type Oxxo = { [ key: string ]: any }; + + export type PayUponInvoice = { [ key: string ]: any }; + + export type OnboardingStepTitle = + | 'PayPal Payments' + | 'Set up store type' + | 'Select product types' + | 'Choose checkout options' + | 'Connect your PayPal account'; + + export type OnboardingAdvancedOptions = { + enableSandboxMode: boolean; + enableManualConnection: boolean; + merchant: PcpMerchant; + }; + + export type OnboardingProductTypes = { + enableVirtual: boolean; + enablePhysicalGoods: boolean; + }; + + export type OnboardingCheckoutOptions = { + enableOptionalPaymentMethods: boolean; + }; +} + +export type PcpConfig = { + merchant?: PcpMerchant; + clearPCPDB?: boolean; + disconnectMerchant?: boolean; + enablePayUponInvoice?: boolean; + standardPayments?: PcpSettings.StandardPayments; + payLater?: PcpSettings.PayLater; + advancedCardProcessing?: PcpSettings.AdvancedCardProcessing; + standardCardButton?: PcpSettings.StandardCardButton; + oxxo?: PcpSettings.Oxxo; + payUponInvoice?: PcpSettings.PayUponInvoice; +}; diff --git a/tests/qa/resources/woocommerce-config.ts b/tests/qa/resources/woocommerce-config.ts new file mode 100644 index 000000000..7a0806b52 --- /dev/null +++ b/tests/qa/resources/woocommerce-config.ts @@ -0,0 +1,52 @@ +/** + * Internal dependencies + */ +import { shopSettings, customers } from '.'; + +const country = 'germany'; + +export const storeConfigDefault = { + classicPages: false, // false = block cart and checkout (default), true = classic cart & checkout pages + wpDebugging: false, // WP Debugging plugin is deactivated + subscription: false, // WC Subscription plugin is deactivated + settings: shopSettings[ country ], // WC general settings + customer: customers[ country ], // registered customer +}; + +export const storeConfigClassic = { + ...storeConfigDefault, + classicPages: true, +}; + +export const storeConfigGermany = { + ...storeConfigDefault, + customer: customers.germany, +}; + +export const storeConfigUsa = { + ...storeConfigDefault, + wpDebugging: true, + settings: shopSettings.usa, + customer: customers.usa, +}; + +export const storeConfigMexico = { + ...storeConfigDefault, + settings: shopSettings.mexico, + customer: customers.mexico, +}; + +const storeConfigSubscription = { + // requireFinalConfirmation: false, + subscription: true, +}; + +export const storeConfigSubscriptionGermany = { + ...storeConfigGermany, + ...storeConfigSubscription, +}; + +export const storeConfigSubscriptionUsa = { + ...storeConfigUsa, + ...storeConfigSubscription, +}; diff --git a/tests/qa/resources/woocommerce-subscriptions-plugin.json b/tests/qa/resources/woocommerce-subscriptions-plugin.json new file mode 100644 index 000000000..9ec1e60ed --- /dev/null +++ b/tests/qa/resources/woocommerce-subscriptions-plugin.json @@ -0,0 +1,7 @@ +{ + "name": "WooCommerce Subscriptions", + "slug": "woocommerce-subscriptions", + "zipFile": "woocommerce-subscriptions.zip", + "zipFilePath": "./resources/files/woocommerce-subscriptions.zip", + "version": "" +} \ No newline at end of file diff --git a/tests/qa/resources/wp-debugging-plugin.json b/tests/qa/resources/wp-debugging-plugin.json new file mode 100644 index 000000000..44b66e574 --- /dev/null +++ b/tests/qa/resources/wp-debugging-plugin.json @@ -0,0 +1,7 @@ +{ + "name": "WP Debugging", + "slug": "wp-debugging", + "zipFile": "", + "zipFilePath": "", + "version": "" +} \ No newline at end of file diff --git a/tests/qa/tests/.setup/gateways/index.ts b/tests/qa/tests/.setup/gateways/index.ts new file mode 100644 index 000000000..e216f4000 --- /dev/null +++ b/tests/qa/tests/.setup/gateways/index.ts @@ -0,0 +1,44 @@ +export const gatewaySetupProjects = [ + { + name: 'setup-gateway-block-paypal-pay-later-acdc', + dependencies: [ 'setup-pcp-block-germany' ], + testMatch: /setup-gateway-paypal-pay-later-acdc\.ts/, + fullyParallel: false, + }, + { + name: 'setup-gateway-classic-paypal-pay-later-acdc', + dependencies: [ 'setup-pcp-classic-germany' ], + testMatch: /setup-gateway-paypal-pay-later-acdc\.ts/, + fullyParallel: false, + }, + { + name: 'setup-gateway-classic-standard-card-button', + dependencies: [ 'setup-pcp-classic-germany' ], + testMatch: /setup-gateway-standard-card-button\.ts/, + fullyParallel: false, + }, + { + name: 'setup-gateway-classic-debit-or-credit-card', + dependencies: [ 'setup-pcp-classic-germany' ], + testMatch: /setup-gateway-debit-or-credit-card\.ts/, + fullyParallel: false, + }, + { + name: 'setup-gateway-classic-pay-upon-invoice', + dependencies: [ 'setup-pcp-classic-germany' ], + testMatch: /setup-gateway-pay-upon-invoice\.ts/, + fullyParallel: false, + }, + { + name: 'setup-gateway-classic-venmo', + dependencies: [ 'setup-pcp-classic-usa' ], + testMatch: /setup-gateway-venmo\.ts/, + fullyParallel: false, + }, + { + name: 'setup-gateway-classic-oxxo', + dependencies: [ 'setup-pcp-classic-mexico' ], + testMatch: /setup-gateway-oxxo\.ts/, + fullyParallel: false, + }, +]; \ No newline at end of file diff --git a/tests/qa/tests/.setup/gateways/setup-gateway-debit-or-credit-card.ts b/tests/qa/tests/.setup/gateways/setup-gateway-debit-or-credit-card.ts new file mode 100644 index 000000000..8fb2d8c3c --- /dev/null +++ b/tests/qa/tests/.setup/gateways/setup-gateway-debit-or-credit-card.ts @@ -0,0 +1,9 @@ +/** + * Internal dependencies + */ +import { test as setup } from '../../../utils'; +import { debitOrCreditCard } from '../../../resources'; + +setup( 'Setup Debit or Credit Card', async ( { utils } ) => { + await utils.pcpPaymentMethodIsEnabled( debitOrCreditCard.method ); +} ); diff --git a/tests/qa/tests/.setup/gateways/setup-gateway-oxxo.ts b/tests/qa/tests/.setup/gateways/setup-gateway-oxxo.ts new file mode 100644 index 000000000..454157501 --- /dev/null +++ b/tests/qa/tests/.setup/gateways/setup-gateway-oxxo.ts @@ -0,0 +1,9 @@ +/** + * Internal dependencies + */ +import { test as setup } from '../../../utils'; +import { oxxo } from '../../../resources'; + +setup( 'Setup OXXO', async ( { utils } ) => { + await utils.pcpPaymentMethodIsEnabled( oxxo.method ); +} ); diff --git a/tests/qa/tests/.setup/gateways/setup-gateway-pay-upon-invoice.ts b/tests/qa/tests/.setup/gateways/setup-gateway-pay-upon-invoice.ts new file mode 100644 index 000000000..49304885e --- /dev/null +++ b/tests/qa/tests/.setup/gateways/setup-gateway-pay-upon-invoice.ts @@ -0,0 +1,9 @@ +/** + * Internal dependencies + */ +import { test as setup } from '../../../utils'; +import { payUponInvoice } from '../../../resources'; + +setup( 'Setup Pay upon Invoice', async ( { utils } ) => { + await utils.pcpPaymentMethodIsEnabled( payUponInvoice.method ); +} ); diff --git a/tests/qa/tests/.setup/gateways/setup-gateway-paypal-pay-later-acdc.ts b/tests/qa/tests/.setup/gateways/setup-gateway-paypal-pay-later-acdc.ts new file mode 100644 index 000000000..654a7d329 --- /dev/null +++ b/tests/qa/tests/.setup/gateways/setup-gateway-paypal-pay-later-acdc.ts @@ -0,0 +1,11 @@ +/** + * Internal dependencies + */ +import { test as setup } from '../../../utils'; +import { acdc, payLater, payPal } from '../../../resources'; + +setup( 'Setup PayPal, Pay Later, ACDC', async ( { utils } ) => { + await utils.pcpPaymentMethodIsEnabled( payPal.method ); + await utils.pcpPaymentMethodIsEnabled( payLater.method ); + await utils.pcpPaymentMethodIsEnabled( acdc.method ); +} ); diff --git a/tests/qa/tests/.setup/gateways/setup-gateway-standard-card-button.ts b/tests/qa/tests/.setup/gateways/setup-gateway-standard-card-button.ts new file mode 100644 index 000000000..308a32611 --- /dev/null +++ b/tests/qa/tests/.setup/gateways/setup-gateway-standard-card-button.ts @@ -0,0 +1,9 @@ +/** + * Internal dependencies + */ +import { test as setup } from '../../../utils'; +import { standardCardButton } from '../../../resources'; + +setup( 'Setup Standard Card Button', async ( { utils } ) => { + await utils.pcpPaymentMethodIsEnabled( standardCardButton.method ); +} ); diff --git a/tests/qa/tests/.setup/gateways/setup-gateway-venmo.ts b/tests/qa/tests/.setup/gateways/setup-gateway-venmo.ts new file mode 100644 index 000000000..7260e36c1 --- /dev/null +++ b/tests/qa/tests/.setup/gateways/setup-gateway-venmo.ts @@ -0,0 +1,9 @@ +/** + * Internal dependencies + */ +import { test as setup } from '../../../utils'; +import { venmo } from '../../../resources'; + +setup( 'Setup Venmo', async ( { utils } ) => { + await utils.pcpPaymentMethodIsEnabled( venmo.method ); +} ); diff --git a/tests/qa/tests/.setup/pcp/index.ts b/tests/qa/tests/.setup/pcp/index.ts new file mode 100644 index 000000000..ca5e1e2ce --- /dev/null +++ b/tests/qa/tests/.setup/pcp/index.ts @@ -0,0 +1,26 @@ +export const pcpSetupProjects = [ + { + name: 'setup-pcp-block-germany', + dependencies: [ 'setup-store-block-germany' ], + testMatch: /setup-pcp-germany\.ts/, + fullyParallel: false, + }, + { + name: 'setup-pcp-classic-germany', + dependencies: [ 'setup-store-classic-germany' ], + testMatch: /setup-pcp-germany\.ts/, + fullyParallel: false, + }, + { + name: 'setup-pcp-classic-mexico', + dependencies: [ 'setup-store-classic-mexico' ], + testMatch: /setup-pcp-mexico\.ts/, + fullyParallel: false, + }, + { + name: 'setup-pcp-classic-usa', + dependencies: [ 'setup-store-classic-usa' ], + testMatch: /setup-pcp-usa\.ts/, + fullyParallel: false, + }, +]; \ No newline at end of file diff --git a/tests/qa/tests/.setup/pcp/setup-pcp-germany.ts b/tests/qa/tests/.setup/pcp/setup-pcp-germany.ts new file mode 100644 index 000000000..ea6e8158c --- /dev/null +++ b/tests/qa/tests/.setup/pcp/setup-pcp-germany.ts @@ -0,0 +1,9 @@ +/** + * Internal dependencies + */ +import { test as setup } from '../../../utils'; +import { pcpConfigGermany } from '../../../resources'; + +setup( 'Setup PCP merchant from Germany', async ( { utils } ) => { + await utils.configurePcp( pcpConfigGermany ); +} ); diff --git a/tests/qa/tests/.setup/pcp/setup-pcp-mexico.ts b/tests/qa/tests/.setup/pcp/setup-pcp-mexico.ts new file mode 100644 index 000000000..47bedfbb8 --- /dev/null +++ b/tests/qa/tests/.setup/pcp/setup-pcp-mexico.ts @@ -0,0 +1,9 @@ +/** + * Internal dependencies + */ +import { test as setup } from '../../../utils'; +import { pcpConfigMexico } from '../../../resources'; + +setup( 'Setup PCP merchant from Mexico', async ( { utils } ) => { + await utils.configurePcp( pcpConfigMexico ); +} ); diff --git a/tests/qa/tests/.setup/pcp/setup-pcp-usa.ts b/tests/qa/tests/.setup/pcp/setup-pcp-usa.ts new file mode 100644 index 000000000..446f4fcdd --- /dev/null +++ b/tests/qa/tests/.setup/pcp/setup-pcp-usa.ts @@ -0,0 +1,9 @@ +/** + * Internal dependencies + */ +import { test as setup } from '../../../utils'; +import { pcpConfigUsa } from '../../../resources'; + +setup( 'Setup PCP merchant from USA', async ( { utils } ) => { + await utils.configurePcp( pcpConfigUsa ); +} ); diff --git a/tests/qa/tests/.setup/store/index.ts b/tests/qa/tests/.setup/store/index.ts new file mode 100644 index 000000000..b7f4f3c9a --- /dev/null +++ b/tests/qa/tests/.setup/store/index.ts @@ -0,0 +1,26 @@ +export const storeSetupProjects = [ + { + name: 'setup-store-block-germany', + dependencies: [ 'setup-woocommerce' ], + testMatch: /setup-store-block-germany\.ts/, + fullyParallel: false, + }, + { + name: 'setup-store-classic-germany', + dependencies: [ 'setup-woocommerce' ], + testMatch: /setup-store-classic-germany\.ts/, + fullyParallel: false, + }, + { + name: 'setup-store-classic-mexico', + dependencies: [ 'setup-woocommerce' ], + testMatch: /setup-store-classic-mexico\.ts/, + fullyParallel: false, + }, + { + name: 'setup-store-classic-usa', + dependencies: [ 'setup-woocommerce' ], + testMatch: /setup-store-classic-usa\.ts/, + fullyParallel: false, + }, +]; \ No newline at end of file diff --git a/tests/qa/tests/.setup/store/setup-store-block-germany.ts b/tests/qa/tests/.setup/store/setup-store-block-germany.ts new file mode 100644 index 000000000..f0489dd5c --- /dev/null +++ b/tests/qa/tests/.setup/store/setup-store-block-germany.ts @@ -0,0 +1,9 @@ +/** + * Internal dependencies + */ +import { test as setup } from '../../../utils'; +import { storeConfigDefault } from '../../../resources'; + +setup( 'Setup Store with Block Pages for Germany', async ( { utils } ) => { + await utils.configureStore( storeConfigDefault ); +} ); diff --git a/tests/qa/tests/.setup/store/setup-store-classic-germany.ts b/tests/qa/tests/.setup/store/setup-store-classic-germany.ts new file mode 100644 index 000000000..03f4dc7ed --- /dev/null +++ b/tests/qa/tests/.setup/store/setup-store-classic-germany.ts @@ -0,0 +1,12 @@ +/** + * Internal dependencies + */ +import { test as setup } from '../../../utils'; +import { storeConfigGermany } from '../../../resources'; + +setup( 'Setup Store with Classic Pages for Germany', async ( { utils } ) => { + await utils.configureStore( { + ...storeConfigGermany, + classicPages: true, + } ); +} ); diff --git a/tests/qa/tests/.setup/store/setup-store-classic-mexico.ts b/tests/qa/tests/.setup/store/setup-store-classic-mexico.ts new file mode 100644 index 000000000..0dc4c6d48 --- /dev/null +++ b/tests/qa/tests/.setup/store/setup-store-classic-mexico.ts @@ -0,0 +1,12 @@ +/** + * Internal dependencies + */ +import { test as setup } from '../../../utils'; +import { storeConfigMexico } from '../../../resources'; + +setup( 'Setup Store with Classic Pages for Mexico', async ( { utils } ) => { + await utils.configureStore( { + ...storeConfigMexico, + classicPages: true, + } ); +} ); diff --git a/tests/qa/tests/.setup/store/setup-store-classic-usa.ts b/tests/qa/tests/.setup/store/setup-store-classic-usa.ts new file mode 100644 index 000000000..fdc5bf3ea --- /dev/null +++ b/tests/qa/tests/.setup/store/setup-store-classic-usa.ts @@ -0,0 +1,12 @@ +/** + * Internal dependencies + */ +import { test as setup } from '../../../utils'; +import { storeConfigUsa } from '../../../resources'; + +setup( 'Setup Store with Classic Pages for USA', async ( { utils } ) => { + await utils.configureStore( { + ...storeConfigUsa, + classicPages: true, + } ); +} ); diff --git a/tests/qa/tests/.setup/woocommerce.setup.ts b/tests/qa/tests/.setup/woocommerce.setup.ts new file mode 100644 index 000000000..35c7972bb --- /dev/null +++ b/tests/qa/tests/.setup/woocommerce.setup.ts @@ -0,0 +1,185 @@ +/** + * Internal dependencies + */ +import { test as setup } from '../../utils'; +import { + shopSettings, + shippingZones, + taxSettings, + products, + coupons, + customers, + wpDebuggingPlugin, + disableNoncePlugin, + subscriptionsPlugin, + disableWcSetupWizard, +} from '../../resources'; +/** + * External dependencies + */ +import { updateDotenv } from '@inpsyde/playwright-utils/build'; + +// setup( 'Setup Permalinks', async ( { requestUtils } ) => { +// await requestUtils.setPermalinks( '/%postname%/' ); +// } ); + +// setup( 'Setup Disable Nonce plugin (inactive)', +// async ( { requestUtils, plugins } ) => { +// if ( +// ! ( await requestUtils.isPluginInstalled( +// disableNoncePlugin.slug +// ) ) +// ) { +// await plugins.installPluginFromFile( +// disableNoncePlugin.zipFilePath +// ); +// } +// await requestUtils.activatePlugin( disableNoncePlugin.slug ); +// } +// ); + +// setup( 'Setup WP Debugging plugin (active)', async ( { requestUtils } ) => { +// if ( +// ! ( await requestUtils.isPluginInstalled( wpDebuggingPlugin.slug ) ) +// ) { +// await requestUtils.installPlugin( wpDebuggingPlugin.slug ); +// } +// await requestUtils.deactivatePlugin( wpDebuggingPlugin.slug ); +// } ); + +// setup( 'Setup Disable WooCommerce Setup Wizard Plugin (active)', +// async ( { requestUtils, plugins } ) => { +// if ( +// ! ( await requestUtils.isPluginInstalled( +// disableWcSetupWizard.slug +// ) ) +// ) { +// await plugins.installPluginFromFile( +// disableWcSetupWizard.zipFilePath +// ); +// } +// await requestUtils.activatePlugin( disableWcSetupWizard.slug ); +// } +// ); + +// setup( 'Setup WooCommerce plugin (active)', async ( { requestUtils } ) => { +// if ( ! ( await requestUtils.isPluginInstalled( 'woocommerce' ) ) ) { +// await requestUtils.installPlugin( 'woocommerce' ); +// } +// await requestUtils.activatePlugin( 'woocommerce' ); +// } ); + +// setup( 'Setup WC Subscriptions plugin (inactive)', +// async ( { requestUtils, plugins } ) => { +// if ( +// ! ( await requestUtils.isPluginInstalled( +// subscriptionsPlugin.slug +// ) ) +// ) { +// await plugins.installPluginFromFile( +// subscriptionsPlugin.zipFilePath +// ); +// } +// await requestUtils.deactivatePlugin( subscriptionsPlugin.slug ); +// } +// ); + +// setup( 'Setup theme', async ( { requestUtils } ) => { +// const slug = 'storefront'; +// if ( ! ( await requestUtils.isThemeInstalled( slug ) ) ) { +// await requestUtils.installTheme( slug ); +// } +// await requestUtils.activateTheme( slug ); +// } ); + +// setup( 'Setup WooCommerce Live site visibility', +// async ( { wooCommerceUtils } ) => { +// await wooCommerceUtils.setSiteVisibility(); +// } +// ); + +// setup( 'Setup WooCommerce API keys', async ( { wooCommerceUtils } ) => { +// if ( ! ( await wooCommerceUtils.apiKeysExist() ) ) { +// const apiKeys = await wooCommerceUtils.createApiKeys(); +// if( ! process.env.CI ) { +// await updateDotenv( './.env', apiKeys ); +// } +// for ( const [ key, value ] of Object.entries( apiKeys ) ) { +// process.env[ key ] = value; +// } +// } +// } ); + +// setup( 'Setup Block and Classic pages', async ( { wooCommerceUtils } ) => { +// await wooCommerceUtils.publishBlockCartPage(); +// await wooCommerceUtils.publishBlockCheckoutPage(); +// await wooCommerceUtils.publishClassicCartPage(); +// await wooCommerceUtils.publishClassicCheckoutPage(); +// } ); + +// setup( 'Setup WooCommerce email settings', async ( { wooCommerceApi } ) => { +// const disabled = { enabled: 'no' }; +// await wooCommerceApi.updateEmailSubSettings( 'email_new_order', disabled ); +// await wooCommerceApi.updateEmailSubSettings( 'email_cancelled_order', disabled ); +// await wooCommerceApi.updateEmailSubSettings( 'email_failed_order', disabled ); +// await wooCommerceApi.updateEmailSubSettings( 'email_customer_on_hold_order', disabled ); +// await wooCommerceApi.updateEmailSubSettings( 'email_customer_processing_order', disabled ); +// await wooCommerceApi.updateEmailSubSettings( 'email_customer_completed_order', disabled ); +// await wooCommerceApi.updateEmailSubSettings( 'email_customer_refunded_order', disabled ); +// await wooCommerceApi.updateEmailSubSettings( 'email_customer_note', disabled ); +// await wooCommerceApi.updateEmailSubSettings( 'email_customer_reset_password', disabled ); +// await wooCommerceApi.updateEmailSubSettings( 'email_customer_new_account', disabled ); +// } ); + +// setup( 'Setup WooCommerce general settings', async ( { wooCommerceApi } ) => { +// await wooCommerceApi.updateGeneralSettings( shopSettings.germany.general ); +// } ); + +// setup( 'Setup WooCommerce shipping', async ( { wooCommerceUtils } ) => { +// await wooCommerceUtils.configureShippingZone( shippingZones.worldwide ); +// } ); + +// setup( 'Setup WooCommerce taxes (included)', async ( { wooCommerceUtils } ) => { +// await wooCommerceUtils.setTaxes( taxSettings.including ); +// } ); + +// setup( 'Setup Registered Customer', async ( { wooCommerceUtils } ) => { +// await wooCommerceUtils.createCustomer( customers.germany ); +// } ); + +// setup( 'Setup Delete Previous Orders', async ( { wooCommerceApi } ) => { +// await wooCommerceApi.deleteAllOrders(); +// } ); + +// setup( 'Setup coupons', async ( { wooCommerceUtils } ) => { +// // create test coupons +// const couponItems = {}; +// const couponEntries = Object.entries( coupons ); +// await Promise.all( +// couponEntries.map( async ( [ key, coupon ] ) => { +// const createdCoupon = await wooCommerceUtils.createCoupon( coupon ); +// couponItems[ coupon.code ] = { id: createdCoupon.id }; +// } ) +// ); +// // store created coupons as CART_ITEMS env var +// process.env.COUPONS = JSON.stringify( couponItems ); +// } ); + +// setup( 'Setup products', async ( { wooCommerceUtils } ) => { +// // create test products +// const cartItems = {}; +// const productEntries = Object.entries( products ); +// await Promise.all( +// productEntries.map( async ( [ key, product ] ) => { +// // check if not subscription product - requires Supscriptions plugin +// if ( ! product.slug.includes( 'subscription' ) ) { +// const createdProduct = await wooCommerceUtils.createProduct( +// product +// ); +// cartItems[ product.slug ] = { id: createdProduct.id }; +// } +// } ) +// ); +// // store created products as CART_ITEMS env var +// process.env.PRODUCTS = JSON.stringify( cartItems ); +// } ); diff --git a/tests/qa/tests/01-plugin-foundation/plugin-foundation.spec.ts b/tests/qa/tests/01-plugin-foundation/plugin-foundation.spec.ts new file mode 100644 index 000000000..53512538f --- /dev/null +++ b/tests/qa/tests/01-plugin-foundation/plugin-foundation.spec.ts @@ -0,0 +1,65 @@ +/** + * External dependencies + */ +import { + testPluginInstallationFromFile, + testPluginReinstallationFromFile, + testPluginInstallationFromMarketplace, + testPluginActivation, + testPluginDeactivation, + testPluginRemoval, +} from '@inpsyde/playwright-utils/build'; +/** + * Internal dependencies + */ +import { test, expect } from '../../utils'; +import { pcpPlugin, customers } from '../../resources'; + +testPluginInstallationFromFile( 'PCP-1000', pcpPlugin, '@Critical' ); + +testPluginReinstallationFromFile( 'PCP-1007', pcpPlugin, '@Critical' ); + +// testPluginInstallationFromMarketplace( 'PCP-1004', pcpPlugin, '@Critical' ); + +testPluginActivation( 'PCP-2003', pcpPlugin, '@Critical' ); + +testPluginDeactivation( 'PCP-1006', pcpPlugin, '@Critical' ); + +testPluginRemoval( 'PCP-1005', pcpPlugin, '@Critical' ); + +// test.skip( 'PCP-0000 | Paypal is present in WooCommerce payment methods', async ( { +// wooCommerceUtils, +// requestUtils, +// plugins, +// wooCommerceSettings, +// } ) => { +// const gateway = { +// name: 'PayPal', +// enabled: false, +// description: 'Accept PayPal, Pay Later and alternative payment types.', +// }; + +// await plugins.installPluginFromFile( pcpPlugin.zipFilePath ); +// await requestUtils.activatePlugin( pcpPlugin.slug ); +// await wooCommerceUtils.createCustomer( customers.usa ); + +// await wooCommerceSettings.visit( 'payments' ); +// await expect( +// wooCommerceSettings.gatewayRow( gateway.name ) +// ).toBeVisible(); +// await expect( +// wooCommerceSettings.gatewayLink( gateway.name ) +// ).toBeVisible(); +// await expect( +// wooCommerceSettings.gatewayToggle( gateway.name ) +// ).toBeVisible(); +// await expect( +// wooCommerceSettings.gatewayDescription( +// gateway.name, +// gateway.description +// ) +// ).toBeVisible(); +// await expect( +// wooCommerceSettings.gatewaySetupButton( gateway.name ) +// ).toBeVisible(); +// } ); diff --git a/tests/qa/tests/02-dashboard-ui/visual-default-ui.spec.ts b/tests/qa/tests/02-dashboard-ui/visual-default-ui.spec.ts new file mode 100644 index 000000000..0e46192aa --- /dev/null +++ b/tests/qa/tests/02-dashboard-ui/visual-default-ui.spec.ts @@ -0,0 +1,117 @@ +/** + * External dependencies + */ +import { shopSettings } from '@inpsyde/playwright-utils/build'; +import { PercyConfig } from '@inpsyde/playwright-utils/build/@types/visual/percy'; +/** + * Internal dependencies + */ +import { test } from '../../utils'; +import { storeConfigDefault } from '../../resources'; + +const percyConfig: PercyConfig = { + scope: '#ppcp-settings-container', + httpCredentials: { + username: process.env.WP_BASIC_AUTH_USER, + password: process.env.WP_BASIC_AUTH_PASS, + }, +}; + +test.beforeAll( async ( { utils } ) => { + await utils.configureStore( storeConfigDefault ); + await utils.configurePcp( { + clearPCPDB: true, + disconnectMerchant: true, + } ); +} ); + +test.describe.serial( () => { + test( 'PCP-0000 | Settings - PayPal Payments - Default UI @percy', async ( { + pcpOnboarding, + percy, + }, testInfo ) => { + await pcpOnboarding.visit(); + await pcpOnboarding.gotoInitialOnboardingPage(); + await pcpOnboarding.page.waitForLoadState(); + await percy.takeSnapshot( testInfo.title, percyConfig ); + } ); + + test( 'PCP-0000 | Settings - PayPal Payments - See advanced options - Default UI @percy', async ( { + pcpOnboarding, + percy, + }, testInfo ) => { + await pcpOnboarding.visit(); + await pcpOnboarding.openAdvancedOptions(); + await percy.takeSnapshot( testInfo.title, percyConfig ); + } ); + + test( 'PCP-0000 | Settings - Select product types - Default UI @percy', async ( { + pcpOnboarding, + percy, + }, testInfo ) => { + await pcpOnboarding.visit(); + await pcpOnboarding.activatePayPalPaymentsButton().click(); + await pcpOnboarding.page.waitForLoadState(); + await percy.takeSnapshot( testInfo.title, percyConfig ); + } ); + + test( 'PCP-0000 | Settings - Choose checkout options - Default UI @percy', async ( { + pcpOnboarding, + percy, + }, testInfo ) => { + await pcpOnboarding.visit(); + await pcpOnboarding.virtualCheckbox().check(); + await pcpOnboarding.continueButton().click(); + await pcpOnboarding.page.waitForLoadState(); + await percy.takeSnapshot( testInfo.title, percyConfig ); + } ); + + test( 'PCP-0000 | Settings - Connect your PayPal account - Default UI @percy', async ( { + pcpOnboarding, + percy, + }, testInfo ) => { + await pcpOnboarding.visit(); + await pcpOnboarding.disableOptionalPaymentMethodsRadio().click(); + await pcpOnboarding.continueButton().click(); + await pcpOnboarding.page.waitForLoadState(); + await percy.takeSnapshot( testInfo.title, percyConfig ); + } ); +} ); + +test( 'PCP-0000 | Settings - Badge values per country @percy', async ( { + wooCommerceApi, + pcpOnboarding, + percy, +}, testInfo ) => { + const countries = [ 'germany', 'usa' ]; + + await pcpOnboarding.visit(); + + for ( const country of countries ) { + const generalCountrySettings = shopSettings[ country ].general; + await wooCommerceApi.updateGeneralSettings( generalCountrySettings ); + await pcpOnboarding.page.reload(); + await pcpOnboarding.gotoInitialOnboardingPage(); + await pcpOnboarding.page.waitForSelector( + 'span.ppcp-r-title-badge.ppcp-r-title-badge--info' + ); + await pcpOnboarding.closeAdvancedOptions(); + await percy.takeSnapshot( + `${ testInfo.title } - PayPal Settings - ${ country }`, + percyConfig + ); + + await pcpOnboarding.activatePayPalPaymentsButton().click(); + if ( country === 'usa' ) { + await pcpOnboarding.businessRadio().click(); + await pcpOnboarding.continueButton().click(); + } + await pcpOnboarding.virtualCheckbox().check(); + await pcpOnboarding.continueButton().click(); + await pcpOnboarding.disableOptionalPaymentMethodsRadio().click(); + await percy.takeSnapshot( + `${ testInfo.title } - Choose checkout options - ${ country }`, + percyConfig + ); + } +} ); diff --git a/tests/qa/tsconfig.json b/tests/qa/tsconfig.json new file mode 100644 index 000000000..2169c8867 --- /dev/null +++ b/tests/qa/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2019", + "types": ["node"], + "esModuleInterop": true, + "skipLibCheck": true, + "strict": false, + "baseUrl": ".", + "resolveJsonModule": true + } + } \ No newline at end of file diff --git a/tests/qa/utils/admin/index.ts b/tests/qa/utils/admin/index.ts new file mode 100644 index 000000000..d40a9b9df --- /dev/null +++ b/tests/qa/utils/admin/index.ts @@ -0,0 +1,9 @@ +export * from './pcp-admin-page'; +export * from './pcp-onboarding'; +export * from './pcp-overview'; +export * from './pcp-pay-later-messaging'; +export * from './pcp-payment-methods'; +export * from './pcp-settings'; +export * from './pcp-styling'; +export * from './woocommerce-order-edit'; +export * from './woocommerce-subscription-edit'; diff --git a/tests/qa/utils/admin/pcp-admin-page.ts b/tests/qa/utils/admin/pcp-admin-page.ts new file mode 100644 index 000000000..9b2ee0b5f --- /dev/null +++ b/tests/qa/utils/admin/pcp-admin-page.ts @@ -0,0 +1,9 @@ +/** + * External dependencies + */ +import { WpPage } from '@inpsyde/playwright-utils/build'; + +export class PcpAdminPage extends WpPage { + // Locators + // Actions +} diff --git a/tests/qa/utils/admin/pcp-onboarding.ts b/tests/qa/utils/admin/pcp-onboarding.ts new file mode 100644 index 000000000..d6e731d44 --- /dev/null +++ b/tests/qa/utils/admin/pcp-onboarding.ts @@ -0,0 +1,83 @@ +/** + * Internal dependencies + */ +import { PcpMerchant, PcpSettings } from '../../resources'; +import { PcpAdminPage } from './pcp-admin-page'; +import urls from '../urls'; + +export class PcpOnboarding extends PcpAdminPage { + url = urls.pcp.onboarding; + + // Locators + navigationPanel = () => this.page.locator( '.ppcp-r-navigation' ); + backButton = () => this.navigationPanel().locator( 'button.is-title' ); + onboardingPageTitle = ( title: PcpSettings.OnboardingStepTitle ) => + this.backButton().getByText( title ); + saveAndExitButton = () => + this.navigationPanel().getByRole( 'button', { name: 'Save and exit' } ); + continueButton = () => + this.navigationPanel().getByRole( 'button', { name: 'Continue' } ); + + activatePayPalPaymentsButton = () => + this.page.getByRole( 'button', { name: 'Activate PayPal Payments' } ); + + advancedOptionsSection = () => this.page.locator( '#advanced-options' ); + seeAdvancedOptionsButton = () => + this.advancedOptionsSection().getByRole( 'button', { + name: 'See advanced options', + } ); + advancedOptionsContent = () => + this.advancedOptionsSection().locator( '.ppcp-r-accordion__content' ); + + businessRadio = () => + this.page.locator( 'input.ppcp-r__radio-value[value="business"]' ); + personalAccountRadio = () => + this.page.locator( 'input.ppcp-r__radio-value[value="casual_seller"]' ); + + virtualCheckbox = () => + this.page.locator( 'input[type="checkbox"][value="virtual"]' ); + physicalGoodsCheckbox = () => + this.page.locator( 'input[type="checkbox"][value="physical"]' ); + + enableOptionalPaymentMethodsRadio = () => + this.page.locator( + 'input.ppcp-r__radio-value[value="true"]' + ); + disableOptionalPaymentMethodsRadio = () => + this.page.locator( + 'input.ppcp-r__radio-value[value="false"]' + ); + + connectToPayPalButton = () => + this.page.getByRole( 'button', { name: 'Connect to PayPal' } ); + + // Actions + isCurrentStep = async ( title: PcpSettings.OnboardingStepTitle ) => { + await this.page.waitForFunction(() => !!document.querySelector('button.is-title')); + return await this.onboardingPageTitle( title ).isVisible(); + }; + + gotoInitialOnboardingPage = async () => { + if ( await this.isCurrentStep( 'PayPal Payments' ) ) { + return; + } + await this.backButton().click(); + await this.gotoInitialOnboardingPage(); + }; + + openAdvancedOptions = async () => { + await this.gotoInitialOnboardingPage(); + if ( ! ( await this.advancedOptionsContent().isVisible() ) ) { + await this.seeAdvancedOptionsButton().click(); + } + }; + + closeAdvancedOptions = async () => { + await this.gotoInitialOnboardingPage(); + if ( await this.advancedOptionsContent().isVisible() ) { + await this.seeAdvancedOptionsButton().click(); + } + }; + + // Assertions +} diff --git a/tests/qa/utils/admin/pcp-overview.ts b/tests/qa/utils/admin/pcp-overview.ts new file mode 100644 index 000000000..71003be52 --- /dev/null +++ b/tests/qa/utils/admin/pcp-overview.ts @@ -0,0 +1,13 @@ +/** + * Internal dependencies + */ +import urls from '../urls'; +import { PcpAdminPage } from './pcp-admin-page'; + +export class PcpOverview extends PcpAdminPage { + url = urls.pcp.overview; + + // Locators + // Actions + // Assertions +} diff --git a/tests/qa/utils/admin/pcp-pay-later-messaging.ts b/tests/qa/utils/admin/pcp-pay-later-messaging.ts new file mode 100644 index 000000000..063b0bcce --- /dev/null +++ b/tests/qa/utils/admin/pcp-pay-later-messaging.ts @@ -0,0 +1,14 @@ +/** + * Internal dependencies + */ +import { PcpAdminPage } from './pcp-admin-page'; +import urls from '../urls'; + +export class PcpPayLaterMessaging extends PcpAdminPage { + url = urls.pcp.payLaterMessaging; + + // Locators + + //Actions + // Assertions +} diff --git a/tests/qa/utils/admin/pcp-payment-methods.ts b/tests/qa/utils/admin/pcp-payment-methods.ts new file mode 100644 index 000000000..dce6fbc32 --- /dev/null +++ b/tests/qa/utils/admin/pcp-payment-methods.ts @@ -0,0 +1,15 @@ +/** + * Internal dependencies + */ + +import urls from '../urls'; +import { PcpAdminPage } from './pcp-admin-page'; + +export class PcpPaymentMethods extends PcpAdminPage { + url = urls.pcp.paymentMehods; + + // Locators + + // Actions + // Assertions +} diff --git a/tests/qa/utils/admin/pcp-settings.ts b/tests/qa/utils/admin/pcp-settings.ts new file mode 100644 index 000000000..f677269a1 --- /dev/null +++ b/tests/qa/utils/admin/pcp-settings.ts @@ -0,0 +1,14 @@ +/** + * Internal dependencies + */ +import urls from '../urls'; +import { PcpAdminPage } from './pcp-admin-page'; + +export class PcpSettings extends PcpAdminPage { + url = urls.pcp.settings; + + // Locators + + // Actions + // Assertions +} diff --git a/tests/qa/utils/admin/pcp-styling.ts b/tests/qa/utils/admin/pcp-styling.ts new file mode 100644 index 000000000..da4511e82 --- /dev/null +++ b/tests/qa/utils/admin/pcp-styling.ts @@ -0,0 +1,14 @@ +/** + * Internal dependencies + */ +import urls from '../urls'; +import { PcpAdminPage } from './pcp-admin-page'; + +export class PcpStyling extends PcpAdminPage { + url = urls.pcp.styling; + + // Locators + + // Actions + // Assertions +} diff --git a/tests/qa/utils/admin/woocommerce-order-edit.ts b/tests/qa/utils/admin/woocommerce-order-edit.ts new file mode 100644 index 000000000..76ec14911 --- /dev/null +++ b/tests/qa/utils/admin/woocommerce-order-edit.ts @@ -0,0 +1,266 @@ +/** + * External dependencies + */ +import { + WooCommerceOrderEdit as WooCommerceOrderEditBase, + expect, + formatMoney, +} from '@inpsyde/playwright-utils/build'; + +export class WooCommerceOrderEdit extends WooCommerceOrderEditBase { + // Locators + paymentVia = ( method ) => + this.orderNumberContainer().getByText( `Payment via ${ method }` ); + transactionIdLink = ( transactionId ) => + this.orderNumberContainer().getByRole( 'link', { + name: transactionId, + } ); + payPalEmailAddress = () => + this.page + .locator( 'p' ) + .filter( { hasText: 'PayPal email address:' } ) + .getByRole( 'link' ); + + totalPayPalFee = () => this.totalsTableRow( 'PayPal Fee:' ); + totalPayPalPayout = () => this.totalsTableRow( 'PayPal Payout' ); + + refundViaButton = ( paymentMethod ) => + this.page.locator( '.do-api-refund', { + hasText: `via ${ paymentMethod }`, + } ); + + productsTable = () => this.page.locator( '#order_line_items' ); + productRow = ( name ) => this.productsTable().getByRole( 'row', { name } ); + productRefundQtyInput = ( name ) => + this.productRow( name ).locator( '.refund_order_item_qty' ); + productRefundTotalInput = ( name ) => + this.productRow( name ).locator( '.refund_line_total' ); + productRefundTaxInput = ( name ) => + this.productRow( name ).locator( '.refund_line_tax' ); + + firstRefundTotalInput = () => + this.productsTable().locator( '.refund_line_total' ).first(); + + totalPayPalRefundFee = () => this.totalsTableRow( 'PayPal Refund Fee:' ); + totalPayPalRefunded = () => this.totalsTableRow( 'PayPal Refunded:' ); + totalPayPalNetTotal = () => this.totalsTableRow( 'PayPal Net Total:' ); + + seeOXXOVoucherButton = () => + this.page.getByRole( 'link', { name: 'See OXXO voucher' } ).first(); + + payPalPackageTrackingSection = () => + this.page.locator( '#ppcp_order-tracking' ); + + cvv2MatchOrderNote = () => + this.orderNoteContent().filter( { hasText: 'CVV2 Match: Y' } ); + addressVerificationOrderNote = () => + this.orderNoteContent().filter( { + hasText: 'Address Verification Result', + } ); + + // Actions + + /** + * Performs refund + * + * @param amount + */ + makePayPalRefund = async ( amount?: string ) => { + // Make full-amount refund if amount is not specified + if ( ! amount ) { + const totalAmount = + ( await this.totalAvailableToRefund().textContent() ) || ''; + amount = parseFloat( + totalAmount.replace( /[^\d.-]+/g, '' ).trim() + ).toFixed( 2 ); + } + + await this.firstRefundTotalInput().fill( amount ); + await this.page.on( 'dialog', ( dialog ) => dialog.accept() ); + // await this.page.on('dialog', dialog => dialog.accept()); + await this.refundViaButton( 'PayPal' ).click(); + }; + + // Assertions + assertPayPalEmailAddress = async ( email: string ) => { + await expect( this.payPalEmailAddress() ).toHaveText( email ); + }; + + /** + * Asserts order note with Address Verification Result for ACDC + * + * @param payment + */ + assertAddressVerificationResult = async ( payment ) => { + // await expect(this.cvv2MatchOrderNote()).toBeVisible(); + // const orderNote = await this.addressVerificationOrderNote(); + // await expect(orderNote).toContainText(`AVS: Y`); + // await expect(orderNote).toContainText(`Address Match: N`); + // await expect(orderNote).toContainText(`Postal Match: N`); + // await expect(orderNote).toContainText(`Card Brand: ${payment.card_type}`); + // await expect(orderNote).toContainText(`Card Last Digits: ${payment.card_number.slice(-4)}`); + }; + + /** + * Asserts data provided on the order page + * + * @param orderId + * @param orderData + * @param pcpData = { + * transactionId: 'QW23134212341234', + * paypalFee: '1.00', + * paypalPayout: '29.00', + * + * paymentMethod: 'PayPal', + * itemsSubtotal: '20.00', + * totalCoupons: '10.00', + * totalFees: '10.00', + * totalShipping: '10.00', + * orderTotal: '30.00', + * currency: 'EUR' + * } + */ + assertOrderDetails = async ( + orderId: number, + orderData: WooCommerce.ShopOrder, + pcpData? + ) => { + await super.assertOrderDetails( orderId, orderData ); + + if ( ! pcpData ) { + return; + } + + // Transaction ID + if ( + pcpData.transaction_id !== undefined && + pcpData.orderTotal !== undefined + ) { + await expect( + this.transactionIdLink( pcpData.transaction_id ) + ).toBeVisible(); + } + + // PayPal fees + if ( + pcpData.paypPalFee !== undefined && + pcpData.orderTotal !== undefined + ) { + await expect( this.totalPayPalFee() ).toHaveText( + '- ' + + ( await formatMoney( + pcpData.payPalFee, + orderData.currency + ) ) + ); + } + + //PayPal payout + if ( + pcpData.payPalPayout !== undefined && + pcpData.orderTotal !== undefined + ) { + await expect( this.totalPayPalPayout() ).toHaveText( + await formatMoney( pcpData.payPalPayout, orderData.currency ) + ); + } + + if ( orderData.payment.dataFundingSource === 'oxxo' ) { + await expect( this.seeOXXOVoucherButton() ).toBeVisible(); + } + + if ( orderData.payment.dataFundingSource === 'acdc' ) { + await this.assertAddressVerificationResult( + orderData.payment.card + ); + } + + if ( + [ 'paypal', 'paylater', 'venmo' ].includes( + orderData.payment.dataFundingSource + ) + ) { + await this.assertPayPalEmailAddress( + orderData.payment.payPalAccount.email + ); + } + }; + + /** + * Asserts data provided on the order page + * + * @param data = { + * refund_id: , + * refunded: , + * totalRefunded: , + * netPayment: , + * payPalFee: , + * payPalRefundFee: , + * payPalRefunded: , + * payPalPayout: , + * payPalNetTotal: , + * currency: 'EUR' + * } + */ + assertRefundData = async ( data ) => { + // Order status + if ( data.orderStatus !== undefined ) { + await expect( this.statusCombobox() ).toHaveText( + data.orderStatus + ); + } + + if ( data.refund_id !== undefined && data.refunded !== undefined ) { + await expect( this.refundNumber() ).toContainText( + `Refund #${ data.refund_id }` + ); + await expect( this.refundAmount() ).toHaveText( + '-' + ( await formatMoney( data.refunded, data.currency ) ) + ); + } + + if ( data.totalRefunded !== undefined ) { + await expect( this.totalRefunded() ).toHaveText( + '-' + ( await formatMoney( data.totalRefunded, data.currency ) ) + ); + } + + if ( data.netPayment !== undefined ) { + await expect( this.totalNetPayment() ).toHaveText( + await formatMoney( data.netPayment, data.currency ) + ); + } + + if ( data.payPalFee !== undefined ) { + await expect( this.totalPayPalFee() ).toHaveText( + '- ' + ( await formatMoney( data.payPalFee, data.currency ) ) + ); + } + + if ( data.payPalRefundFee !== undefined ) { + await expect( this.totalPayPalRefundFee() ).toHaveText( + '- ' + + ( await formatMoney( data.payPalRefundFee, data.currency ) ) + ); + } + + if ( data.payPalRefunded !== undefined ) { + await expect( this.totalPayPalRefunded() ).toHaveText( + '- ' + + ( await formatMoney( data.payPalRefunded, data.currency ) ) + ); + } + + if ( data.payPalPayout !== undefined ) { + await expect( this.totalPayPalPayout() ).toHaveText( + await formatMoney( data.payPalPayout, data.currency ) + ); + } + + if ( data.payPalNetTotal !== undefined ) { + await expect( this.totalPayPalNetTotal() ).toHaveText( + await formatMoney( data.payPalNetTotal, data.currency ) + ); + } + }; +} diff --git a/tests/qa/utils/admin/woocommerce-subscription-edit.ts b/tests/qa/utils/admin/woocommerce-subscription-edit.ts new file mode 100644 index 000000000..0ffbc36b2 --- /dev/null +++ b/tests/qa/utils/admin/woocommerce-subscription-edit.ts @@ -0,0 +1,41 @@ +/** + * External dependencies + */ +import { + WooCommerceSubscriptionEdit as WooCommerceSubscriptionEditBase, + expect, +} from '@inpsyde/playwright-utils/build'; + +export class WooCommerceSubscriptionEdit extends WooCommerceSubscriptionEditBase { + // Locators + transactionIdKey = () => + this.page.locator( + 'input[value="ppcp_previous_transaction_reference"]' + ); + transactionIdRow = () => + this.page.locator( '#the-list tr', { has: this.transactionIdKey() } ); + transactionIdTextarea = () => this.transactionIdRow().getByLabel( 'Value' ); + + // Actions + + // Assertions + + /** + * Asserts data on subscription page + * + * @param subscriptionId + * @param data + */ + assertSubscriptionDetails = async ( subscriptionId, data ) => { + const statusLabels = { + active: 'Active', + }; + const status = statusLabels[ data.subscription.status ]; + + await this.visit( subscriptionId ); + await expect( this.customerCombobox() ).toContainText( + data.customer.email + ); + await expect( this.statusCombobox() ).toHaveText( status ); + }; +} diff --git a/tests/qa/utils/frontend/cart.ts b/tests/qa/utils/frontend/cart.ts new file mode 100644 index 000000000..52c3822ef --- /dev/null +++ b/tests/qa/utils/frontend/cart.ts @@ -0,0 +1,43 @@ +/** + * External dependencies + */ +import { Cart as CartBase } from '@inpsyde/playwright-utils/build'; +/** + * Internal dependencies + */ +import { PayPalUI } from './paypal-ui'; + +export class Cart extends CartBase { + ppui: PayPalUI; + + constructor( { page, ppui } ) { + super( { page } ); + this.ppui = ppui; + } + + // Locators + + // Actions + + makeOrder = async ( tested ) => { + await this.visit(); + + // Add coupons if needed + if ( tested.coupons ) { + for ( const coupon of tested.coupons ) { + await this.applyCoupon( coupon.code ); + } + } + + // Select shipping or initial shipment (for subscriptions) option: + await this.selectShippingMethod( tested.shipping.settings.title ); + + // Make payment with tested method + await this.ppui.makePayment( { + merchant: tested.merchant, + payment: tested.payment, + } ); + }; + + // Assertions +} diff --git a/tests/qa/utils/frontend/checkout.ts b/tests/qa/utils/frontend/checkout.ts new file mode 100644 index 000000000..5ae79169d --- /dev/null +++ b/tests/qa/utils/frontend/checkout.ts @@ -0,0 +1,77 @@ +/** + * External dependencies + */ +import { + Checkout as CheckoutBase, + expect, +} from '@inpsyde/playwright-utils/build'; +/** + * Internal dependencies + */ +import { PayPalUI } from './paypal-ui'; + +export class Checkout extends CheckoutBase { + ppui: PayPalUI; + + constructor( { page, ppui } ) { + super( { page } ); + this.ppui = ppui; + } + + // Locators + proceedToPayPalButton = () => + this.page.getByRole( 'button', { name: 'Proceed to PayPal' } ); + + // Actions + + applyCouponIfNeeded = async ( coupons? ) => { + if ( coupons ) { + for ( const coupon of coupons ) { + await super.applyCoupon( coupon.code ); + } + } + }; + + makeOrder = async ( tested ) => { + await this.visit(); + + // Add coupons if needed + await this.applyCouponIfNeeded( tested.coupons ); + + // Fill billing details + await this.fillCheckoutForm( tested.customer ); + + // Select shipping or initial shipment (for subscriptions) option: + await this.selectShippingMethod( tested.shipping.settings.title ); + + // Make payment with tested method + await this.ppui.makePayment( { + merchant: tested.merchant, + payment: tested.payment, + } ); + await this.placeOrder(); + }; + + completeOrderFromProduct = async ( tested ) => { + await this.assertUrl(); + await expect( + this.page.getByText( + `You are currently paying with ${ tested.payment.gatewayName }.` + ) + ).toBeVisible(); + + // Add coupons if needed + await this.applyCouponIfNeeded( tested.coupons ); + + // Fill billing details + await this.fillCheckoutForm( tested.customer ); + + // Select shipping or initial shipment (for subscriptions) option: + await this.selectShippingMethod( tested.shipping.settings.title ); + + // Make payment with tested method + await this.placeOrder(); + }; + + // Assertions +} diff --git a/tests/qa/utils/frontend/classic-cart.ts b/tests/qa/utils/frontend/classic-cart.ts new file mode 100644 index 000000000..ad5f279d4 --- /dev/null +++ b/tests/qa/utils/frontend/classic-cart.ts @@ -0,0 +1,40 @@ +/** + * External dependencies + */ +import { ClassicCart as ClassicCartBase } from '@inpsyde/playwright-utils/build'; +/** + * Internal dependencies + */ +import { PayPalUI } from './paypal-ui'; + +export class ClassicCart extends ClassicCartBase { + ppui: PayPalUI; + + constructor( { page, ppui } ) { + super( { page } ); + this.ppui = ppui; + } + + // Locators + + // Actions + + makeOrder = async ( tested ) => { + await this.visit(); + // Add coupons if needed + if ( tested.coupons ) { + for ( const coupon of tested.coupons ) { + await this.applyCoupon( coupon.code ); + } + } + // Select shipping or initial shipment (for subscriptions) option: + await this.selectShippingMethod( tested.shipping.settings.title ); + // Make payment with tested method + await this.ppui.makeClassicPayment( { + merchant: tested.merchant, + payment: tested.payment, + } ); + }; + + // Assertions +} diff --git a/tests/qa/utils/frontend/classic-checkout.ts b/tests/qa/utils/frontend/classic-checkout.ts new file mode 100644 index 000000000..1875d5b94 --- /dev/null +++ b/tests/qa/utils/frontend/classic-checkout.ts @@ -0,0 +1,90 @@ +/** + * External dependencies + */ +import { + ClassicCheckout as ClassicCheckoutBase, + expect, +} from '@inpsyde/playwright-utils/build'; +/** + * Internal dependencies + */ +import { PayPalUI } from './paypal-ui'; + +export class ClassicCheckout extends ClassicCheckoutBase { + ppui: PayPalUI; + + constructor( { page, ppui } ) { + super( { page } ); + this.ppui = ppui; + } + + // Locators + + // Actions + + applyCouponIfNeeded = async ( coupons? ) => { + if ( coupons ) { + for ( const coupon of coupons ) { + await super.applyCoupon( coupon.code ); + } + } + }; + + makeOrder = async ( tested ) => { + await this.visit(); + + // Add coupons if needed + await this.applyCouponIfNeeded( tested.coupons ); + + // Select shipping or initial shipment (for subscriptions) option: + if ( + tested.products.some( + ( product ) => product.type === 'subscription' + ) + ) { + await this.selectInitialShipment( tested.shipping.settings.title ); + } else { + await this.selectShippingMethod( tested.shipping.settings.title ); + } + + // Fill billing details + await this.fillCheckoutForm( tested.customer ); + + // Make payment with tested method + await this.ppui.makeClassicPayment( { + merchant: tested.merchant, + payment: tested.payment, + } ); + }; + + completeOrderFromProduct = async ( tested ) => { + await this.assertUrl(); + await expect( + this.page.getByText( + `You are currently paying with ${ tested.payment.gatewayName }.` + ) + ).toBeVisible(); + + // Add coupons if needed + await this.applyCouponIfNeeded( tested.coupons ); + + // Select shipping or initial shipment (for subscriptions) option: + if ( + tested.products.some( + ( product ) => product.type === 'subscription' + ) + ) { + await this.selectInitialShipment( tested.shipping.settings.title ); + } else { + await this.selectShippingMethod( tested.shipping.settings.title ); + } + + // Fill billing details + await this.fillCheckoutForm( tested.customer ); + + // Make payment with tested method + await this.placeOrder(); + }; + + // Assertions +} diff --git a/tests/qa/utils/frontend/classic-pay-for-order.ts b/tests/qa/utils/frontend/classic-pay-for-order.ts new file mode 100644 index 000000000..e29416891 --- /dev/null +++ b/tests/qa/utils/frontend/classic-pay-for-order.ts @@ -0,0 +1,14 @@ +/** + * Internal dependencies + */ +import { PayForOrder } from './pay-for-order'; + +export class ClassicPayForOrder extends PayForOrder { + url = './classic-checkout/order-pay/'; + + // Locators + + // Actions + + // Assertions +} diff --git a/tests/qa/utils/frontend/customer-account.ts b/tests/qa/utils/frontend/customer-account.ts new file mode 100644 index 000000000..8b2ad5b8e --- /dev/null +++ b/tests/qa/utils/frontend/customer-account.ts @@ -0,0 +1,23 @@ +/** + * External dependencies + */ +import { CustomerAccount as CustomerAccountBase } from '@inpsyde/playwright-utils/build'; +/** + * Internal dependencies + */ +import { PayPalUI } from './paypal-ui'; + +export class CustomerAccount extends CustomerAccountBase { + ppui: PayPalUI; + + constructor( { page, ppui } ) { + super( { page } ); + this.ppui = ppui; + } + + // Locators + + // Actions + + // Assertions +} diff --git a/tests/qa/utils/frontend/customer-payment-methods.ts b/tests/qa/utils/frontend/customer-payment-methods.ts new file mode 100644 index 000000000..1a94da278 --- /dev/null +++ b/tests/qa/utils/frontend/customer-payment-methods.ts @@ -0,0 +1,58 @@ +/** + * External dependencies + */ +import { CustomerPaymentMethods as CustomerPaymentMethodsBase } from '@inpsyde/playwright-utils/build'; +/** + * Internal dependencies + */ +import { PayPalUI } from './paypal-ui'; +import { PcpPayment } from '../../resources'; + +export class CustomerPaymentMethods extends CustomerPaymentMethodsBase { + ppui: PayPalUI; + + constructor( { page, ppui } ) { + super( { page } ); + this.ppui = ppui; + } + + // Locators + noSavedMethodsMessage = () => + this.page.getByText( 'No saved methods found' ); + + // Actions + isSavedPaymentMethod = async ( payment: PcpPayment ) => { + await this.visit(); + + if ( await this.noSavedMethodsMessage().isVisible() ) { + return false; + } + + switch ( payment.dataFundingSource ) { + case 'paypal': + return await this.savedPaymentMethodRow( + `Paypal /` + ).isVisible(); + + case 'card': + return await this.savedPaymentMethodRow( + payment.card?.card_number + ).isVisible(); + } + }; + + /** + * Adds payment method on My Account/Payment Methods page + * + * @param payment + */ + savePaymentMethod = async ( payment: PcpPayment ) => { + if ( ! ( await this.isSavedPaymentMethod( payment ) ) ) { + await this.addPaymentMethodButton().click(); + await this.page.waitForLoadState(); + await this.ppui.savePaymentMethod( payment ); + } + }; + + // Assertions +} diff --git a/tests/qa/utils/frontend/customer-subscriptions.ts b/tests/qa/utils/frontend/customer-subscriptions.ts new file mode 100644 index 000000000..763166a4f --- /dev/null +++ b/tests/qa/utils/frontend/customer-subscriptions.ts @@ -0,0 +1,23 @@ +/** + * External dependencies + */ +import { CustomerSubscriptions as CustomerSubscriptionsBase } from '@inpsyde/playwright-utils/build'; +/** + * Internal dependencies + */ +import { PayPalUI } from './paypal-ui'; + +export class CustomerSubscriptions extends CustomerSubscriptionsBase { + ppui: PayPalUI; + + constructor( { page, ppui } ) { + super( { page } ); + this.ppui = ppui; + } + + // Locators + + // Actions + + // Assertions +} diff --git a/tests/qa/utils/frontend/index.ts b/tests/qa/utils/frontend/index.ts new file mode 100644 index 000000000..b28fc4726 --- /dev/null +++ b/tests/qa/utils/frontend/index.ts @@ -0,0 +1,14 @@ +export * from './cart'; +export * from './checkout'; +export * from './classic-cart'; +export * from './classic-checkout'; +export * from './classic-pay-for-order'; +export * from './customer-account'; +export * from './customer-payment-methods'; +export * from './customer-subscriptions'; +export * from './order-received'; +export * from './pay-for-order'; +export * from './paypal-popup'; +export * from './paypal-ui'; +export * from './product'; +export * from './shop'; diff --git a/tests/qa/utils/frontend/order-received.ts b/tests/qa/utils/frontend/order-received.ts new file mode 100644 index 000000000..dba221e33 --- /dev/null +++ b/tests/qa/utils/frontend/order-received.ts @@ -0,0 +1,47 @@ +/** + * External dependencies + */ +import { + OrderReceived as OrderReceivedBase, + expect, +} from '@inpsyde/playwright-utils/build'; +/** + * Internal dependencies + */ +import { PayPalUI } from './paypal-ui'; + +export class OrderReceived extends OrderReceivedBase { + ppui: PayPalUI; + + constructor( { page, ppui } ) { + super( { page } ); + this.ppui = ppui; + } + + // Locators + seeOXXOVoucherButton_1 = () => + this.page.getByRole( 'link', { name: 'See OXXO voucher' } ).first(); + seeOXXOVoucherButton_2 = () => + this.page.getByRole( 'link', { name: 'See OXXO voucher' } ).last(); + + // Actions + + /** + * Asserts that + * - Order Received heading is visible + * - Expected payment method is displayed + * - Other optional payment details + * + * @param order + */ + assertOrderDetails = async ( order: WooCommerce.ShopOrder ) => { + await super.assertOrderDetails( order ); + + if ( order.payment.dataFundingSource === 'oxxo' ) { + await expect( this.seeOXXOVoucherButton_1() ).toBeVisible(); + await expect( this.seeOXXOVoucherButton_2() ).toBeVisible(); + } + }; + + // Assertions +} diff --git a/tests/qa/utils/frontend/pay-for-order.ts b/tests/qa/utils/frontend/pay-for-order.ts new file mode 100644 index 000000000..7b453738b --- /dev/null +++ b/tests/qa/utils/frontend/pay-for-order.ts @@ -0,0 +1,31 @@ +/** + * External dependencies + */ +import { PayForOrder as PayForOrderBase } from '@inpsyde/playwright-utils/build'; +/** + * Internal dependencies + */ +import { PayPalUI } from './paypal-ui'; + +export class PayForOrder extends PayForOrderBase { + ppui: PayPalUI; + + constructor( { page, ppui } ) { + super( { page } ); + this.ppui = ppui; + } + + // Locators + + // Actions + + makeOrder = async ( tested, order ) => { + await this.visit( order.id, order.order_key ); + await this.ppui.makeClassicPayment( { + merchant: tested.merchant, + payment: tested.payment, + } ); + }; + + // Assertions +} diff --git a/tests/qa/utils/frontend/paypal-popup.ts b/tests/qa/utils/frontend/paypal-popup.ts new file mode 100644 index 000000000..19ff48681 --- /dev/null +++ b/tests/qa/utils/frontend/paypal-popup.ts @@ -0,0 +1,65 @@ +/** + * External dependencies + */ +import { expect, Page } from '@playwright/test'; + +export class PayPalPopup { + popup: Page; + + constructor( popup ) { + this.popup = popup; + } + + // Locators + loginWithPasswordInsteadLink = () => + this.popup.getByRole( 'link', { + name: 'Log in with a password instead', + } ); + loginInput = () => this.popup.locator( '[name="login_email"]' ); + passwordInput = () => this.popup.locator( '[name="login_password"]' ); + nextButton = () => this.popup.locator( '#btnNext' ); + loginButton = () => this.popup.locator( '#btnLogin' ); + submitPaymentButton = () => this.popup.locator( '#payment-submit-btn' ); + payLaterSwitcher = () => this.popup.getByTestId( 'paylater-tab' ); + payLaterRadio = () => + this.popup.locator( 'label[for^="credit-offer"]' ).first(); + saveAndContinueButton = () => this.popup.getByTestId( 'consentButton' ); + cancelLink = () => this.popup.locator( '#cancelLink' ); + + // Actions + + login = async ( email, password ) => { + await expect( this.popup ).toHaveTitle( + 'Log in to your PayPal account' + ); + + if ( + ! ( await this.loginInput().isEditable() ) && + this.loginWithPasswordInsteadLink().isVisible() + ) { + this.loginWithPasswordInsteadLink().click(); + } + + await this.loginInput().fill( email ); + // Sometimes we get a popup with email and password fields at the same screen + if ( await this.nextButton().isVisible() ) { + await this.nextButton().click(); + } + await this.passwordInput().fill( password ); + await this.loginButton().click(); + }; + + completePayment = async () => { + await Promise.all( [ + this.popup.waitForEvent( 'close' ), + this.submitPaymentButton().click(), + ] ); + }; + + savePaymentMethodAndContinue = async () => { + await Promise.all( [ + this.popup.waitForEvent( 'close' ), + this.saveAndContinueButton().click(), + ] ); + }; +} diff --git a/tests/qa/utils/frontend/paypal-ui.ts b/tests/qa/utils/frontend/paypal-ui.ts new file mode 100644 index 000000000..83742016c --- /dev/null +++ b/tests/qa/utils/frontend/paypal-ui.ts @@ -0,0 +1,855 @@ +/** + * External dependencies + */ +import { Page } from '@playwright/test'; +import { expect, getLast4CardDigits } from '@inpsyde/playwright-utils/build'; +/** + * Internal dependencies + */ +import { PayPalAccount, PcpMerchant, PcpPayment } from '../../resources'; +import { PayPalPopup } from './paypal-popup'; +import { PayPalAPI } from '../paypal-api'; + +/** + * Class for common dashboard locators, actions, assertions + */ + +export class PayPalUI { + page: Page; + ppapi: PayPalAPI; + + constructor( { page, ppapi } ) { + this.page = page; + this.ppapi = ppapi; + } + + // Locators + placeOrderButton = () => + this.page.getByRole( 'button', { name: 'Place order' } ); + payForOrderButton = () => + this.page.getByRole( 'button', { name: 'Pay for order' } ); + cartMenu = () => this.page.locator( '#site-header-cart' ); + + payPalIframe = () => + this.page.frameLocator( + '[id^="ppc-button-ppcp-gateway"] .component-frame.visible' + ); + fundingSourceButton = ( name ) => + this.payPalIframe().locator( `[data-funding-source="${ name }"]` ); + + payPalButton = () => this.fundingSourceButton( 'paypal' ); + payLaterButton = () => this.fundingSourceButton( 'paylater' ); + sepaButton = () => this.fundingSourceButton( 'sepa' ); + giropayButton = () => this.fundingSourceButton( 'giropay' ); + sofortButton = () => this.fundingSourceButton( 'sofort' ); + debitOrCreditCardButton = () => this.fundingSourceButton( 'card' ); + venmoButton = () => this.fundingSourceButton( 'venmo' ); + + fundingSourceButtonLabelText = ( name ) => + this.fundingSourceButton( name ).locator( '.paypal-button-text' ); // additional text on paypal buttons + fundingSourceButtonPayLabel = ( name ) => + this.fundingSourceButton( name ).locator( '.pay-label' ); // for example Pay Now when vaulting + fundingSourceButtonLabel = ( name ) => + this.fundingSourceButton( name ).locator( '.label' ); // customer's email if Vaulting + iframePayPalButton = () => this.payPalIframe().locator( '.paypal-button' ); + iframePayPalButtonText = () => + this.payPalIframe().locator( '.paypal-button-text.true' ); + + payPalGateway = () => this.page.locator( 'li.payment_method_ppcp-gateway' ); + acdcGateway = () => + this.page.locator( 'li.payment_method_ppcp-credit-card-gateway' ); + debitCreditCardsGateway = () => + this.page.locator( 'li.payment_method_ppcp-credit-card-gateway' ); + standardCardButtonGateway = () => + this.page.locator( 'li.payment_method_ppcp-card-button-gateway' ); + oxxoGateway = () => + this.page.locator( 'li.payment_method_ppcp-oxxo-gateway' ); + payUponInvoiceGateway = () => + this.page.locator( 'li.payment_method_ppcp-pay-upon-invoice-gateway' ); + + taglineText = () => this.payPalIframe().locator( '.paypal-button-tagline' ); + payPalGatewayText = () => + this.page.locator( '.payment_method_ppcp-gateway>label' ); + payPalGatewayDescription = () => + this.page.locator( '.payment_method_ppcp-gateway>p' ); + acdcGatewayText = () => + this.page.locator( '.payment_method_ppcp-credit-card-gateway>label' ); + + blockSmartButtonIframe = () => + this.page.locator( '[id^="express-payment-method-ppcp-gateway-"]' ); + blockPayPalButton = () => + this.page + .frameLocator( + '#express-payment-method-ppcp-gateway-paypal .component-frame' + ) + .locator( `[data-funding-source="paypal"]` ); + blockPayLaterButton = () => + this.page + .frameLocator( + '#express-payment-method-ppcp-gateway-paylater .component-frame' + ) + .locator( `[data-funding-source="paylater"]` ); + blockVenmoButton = () => + this.page + .frameLocator( + '#express-payment-method-ppcp-gateway-venmo .component-frame' + ) + .locator( `[data-funding-source="venmo"]` ); + + cardNumberInput = () => + this.page + .frameLocator( 'iframe[title="paypal_card_number_field"]' ) + .locator( 'input.card-field-number ' ); + cardExpirationInput = () => + this.page + .frameLocator( 'iframe[title="paypal_card_expiry_field"]' ) + .locator( 'input.card-field-expiry' ); + cardCVVInput = () => + this.page + .frameLocator( 'iframe[title="paypal_card_cvv_field"]' ) + .locator( 'input.card-field-cvv' ); + addPaymentMethodButton = () => this.page.locator( '#place_order' ); + + payPalCardGatewayIframe = () => + this.payPalIframe().frameLocator( 'iframe.zoid-visible' ); + payPalCardGatewayCardNumberInput = () => + this.payPalCardGatewayIframe().locator( '#credit-card-number' ); + payPalCardGatewayCardExpirationInput = () => + this.payPalCardGatewayIframe().locator( '#expiry-date' ); + payPalCardGatewayCSCCode = () => + this.payPalCardGatewayIframe().locator( '#credit-card-security' ); + payPalCardGatewayBuyNowButton = () => + this.payPalCardGatewayIframe().locator( '#submit-button' ); + payPalCardGatewayPoweredByText = () => + this.page + .frameLocator( 'iframe[name^="__zoid__paypal_buttons__"]' ) + .locator( '.paypal-powered-by' ) + .getByText( 'Powered by' ); + payPalCardGatewayPoweredByLogo = () => + this.page + .frameLocator( 'iframe[name^="__zoid__paypal_buttons__"]' ) + .locator( '.paypal-powered-by .paypal-logo' ); + payPalCardGatewayFirsNameField = () => + this.payPalCardGatewayIframe().locator( + '[id="billingAddress.givenName"]' + ); + payPalCardGatewayLastNameField = () => + this.payPalCardGatewayIframe().locator( + '[id="billingAddress.familyName"]' + ); + payPalCardGatewayStreetField = () => + this.payPalCardGatewayIframe().locator( '[id="billingAddress.line1"]' ); + payPalCardGatewayApartmentBuildingField = () => + this.payPalCardGatewayIframe().locator( '[id="billingAddress.line2"]' ); + payPalCardGatewayCityField = () => + this.payPalCardGatewayIframe().locator( '[id="billingAddress.city"]' ); + payPalCardGatewayStateField = () => + this.payPalCardGatewayIframe().locator( '[id="billingAddress.state"]' ); + payPalCardGatewayZipCodeField = () => + this.payPalCardGatewayIframe().locator( + '[id="billingAddress.postcode"]' + ); + payPalCardGatewayPhoneField = () => + this.payPalCardGatewayIframe().locator( '[id="phone"]' ); + payPalCardGatewayEmailField = () => + this.payPalCardGatewayIframe().locator( '[id="email"]' ); + + miniCartButtonIframe = () => + this.page.frameLocator( '#ppc-button-minicart .component-frame' ); + miniCartFundingSourceButton = ( name ) => + this.miniCartButtonIframe().locator( + `[data-funding-source="${ name }"]` + ); + miniCartIframePayPalButton = () => + this.miniCartButtonIframe().locator( '.paypal-button' ); + miniCartPayPalButton = () => this.miniCartFundingSourceButton( 'paypal' ); + + debitOrCreditCardIframe = () => + this.payPalIframe().frameLocator( 'iframe.zoid-visible' ); + debitOrCreditCardNumberInput = () => + this.debitOrCreditCardIframe().locator( '#credit-card-number' ); + debitOrCreditCardExpirationInput = () => + this.debitOrCreditCardIframe().locator( '#expiry-date' ); + debitOrCreditCardCSCInput = () => + this.debitOrCreditCardIframe().locator( '#credit-card-security' ); + debitOrCreditCardBuyNowButton = () => + this.debitOrCreditCardIframe().locator( '#submit-button' ); + debitOrCreditCardFirstNameInput = () => + this.debitOrCreditCardIframe().locator( + '[id="billingAddress.givenName"]' + ); + debitOrCreditCardLastNameInput = () => + this.debitOrCreditCardIframe().locator( + '[id="billingAddress.familyName"]' + ); + debitOrCreditCardStreetInput = () => + this.debitOrCreditCardIframe().locator( '[id="billingAddress.line1"]' ); + debitOrCreditCardApartmentInput = () => + this.debitOrCreditCardIframe().locator( '[id="billingAddress.line2"]' ); + debitOrCreditCardCityInput = () => + this.debitOrCreditCardIframe().locator( '[id="billingAddress.city"]' ); + debitOrCreditCardStateInput = () => + this.debitOrCreditCardIframe().locator( '[id="billingAddress.state"]' ); + debitOrCreditCardZipCodeInput = () => + this.debitOrCreditCardIframe().locator( + '[id="billingAddress.postcode"]' + ); + debitOrCreditCardPhoneInput = () => + this.debitOrCreditCardIframe().locator( '#phone' ); + debitOrCreditCardEmailInput = () => + this.debitOrCreditCardIframe().locator( '#email' ); + debitOrCreditCardPayNowButton = () => + this.debitOrCreditCardIframe().locator( '#submit-button' ); + debitOrCreditCardTagline = () => + this.page + .frameLocator( 'iframe[name^="__zoid__paypal_buttons__"]' ) + .locator( '.paypal-powered-by' ); + debitOrCreditCardPoweredByText = () => + this.debitOrCreditCardTagline().getByText( 'Powered by' ); + debitOrCreditCardPoweredByLogo = () => + this.debitOrCreditCardTagline().locator( '.paypal-logo' ); + + standardCardButtonIframe = () => + this.page.frameLocator( + '#ppc-button-ppcp-card-button-gateway .component-frame' + ); + standardCardButton = () => + this.standardCardButtonIframe().locator( + `[data-funding-source="card"]` + ); + standardCardButtonDetailsIframe = () => + this.standardCardButtonIframe().frameLocator( 'iframe.zoid-visible' ); + standardCardButtonNumberInput = () => + this.standardCardButtonDetailsIframe().locator( '#credit-card-number' ); + standardCardButtonExpirationInput = () => + this.standardCardButtonDetailsIframe().locator( '#expiry-date' ); + standardCardButtonCSCInput = () => + this.standardCardButtonDetailsIframe().locator( + '#credit-card-security' + ); + standardCardButtonBuyNowButton = () => + this.standardCardButtonDetailsIframe().locator( '#submit-button' ); + standardCardButtonFirstNameInput = () => + this.standardCardButtonDetailsIframe().locator( + '[id="billingAddress.givenName"]' + ); + standardCardButtonLastNameInput = () => + this.standardCardButtonDetailsIframe().locator( + '[id="billingAddress.familyName"]' + ); + standardCardButtonStreetInput = () => + this.standardCardButtonDetailsIframe().locator( + '[id="billingAddress.line1"]' + ); + standardCardButtonApartmentInput = () => + this.standardCardButtonDetailsIframe().locator( + '[id="billingAddress.line2"]' + ); + standardCardButtonCityInput = () => + this.standardCardButtonDetailsIframe().locator( + '[id="billingAddress.city"]' + ); + standardCardButtonStateInput = () => + this.standardCardButtonDetailsIframe().locator( + '[id="billingAddress.state"]' + ); + standardCardButtonZipCodeInput = () => + this.standardCardButtonDetailsIframe().locator( + '[id="billingAddress.postcode"]' + ); + standardCardButtonPhoneInput = () => + this.standardCardButtonDetailsIframe().locator( '#phone' ); + standardCardButtonEmailInput = () => + this.standardCardButtonDetailsIframe().locator( '#email' ); + standardCardButtonPayNowButton = () => + this.standardCardButtonDetailsIframe().locator( '#submit-button' ); + standardCardButtonTagline = () => + this.page + .frameLocator( 'iframe[name^="__zoid__paypal_buttons__"]' ) + .locator( '.paypal-powered-by' ); + standardCardButtonPoweredByText = () => + this.standardCardButtonTagline().getByText( 'Powered by' ); + standardCardButtonPoweredByLogo = () => + this.standardCardButtonTagline().locator( '.paypal-logo' ); + + acdcCardsIcons = () => + this.page.locator( + '.payment_method_ppcp-credit-card-gateway > label > img' + ); + acdcStoredCredentialsText = () => + this.page.locator( + '#wc-ppcp-credit-card-gateway-payment-token-3 > label' + ); + acdcSaveToAccountCheckbox = () => + this.page.locator( '#wc-ppcp-credit-card-gateway-new-payment-method' ); + acdcSavedCards = () => + this.page.locator( '.woocommerce-SavedPaymentMethods-token' ); + acdcSavedCardByNumber = ( cardNumber ) => + this.acdcSavedCards() + .filter( { + hasText: `ending in ${ getLast4CardDigits( cardNumber ) }`, + } ) + .first(); + acdcUseNewPaymentRadio = () => + this.page.locator( '.woocommerce-SavedPaymentMethods-new > input' ); + + threeDSFrame1 = () => + this.page + .frameLocator( '.paypal-checkout-sandbox-iframe' ) + .frameLocator( '[name^="__zoid__three_domain_secure__"]' ); + threeDSFrame2 = () => + this.threeDSFrame1() + .frameLocator( '#threedsIframeV2' ) + .frameLocator( '[id^="cardinal-stepUpIframe-"]' ); + threeDSAcceptCookiesButton = () => + this.threeDSFrame1().locator( '#acceptAllButton' ); + threeDSOtpInput = () => + this.threeDSFrame2().locator( 'input[name="challengeDataEntry"]' ); + threeDSSubmitButton = () => + this.threeDSFrame2().locator( 'input.primary[type="submit"]' ); + + payUponInvoiceBirthDateInput = () => + this.page.locator( '#billing_birth_date' ); + payUponInvoiceGatewayTitle = () => + this.page.locator( + 'label[for="payment_method_ppcp-pay-upon-invoice-gateway"]' + ); + payUponInvoiceGatewayDescription = () => + this.payUponInvoiceGateway().locator( + 'div.payment_method_ppcp-pay-upon-invoice-gateway>p' + ); + + payPalSpinner = () => + this.page.locator( '.ppc-button-wrapper .blockUI.blockOverlay' ); + + venmoOverlayIframe = () => + this.page.frameLocator( '.venmo-checkout-sandbox-iframe' ); + venmoOverlayContinueButton = () => + this.venmoOverlayIframe().locator( '.venmo-checkout-continue' ); + + payPalButtonMoreOptions = () => + this.payPalIframe().locator( + '.paypal-button-wallet-menu .menu-button' + ); + payPalMenuIframe = () => + this.page.frameLocator( 'iframe[name^="__zoid__paypal_menu__"]' ); + payWithDifferentAccountButton = () => + this.payPalMenuIframe().getByText( 'pay with a different account' ); + + submitOrder = async () => { + // on Pay for Order page the button name is Pay for order + if ( this.page.url().includes( 'pay_for_order' ) ) { + await this.payForOrderButton().click(); + } else { + await this.placeOrderButton().click(); + } + }; + + // Actions + + /** + * Completes payment on Classic pages with given payment method + * + * @param data + * @param data.payment + * @param data.merchant + */ + makeClassicPayment = async ( data: { + payment: PcpPayment; + merchant?: PcpMerchant; + } ) => { + // Map to the tested method + switch ( data.payment.method ) { + case 'PayPal': + // pay with vaulted account + if ( data.payment.isVaulted ) { + await this.completePayPalVaultedPayment( + data.payment.payPalAccount + ); + break; + } + // pay with account other than vaulted + if ( data.payment.useNotVaultedAccount ) { + await this.completePayPalPayment( + await this.openPayPalPupupDifferentAccount(), + data.payment.useNotVaultedAccount + ); + break; + } + + await this.completePayPalPayment( + await this.openPayPalPupup(), + data.payment.payPalAccount + ); + break; + + case 'PayLater': + await this.completePayLaterPayment( + await this.openPayPalPupup( 'paylater' ), + data.payment.payPalAccount + ); + break; + + case 'ACDC': + if ( data.payment.isVaulted ) { + await this.completeAcdcVaultedPayment( + data.payment, + data.merchant + ); + break; + } + await this.completeAcdcPayment( data.payment, data.merchant ); + break; + + case 'ACDC3DS': + await this.completeAcdc3dsPayment( + data.payment, + data.merchant + ); + break; + + case 'OXXO': + await this.completeOXXOPayment(); + break; + + case 'Venmo': + await this.completePayPalPayment( + await this.openVenmoPopup(), + data.payment.payPalAccount + ); + break; + + case 'DebitOrCreditCard': + await this.completeDebitOrCreditCardPayment( + data.payment.card + ); + break; + + case 'StandardCardButton': + await this.completeStandardCardButtonPayment( + data.payment.card + ); + break; + + case 'PayUponInvoice': + await this.completePayUponInvoicePayment( + data.payment.birthDate + ); + break; + } + }; + + /** + * Completes payment on Block pages with given payment method + * + * @param data + */ + makePayment = async ( data ) => { + // Make payment with tested method + switch ( data.payment.method ) { + case 'PayPal': + // if(paymentData.payment.isVaulted) { + // await this.completePayPalVaultedPayment(paymentData.payment.payPalAccount); + // return; + // } + await this.completePayPalPayment( + await this.openBlockPayPalPopup(), + data.payment.payPalAccount + ); + break; + + case 'PayLater': + await this.completePayLaterPayment( + await this.openBlockPayLaterPopup(), + data.payment.payPalAccount + ); + break; + } + }; + + /** + * Adds payment method on My Account/Payment Methods page + * + * @param payment + */ + savePaymentMethod = async ( payment: PcpPayment ) => { + switch ( payment.method ) { + case 'PayPal': + await this.addPayPalPaymentMethod( payment.payPalAccount ); + break; + + case 'ACDC': + await this.addCardPaymentMethod( payment ); + break; + } + }; + + /** + * Opens PayPal popup + * + * @param fundingSource + * @return PayPalPopup + */ + openPayPalPupup = async ( fundingSource = 'paypal' ) => { + const popupPromise = this.page.waitForEvent( 'popup' ); + + await expect( this.fundingSourceButton( fundingSource ) ).toBeVisible(); + await this.fundingSourceButton( fundingSource ).click(); + + const popup = await popupPromise; + await popup.waitForLoadState(); + return new PayPalPopup( popup ); + }; + + openPayPalPupupDifferentAccount = async () => { + const popupPromise = this.page.waitForEvent( 'popup' ); + + await expect( this.payPalButtonMoreOptions() ).toBeVisible(); + await this.payPalButtonMoreOptions().click(); + + await expect( this.payWithDifferentAccountButton() ).toBeVisible(); + await this.payWithDifferentAccountButton().click(); + + const popup = await popupPromise; + await popup.waitForLoadState(); + return new PayPalPopup( popup ); + }; + + /** + * Opens Venmo popup + */ + openVenmoPopup = async () => { + const popupPromise = this.page.waitForEvent( 'popup' ); + + await expect( this.venmoButton() ).toBeVisible(); + await this.venmoButton().click(); + + const popup = await popupPromise; + await popup.waitForLoadState(); + await popup.locator( '.venmo-button-wrapper>button' ).click(); + return new PayPalPopup( popup ); + }; + + openBlockPayPalPopup = async () => { + const popupPromise = this.page.waitForEvent( 'popup' ); + + await expect( this.blockPayPalButton() ).toBeVisible(); + await this.blockPayPalButton().click(); + + const popup = await popupPromise; + await popup.waitForLoadState(); + return new PayPalPopup( popup ); + }; + + openBlockPayLaterPopup = async () => { + const popupPromise = this.page.waitForEvent( 'popup' ); + + await expect( this.blockPayLaterButton() ).toBeVisible(); + await this.blockPayLaterButton().click(); + + const popup = await popupPromise; + await popup.waitForLoadState(); + return new PayPalPopup( popup ); + }; + + /** + * Completes payment with PayPal + * + * @param payPalPopup + * @param payPalAccount + */ + completePayPalPayment = async ( + payPalPopup: PayPalPopup, + payPalAccount: PayPalAccount + ) => { + await payPalPopup.login( payPalAccount.email, payPalAccount.password ); + await expect( payPalPopup.popup ).toHaveTitle( 'PayPal Checkout' ); + await payPalPopup.completePayment(); + }; + + /** + * Completes payment with PayPal + * + * @param payPalAccount + */ + completePayPalVaultedPayment = async ( payPalAccount?: PayPalAccount ) => { + // await expect(this.fundingSourceButtonPayLabel('paypal')).toHaveText('Pay with'); + // await expect(this.fundingSourceButtonLabel('paypal')).toHaveText(payPalAccount.email); + const popupPromise = this.page.waitForEvent( 'popup' ); + + await expect( this.payPalButton() ).toBeVisible(); + await this.payPalButton().click(); + + const popup = await popupPromise; + await popup.waitForLoadState(); + const payPalPopup = new PayPalPopup( popup ); + await expect( payPalPopup.popup ).toHaveTitle( 'PayPal Checkout' ); + + await payPalPopup.completePayment(); + + await this.page.waitForLoadState(); + }; + + /** + * Completes payment with Pay Later + * + * @param payPalPopup + * @param payPalAccount = { "email": "...", "password": "..." } + */ + completePayLaterPayment = async ( + payPalPopup: PayPalPopup, + payPalAccount: PayPalAccount + ) => { + await payPalPopup.login( payPalAccount.email, payPalAccount.password ); + await expect( payPalPopup.payLaterSwitcher() ).toHaveAttribute( + 'aria-selected', + 'true' + ); + await expect( payPalPopup.submitPaymentButton() ).toBeVisible(); + await expect( payPalPopup.payLaterRadio() ).toBeVisible(); + await expect( payPalPopup.payLaterSwitcher() ).toBeEnabled(); + await payPalPopup.payLaterRadio().check(); + await payPalPopup.submitPaymentButton().click(); + await payPalPopup.completePayment(); + }; + + // In the following request Playwright replaces Auth header with Basic Auth from .env, + // But the header should be from PayPal. Here it's replaced manually: + replacePayPalAuthToken = async ( merchant: PcpMerchant ) => { + await this.page.route( + 'https://www.sandbox.paypal.com/v2/checkout/orders/**/*', + async ( route ) => { + const token = await this.ppapi.getToken( merchant ); + const originalHeaders = route.request().headers(); + const updatedHeaders = { + ...originalHeaders, + Authorization: `Bearer ${ token }`, + }; + await route.continue( { headers: updatedHeaders } ); + } + ); + }; + + /** + * Completes payment with ACDC + * + * @param payment + * @param merchant + */ + completeAcdcPayment = async ( + payment: PcpPayment, + merchant: PcpMerchant + ) => { + await expect( this.acdcGateway() ).toBeVisible(); + await this.acdcGateway().click(); + + //if some cards are already stored then "Use a new payment method" radio should be checked + if ( await this.acdcUseNewPaymentRadio().isVisible() ) { + await this.acdcUseNewPaymentRadio().check(); + } + + await this.cardNumberInput().fill( payment.card.card_number ); + // trick to properly fill expiration date input + await this.cardExpirationInput().click(); + for ( const char of payment.card.expiration_date ) { + await this.page.keyboard.type( char ); + await this.page.waitForTimeout( 250 ); + } + await this.cardCVVInput().fill( payment.card.card_cvv ); + + if ( payment.saveToAccount ) { + await this.acdcSaveToAccountCheckbox().check(); + } + + await this.submitOrder(); + await this.replacePayPalAuthToken( merchant ); + }; + + completeAcdcVaultedPayment = async ( + payment: PcpPayment, + merchant: PcpMerchant + ) => { + await expect( this.acdcGateway() ).toBeVisible(); + await this.acdcGateway().click(); + await this.acdcSavedCardByNumber( payment.card.card_number ).click(); + await this.submitOrder(); + await this.replacePayPalAuthToken( merchant ); + }; + + completeAcdc3dsPayment = async ( + payment: PcpPayment, + merchant: PcpMerchant + ) => { + await this.completeAcdcPayment( payment, merchant ); + await this.threeDSAcceptCookiesButton().click(); + await this.threeDSOtpInput().fill( payment.card.code_3ds ); + await this.threeDSSubmitButton().click(); + }; + + /** + * Completes payment with OXXO + */ + completeOXXOPayment = async () => { + await expect( this.oxxoGateway() ).toBeVisible(); + await this.oxxoGateway().click(); + await expect( + this.page.getByText( + 'OXXO allows you to pay bills and online purchases in-store with cash.' + ) + ).toBeVisible(); + + const popupPromise = this.page.waitForEvent( 'popup' ); + await this.submitOrder(); + const popup = await popupPromise; + const paypal = new PayPalPopup( popup ); + + await expect( + paypal.popup.getByText( 'Successful Payment', { exact: true } ) + ).toBeVisible(); + + await popup.close(); + }; + + /** + * Completes payment with Debit Or Credit Card + * + * @param card + */ + completeDebitOrCreditCardPayment = async ( + card: WooCommerce.CreditCard + ) => { + await expect( this.debitOrCreditCardButton() ).toBeVisible(); + await this.debitOrCreditCardButton().click(); + await this.debitOrCreditCardNumberInput().fill( card.card_number ); + await this.debitOrCreditCardExpirationInput().fill( + card.expiration_date + ); + await this.debitOrCreditCardCSCInput().fill( card.card_cvv ); + await this.debitOrCreditCardPayNowButton().click(); + }; + + /** + * Completes payment with Standard Card Button + * + * @param card + */ + completeStandardCardButtonPayment = async ( + card: WooCommerce.CreditCard + ) => { + await expect( this.standardCardButtonGateway() ).toBeVisible(); + await this.standardCardButtonGateway().click(); + await this.standardCardButton().click(); + await this.standardCardButtonNumberInput().fill( card.card_number ); + await this.standardCardButtonExpirationInput().fill( + card.expiration_date + ); + await this.standardCardButtonCSCInput().fill( card.card_cvv ); + await this.standardCardButtonPayNowButton().click(); + }; + + /** + * Completes payment with Pay upon Invoice + * + * @param birthDate + */ + completePayUponInvoicePayment = async ( birthDate: string ) => { + await expect( this.payUponInvoiceGateway() ).toBeVisible(); + await this.payUponInvoiceGateway().click(); + await this.payUponInvoiceBirthDateInput().click(); + await this.page.keyboard.type( birthDate ); + await this.submitOrder(); + }; + + /** + * Adds PayPal as customer's saved payment method (Vaulting) + * + * @param payPalAccount = { "email": "...", "password": "..." } + */ + addPayPalPaymentMethod = async ( payPalAccount: PayPalAccount ) => { + await expect( this.payPalGateway() ).toBeVisible(); + await this.payPalGateway().click(); + const payPal = await this.openPayPalPupup(); + await expect( payPal.popup ).toHaveTitle( + 'Log in to your PayPal account' + ); + await payPal.login( payPalAccount.email, payPalAccount.password ); + await expect( payPal.popup ).toHaveTitle( + 'PayPal Checkout - Review your payment' + ); + await payPal.savePaymentMethodAndContinue(); + }; + + addCardPaymentMethod = async ( payment: PcpPayment ) => { + await expect( this.debitCreditCardsGateway() ).toBeVisible(); + await this.debitCreditCardsGateway().click(); + await this.cardNumberInput().fill( payment.card.card_number ); + await this.cardExpirationInput().click(); + await this.page.keyboard.type( payment.card.expiration_date ); + await this.cardCVVInput().fill( payment.card.card_cvv ); + await this.addPaymentMethodButton().click(); + }; + + // Assertions + + assertPayPalButtonVisibility = async ( isVisible: boolean ) => { + await expect( this.payPalButton() ).toBeVisible( { + visible: isVisible, + } ); + }; + + assertPayLaterButtonVisibility = async ( isVisible: boolean ) => { + await expect( this.payLaterButton() ).toBeVisible( { + visible: isVisible, + } ); + }; + + assertDebitOrCreditCardButtonVisibility = async ( isVisible: boolean ) => { + await expect( this.debitOrCreditCardButton() ).toBeVisible( { + visible: isVisible, + } ); + }; + + assertPoweredByPayPalTextVisibility = async ( isVisible: boolean ) => { + await expect( this.debitOrCreditCardPoweredByText() ).toBeVisible( { + visible: isVisible, + } ); + await expect( this.debitOrCreditCardPoweredByLogo() ).toBeVisible( { + visible: isVisible, + } ); + }; + + assertMiniCartPayPalButtonVisibility = async ( isVisible: boolean ) => { + await this.cartMenu().hover(); + await expect( this.miniCartPayPalButton() ).toBeVisible( { + visible: isVisible, + } ); + }; + + assertPayPalButtonsHaveClass = async ( payPalButtonsFrontEnd, regex ) => { + const listLengthFrontEnd: any = await payPalButtonsFrontEnd.length; + for ( let el = 0; el < listLengthFrontEnd; el++ ) { + await expect( payPalButtonsFrontEnd[ el ] ).toHaveClass( regex ); + } + }; + + assertPayPalButtonsMiniCartHaveClass = async ( regex ) => { + const payPalButtonsFrontEnd = + await this.miniCartIframePayPalButton().all(); + const listLengthFrontEnd: any = await payPalButtonsFrontEnd.length; + for ( let el = 0; el < listLengthFrontEnd; el++ ) { + await expect( payPalButtonsFrontEnd[ el ] ).toHaveClass( regex ); + } + }; + + collectBlockSmartButtons = async () => { + const blockSmartButtons: any = []; + const listIframes = await this.blockSmartButtonIframe().all(); + for ( const iframe of listIframes ) { + const smartButton = iframe + .frameLocator( '.component-frame' ) + .locator( '.paypal-button' ); + await blockSmartButtons.push( smartButton ); + } + return blockSmartButtons; + }; +} diff --git a/tests/qa/utils/frontend/product.ts b/tests/qa/utils/frontend/product.ts new file mode 100644 index 000000000..024ab9903 --- /dev/null +++ b/tests/qa/utils/frontend/product.ts @@ -0,0 +1,39 @@ +/** + * External dependencies + */ +import { + Product as ProductBase, + expect, +} from '@inpsyde/playwright-utils/build'; +/** + * Internal dependencies + */ +import { PayPalUI } from './paypal-ui'; + +export class Product extends ProductBase { + ppui: PayPalUI; + + constructor( { page, ppui } ) { + super( { page } ); + this.ppui = ppui; + } + + // Locators + + // Actions + + addToCart = async ( productSlug: string ) => { + await this.visit( productSlug ); + await this.addToCartButton().click(); + }; + + makeOrder = async ( tested ) => { + await this.visit( tested.products[ 0 ].slug ); + await this.ppui.makeClassicPayment( { + merchant: tested.merchant, + payment: tested.payment, + } ); + }; + + // Assertions +} diff --git a/tests/qa/utils/frontend/shop.ts b/tests/qa/utils/frontend/shop.ts new file mode 100644 index 000000000..9c1ccd69e --- /dev/null +++ b/tests/qa/utils/frontend/shop.ts @@ -0,0 +1,23 @@ +/** + * External dependencies + */ +import { Shop as ShopBase } from '@inpsyde/playwright-utils/build'; +/** + * Internal dependencies + */ +import { PayPalUI } from './paypal-ui'; + +export class Shop extends ShopBase { + ppui: PayPalUI; + + constructor( { page, ppui } ) { + super( { page } ); + this.ppui = ppui; + } + + // Locators + + // Actions + + // Assertions +} diff --git a/tests/qa/utils/helpers.ts b/tests/qa/utils/helpers.ts new file mode 100644 index 000000000..d66d6ca98 --- /dev/null +++ b/tests/qa/utils/helpers.ts @@ -0,0 +1,256 @@ +/** + * Sets annotation about tested customer + * If tested customer is registered (has non-empty username), + * then his billing.country will be added to annotation for example: customer-germany + * Only one customer per country is used. + * customer-germany - is a name of storage state file to use in the test. + * + * @default {} - empty annotation for guest + * @param customer + * @return object with test annotation + */ +export function annotateVisitor( customer: WooCommerce.CreateCustomer ) { + const storageStateName = getCustomerStorageStateName( customer ); + return { + annotation: { + type: 'visitor', + description: storageStateName, + }, + }; +} + +/** + * Builds storage state file name for customer + * + * @param customer + * @return 'goest' or 'customer-' + */ +export function getCustomerStorageStateName( + customer: WooCommerce.CreateCustomer +) { + // check is tested customer is guest (has empty username) + if ( ! customer.username ) { + return 'guest'; + } + // for registered customers + const visitorCountry = codeToCountry( customer.billing.country ); + return `customer-${ visitorCountry }`; +} + +/** + * Converts customer.billing.country (code) to country name + * + * @param countryCode + * @return country name in lower case + */ +export function codeToCountry( countryCode: string ) { + const countries = { + AF: 'afghanistan', + AL: 'albania', + DZ: 'algeria', + AD: 'andorra', + AO: 'angola', + AG: 'antigua-and-barbuda', + AR: 'argentina', + AM: 'armenia', + AU: 'australia', + AT: 'austria', + AZ: 'azerbaijan', + BS: 'bahamas', + BH: 'bahrain', + BD: 'bangladesh', + BB: 'barbados', + BY: 'belarus', + BE: 'belgium', + BZ: 'belize', + BJ: 'benin', + BT: 'bhutan', + BO: 'bolivia', + BA: 'bosnia-and-herzegovina', + BW: 'botswana', + BR: 'brazil', + BN: 'brunei', + BG: 'bulgaria', + BF: 'burkina-faso', + BI: 'burundi', + CV: 'cape-verde', + KH: 'cambodia', + CM: 'cameroon', + CA: 'canada', + CF: 'central-african-republic', + TD: 'chad', + CL: 'chile', + CN: 'china', + CO: 'colombia', + KM: 'comoros', + CG: 'congo', + CD: 'democratic-republic-of-the-congo', + CR: 'costa-rica', + CI: 'cote-d-ivoire', + HR: 'croatia', + CU: 'cuba', + CY: 'cyprus', + CZ: 'czech-republic', + DK: 'denmark', + DJ: 'djibouti', + DM: 'dominica', + DO: 'dominican-republic', + EC: 'ecuador', + EG: 'egypt', + SV: 'el-salvador', + GQ: 'equatorial-guinea', + ER: 'eritrea', + EE: 'estonia', + SZ: 'eswatini', + ET: 'ethiopia', + FJ: 'fiji', + FI: 'finland', + FR: 'france', + GA: 'gabon', + GM: 'gambia', + GE: 'georgia', + DE: 'germany', + GH: 'ghana', + GR: 'greece', + GD: 'grenada', + GT: 'guatemala', + GN: 'guinea', + GW: 'guinea-bissau', + GY: 'guyana', + HT: 'haiti', + HN: 'honduras', + HU: 'hungary', + IS: 'iceland', + IN: 'india', + ID: 'indonesia', + IR: 'iran', + IQ: 'iraq', + IE: 'ireland', + IL: 'israel', + IT: 'italy', + JM: 'jamaica', + JP: 'japan', + JO: 'jordan', + KZ: 'kazakhstan', + KE: 'kenya', + KI: 'kiribati', + KP: 'north-korea', + KR: 'south-korea', + KW: 'kuwait', + KG: 'kyrgyzstan', + LA: 'laos', + LV: 'latvia', + LB: 'lebanon', + LS: 'lesotho', + LR: 'liberia', + LY: 'libya', + LI: 'liechtenstein', + LT: 'lithuania', + LU: 'luxembourg', + MG: 'madagascar', + MW: 'malawi', + MY: 'malaysia', + MV: 'maldives', + ML: 'mali', + MT: 'malta', + MH: 'marshall-islands', + MR: 'mauritania', + MU: 'mauritius', + MX: 'mexico', + FM: 'micronesia', + MD: 'moldova', + MC: 'monaco', + MN: 'mongolia', + ME: 'montenegro', + MA: 'morocco', + MZ: 'mozambique', + MM: 'myanmar', + NA: 'namibia', + NR: 'nauru', + NP: 'nepal', + NL: 'netherlands', + NZ: 'new-zealand', + NI: 'nicaragua', + NE: 'niger', + NG: 'nigeria', + MK: 'north-macedonia', + NO: 'norway', + OM: 'oman', + PK: 'pakistan', + PW: 'palau', + PS: 'palestine', + PA: 'panama', + PG: 'papua-new-guinea', + PY: 'paraguay', + PE: 'peru', + PH: 'philippines', + PL: 'poland', + PT: 'portugal', + QA: 'qatar', + RO: 'romania', + RU: 'russia', + RW: 'rwanda', + KN: 'saint-kitts-and-nevis', + LC: 'saint-lucia', + VC: 'saint-vincent-and-the-grenadines', + WS: 'samoa', + SM: 'san-marino', + ST: 'sao-tome-and-principe', + SA: 'saudi-arabia', + SN: 'senegal', + RS: 'serbia', + SC: 'seychelles', + SL: 'sierra-leone', + SG: 'singapore', + SK: 'slovakia', + SI: 'slovenia', + SB: 'solomon-islands', + SO: 'somalia', + ZA: 'south-africa', + SS: 'south-sudan', + ES: 'spain', + LK: 'sri-lanka', + SD: 'sudan', + SR: 'suriname', + SE: 'sweden', + CH: 'switzerland', + SY: 'syria', + TW: 'taiwan', + TJ: 'tajikistan', + TZ: 'tanzania', + TH: 'thailand', + TL: 'timor-leste', + TG: 'togo', + TO: 'tonga', + TT: 'trinidad-and-tobago', + TN: 'tunisia', + TR: 'turkey', + TM: 'turkmenistan', + TV: 'tuvalu', + UG: 'uganda', + UA: 'ukraine', + AE: 'united-arab-emirates', + GB: 'united-kingdom', + US: 'usa', + UY: 'uruguay', + UZ: 'uzbekistan', + VU: 'vanuatu', + VA: 'vatican-city', + VE: 'venezuela', + VN: 'vietnam', + YE: 'yemen', + ZM: 'zambia', + ZW: 'zimbabwe', + }; + return countries[ countryCode ]; +} + +export function generateRandomString( length: number ): string { + const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + let result = ''; + for ( let i = 0; i < length; i++ ) { + const randomIndex = Math.floor( Math.random() * alphabet.length ); + result += alphabet[ randomIndex ]; + } + return result; +} diff --git a/tests/qa/utils/index.ts b/tests/qa/utils/index.ts new file mode 100644 index 000000000..59ce3604a --- /dev/null +++ b/tests/qa/utils/index.ts @@ -0,0 +1,7 @@ +export * from './test'; +export * from './helpers'; +export * from './paypal-api'; +export * from './urls'; +export * from './utils'; +export * from './admin'; +export * from './frontend'; diff --git a/tests/qa/utils/paypal-api.ts b/tests/qa/utils/paypal-api.ts new file mode 100644 index 000000000..7e3138e52 --- /dev/null +++ b/tests/qa/utils/paypal-api.ts @@ -0,0 +1,372 @@ +/** + * External dependencies + */ +import { APIRequestContext, expect } from '@playwright/test'; +import { createAuthHeader } from '@inpsyde/playwright-utils/build'; +/** + * Internal dependencies + */ +import { PcpFundingSource, PcpMerchant, PcpPayment } from '../resources'; + +/** + * Class for PayPal API + */ + +export class PayPalAPI { + request: APIRequestContext; + apiBaseUrl = 'https://api-m.sandbox.paypal.com/v2'; + + constructor( { request } ) { + this.request = request; + } + + private apiRequest = async ( + requestType: string, + endPoint: string, + merchant: PcpMerchant, + data?: any + ) => { + try { + const response = await this.request[ requestType ]( + this.apiBaseUrl + endPoint, + { + headers: createAuthHeader( + merchant.client_id, + merchant.client_secret + ), + data, + } + ); + + if ( ! ( await response.ok() ) ) { + throw new Error( + `Request failed with status ${ await response.status() }` + ); + } + + return await response.json(); + } catch ( error ) { + console.error( 'An error occurred:', error ); + throw error; // Re-throw the error to propagate it further if needed + } + }; + + /** + * Gets payment info by PayPal Transaction ID + * + * @param merchant - { client_id: '...', client_secret: '...' } + */ + getToken = async ( merchant: PcpMerchant ) => { + const response = await this.request.post( + 'https://api.sandbox.paypal.com/v1/oauth2/token', + { + headers: createAuthHeader( + merchant.client_id, + merchant.client_secret + ), + form: { grant_type: 'client_credentials' }, + } + ); + return ( await response.json() ).access_token; + }; + + /** + * Gets payment info by PayPal Transaction ID + * + * @param resourceId - PayPal transaction ID + * @param merchant - { client_id: '...', client_secret: '...' } + */ + getCapturedPayment = async ( + resourceId: string, + merchant: PcpMerchant + ) => { + return await this.apiRequest( + 'get', + `/payments/captures/${ resourceId }`, + merchant + ); + }; + + /** + * Gets payment info for authorized transactions by PayPal Transaction ID + * + * @param resourceId - PayPal transaction ID + * @param merchant - { client_id: '...', client_secret: '...' } + */ + getAuthorizedPayment = async ( + resourceId: string, + merchant: PcpMerchant + ) => { + return await this.apiRequest( + 'get', + `/payments/authorizations/${ resourceId }`, + merchant + ); + }; + + /** + * Gets order info by PayPal Order ID + * + * @param resourceId - PayPal order ID + * @param merchant - { client_id: '...', client_secret: '...' } + */ + getOrder = async ( resourceId: string, merchant: PcpMerchant ) => { + return await this.apiRequest( + 'get', + `/checkout/orders/${ resourceId }`, + merchant + ); + }; + + /** + * Gets order info by PayPal Order ID + * + * @param resourceId - PayPal order ID + * @param merchant - { client_id: '...', client_secret: '...' } + */ + getRefund = async ( resourceId: string, merchant: PcpMerchant ) => { + return await this.apiRequest( + 'get', + `/payments/refunds/${ resourceId }`, + merchant + ); + }; + + getOrderIdFromWooCommerce = async ( + wooCommerceOrderJson: WooCommerce.Order + ) => { + return wooCommerceOrderJson.meta_data.filter( + ( el ) => el.key === '_ppcp_paypal_order_id' + )[ 0 ].value; + }; + + /** + * Gets PayPal payment ID for different gateways + * + * @param payPalOrder + * @param payment + */ + getPaymentIdFromOrder = async ( payPalOrder, payment: PcpPayment ) => { + const fundingSource: PcpFundingSource = payment.dataFundingSource; + + if ( fundingSource === 'pay_upon_invoice' ) { + return ''; + } + + if ( fundingSource === 'oxxo' ) { + return payPalOrder.purchase_units[ 0 ].payments.captures[ 0 ].id; + } + + if ( payment.isAuthorized ) { + return payPalOrder.purchase_units[ 0 ].payments.authorizations[ 0 ] + .id; + } + + return payPalOrder.purchase_units[ 0 ].payments.captures[ 0 ].id; + }; + + /** + * Gets payment from PayPal API + * + * @param paymentId + * @param shopOrder + */ + getPayment = async ( + paymentId: string, + shopOrder: WooCommerce.ShopOrder + ) => { + if ( shopOrder.payment.isAuthorized ) { + return await this.getAuthorizedPayment( + paymentId, + shopOrder.merchant + ); + } + return await this.getCapturedPayment( paymentId, shopOrder.merchant ); + }; + + /** + * + * @param resourceId + * @param shopOrder + */ + getFee = async ( resourceId: string, shopOrder: WooCommerce.ShopOrder ) => { + const fundingSource: PcpFundingSource = + shopOrder.payment.dataFundingSource; + if ( + [ 'pay_upon_invoice', 'oxxo' ].includes( fundingSource ) || + shopOrder.payment.isAuthorized + ) { + return; + } + const payment = await this.getPayment( resourceId, shopOrder ); + return payment.seller_receivable_breakdown.paypal_fee.value; + }; + + getPayout = async ( + resourceId: string, + shopOrder: WooCommerce.ShopOrder + ) => { + const fundingSource: PcpFundingSource = + shopOrder.payment.dataFundingSource; + if ( + [ 'pay_upon_invoice', 'oxxo' ].includes( fundingSource ) || + shopOrder.payment.isAuthorized + ) { + return; + } + const payment = await this.getPayment( resourceId, shopOrder ); + return payment.seller_receivable_breakdown.net_amount.value; + }; + + // Assertions + + /** + * Asserts PayPal order for different funding sources + * + * @param wooCommerceOrderJson + * @param shopOrder + */ + assertOrder = async ( + wooCommerceOrderJson: WooCommerce.Order, + shopOrder: WooCommerce.ShopOrder + ) => { + const fundingSource: PcpFundingSource = + shopOrder.payment.dataFundingSource; + const payPalOrderId = await this.getOrderIdFromWooCommerce( + wooCommerceOrderJson + ); + await expect( payPalOrderId ).not.toHaveLength( 0 ); + + const payPalOrder = await this.getOrder( + payPalOrderId, + shopOrder.merchant + ); + + if ( fundingSource !== 'oxxo' ) { + const payPalPaymentId = await this.getPaymentIdFromOrder( + payPalOrder, + shopOrder.payment + ); + await expect( + String( wooCommerceOrderJson.transaction_id ) + ).toEqual( String( payPalPaymentId ) ); + } + + const expectedIntent = shopOrder.payment.isAuthorized + ? 'AUTHORIZE' + : 'CAPTURE'; + await expect( payPalOrder.intent ).toEqual( expectedIntent ); + + switch ( fundingSource ) { + case 'oxxo': + await expect( payPalOrder.status ).toEqual( 'COMPLETED' ); + await expect( payPalOrder.payment_source ).toHaveProperty( + 'oxxo' + ); + await expect( payPalOrder.payment_source.oxxo.email ).toEqual( + shopOrder.customer.email + ); + break; + + case 'acdc': + await expect( payPalOrder.status ).toEqual( 'COMPLETED' ); + await expect( payPalOrder.payment_source ).toHaveProperty( + 'card' + ); + await expect( payPalOrder.payment_source.card.name ).toEqual( + `${ shopOrder.customer.first_name } ${ shopOrder.customer.last_name }` + ); + break; + + case 'card': + await expect( payPalOrder.status ).toEqual( 'COMPLETED' ); + await expect( payPalOrder.payment_source ).toHaveProperty( + 'paypal' + ); + await expect( + payPalOrder.payment_source.paypal.email_address + ).toEqual( shopOrder.customer.email ); + await expect( + payPalOrder.payment_source.paypal.name.given_name + ).toEqual( shopOrder.customer.first_name ); + await expect( + payPalOrder.payment_source.paypal.name.surname + ).toEqual( shopOrder.customer.last_name ); + break; + + case 'pay_upon_invoice': + await expect( payPalOrder.status ).toEqual( + 'PENDING_APPROVAL' + ); + const birthDate = shopOrder.payment.birthDate.split( '.' ); + await expect( payPalOrder.payment_source ).toHaveProperty( + 'pay_upon_invoice' + ); + await expect( + payPalOrder.payment_source.pay_upon_invoice.birth_date + ).toEqual( + `${ birthDate[ 2 ] }-${ birthDate[ 1 ] }-${ birthDate[ 0 ] }` + ); + break; + + default: + await expect( payPalOrder.status ).toEqual( 'COMPLETED' ); + await expect( payPalOrder.payment_source ).toHaveProperty( + 'paypal' + ); + if ( shopOrder.payment.isVaulted ) { + await expect( + payPalOrder.payment_source.paypal + ).toHaveProperty( 'attributes' ); + await expect( + payPalOrder.payment_source.paypal.attributes + ).toHaveProperty( 'vault' ); + await expect( + payPalOrder.payment_source.paypal.attributes.vault + .status + ).toEqual( 'VAULTED' ); + break; + } + + // SIARHEI-TODO fix this: + // if (!(shopOrder.payment.isVaulted==false && shopOrder.payment.method === 'PayPal')) { + await expect( + payPalOrder.payment_source.paypal.email_address + ).toEqual( shopOrder.payment.payPalAccount.email ); + // break; + // } + } + }; + + /** + * Asserts PayPal payment + * + * @param paymentId PayPal payment ID + * @param shopOrder + */ + assertPayment = async ( + paymentId: string, + shopOrder: WooCommerce.ShopOrder + ) => { + const fundingSource: PcpFundingSource = + shopOrder.payment.dataFundingSource; + + if ( fundingSource === 'pay_upon_invoice' ) { + return; + } + + const payment = await this.getPayment( paymentId, shopOrder ); + + if ( fundingSource === 'oxxo' ) { + await expect( payment.status ).toEqual( 'PENDING' ); + return; + } + + if ( shopOrder.payment.isAuthorized ) { + await expect( payment.status ).toEqual( 'CREATED' ); + return; + } + + await expect( payment.status ).toEqual( 'COMPLETED' ); + }; +} diff --git a/tests/qa/utils/test.ts b/tests/qa/utils/test.ts new file mode 100644 index 000000000..2f6785c2f --- /dev/null +++ b/tests/qa/utils/test.ts @@ -0,0 +1,193 @@ +/** + * External dependencies + */ +import fs from 'fs'; +import { APIRequestContext, Page } from '@playwright/test'; +import { + test as base, + expect, + WooCommerceApi, +} from '@inpsyde/playwright-utils/build'; +/** + * Internal dependencies + */ +import { PayPalAPI } from './paypal-api'; +import { PayPalUI } from './frontend/paypal-ui'; +import { Utils } from './utils'; + +// PCP tabs +import { + PcpOnboarding, + WooCommerceOrderEdit, + WooCommerceSubscriptionEdit, +} from './admin'; + +// WooCommerce front end +import { + Shop, + Product, + Cart, + Checkout, + ClassicCart, + ClassicCheckout, + PayForOrder, + OrderReceived, + CustomerAccount, + CustomerPaymentMethods, + CustomerSubscriptions, + ClassicPayForOrder, +} from './frontend'; + +type BaseExtend = { + ppapi: PayPalAPI; + ppui: PayPalUI; + visitorPage: Page; + visitorRequest: APIRequestContext; + visitorWooCommerceApi: WooCommerceApi; + + // PCP tabs + pcpOnboarding: PcpOnboarding; + + // WooCommerce dashboard + wooCommerceOrderEdit: WooCommerceOrderEdit; + wooCommerceSubscriptionEdit: WooCommerceSubscriptionEdit; + + // WooCommerce Guest front end + shop: Shop; + product: Product; + cart: Cart; + checkout: Checkout; + classicCart: ClassicCart; + classicCheckout: ClassicCheckout; + classicPayForOrder: ClassicPayForOrder; + payForOrder: PayForOrder; + orderReceived: OrderReceived; + customerAccount: CustomerAccount; + customerPaymentMethods: CustomerPaymentMethods; + customerSubscriptions: CustomerSubscriptions; + + // Utils & preconditions + utils: Utils; +}; + +const test = base.extend< BaseExtend >( { + ppapi: async ( { request }, use ) => { + await use( new PayPalAPI( { request } ) ); + }, + visitorPage: async ( { browser }, use, testInfo ) => { + // check if visitor is specified in test otherwise use guest + const storageStateName = + testInfo.annotations?.find( ( el ) => el.type === 'visitor' ) + ?.description || 'guest'; + const storageStatePath = `${ process.env.STORAGE_STATE_PATH }/${ storageStateName }.json`; + // apply current visitor's storage state to the context + const context = await browser.newContext( { + storageState: fs.existsSync( storageStatePath ) + ? storageStatePath + : undefined, + } ); + const page = await context.newPage(); + await use( page ); + await page.close(); + await context.close(); + }, + visitorRequest: async ( { visitorPage }, use ) => { + const request = visitorPage.request; + await use( request ); + }, + visitorWooCommerceApi: async ( { visitorRequest }, use ) => { + await use( new WooCommerceApi( { request: visitorRequest } ) ); + }, + ppui: async ( { visitorPage, ppapi }, use ) => { + await use( new PayPalUI( { page: visitorPage, ppapi } ) ); + }, + + // PCP settings + pcpOnboarding: async ( { page }, use ) => { + await use( new PcpOnboarding( { page } ) ); + }, + + // WooCommerce dashboard + wooCommerceOrderEdit: async ( { page }, use ) => { + await use( new WooCommerceOrderEdit( { page } ) ); + }, + wooCommerceSubscriptionEdit: async ( { page }, use ) => { + await use( new WooCommerceSubscriptionEdit( { page } ) ); + }, + + // WooCommerce front end + shop: async ( { visitorPage, ppui }, use ) => { + await use( new Shop( { page: visitorPage, ppui } ) ); + }, + product: async ( { visitorPage, ppui }, use ) => { + await use( new Product( { page: visitorPage, ppui } ) ); + }, + cart: async ( { visitorPage, ppui }, use ) => { + await use( new Cart( { page: visitorPage, ppui } ) ); + }, + checkout: async ( { visitorPage, ppui }, use ) => { + await use( new Checkout( { page: visitorPage, ppui } ) ); + }, + classicCart: async ( { visitorPage, ppui }, use ) => { + await use( new ClassicCart( { page: visitorPage, ppui } ) ); + }, + classicCheckout: async ( { visitorPage, ppui }, use ) => { + await use( new ClassicCheckout( { page: visitorPage, ppui } ) ); + }, + classicPayForOrder: async ( { visitorPage, ppui }, use ) => { + await use( new ClassicPayForOrder( { page: visitorPage, ppui } ) ); + }, + payForOrder: async ( { visitorPage, ppui }, use ) => { + await use( new PayForOrder( { page: visitorPage, ppui } ) ); + }, + orderReceived: async ( { visitorPage, ppui }, use ) => { + await use( new OrderReceived( { page: visitorPage, ppui } ) ); + }, + customerAccount: async ( { visitorPage, ppui }, use ) => { + await use( new CustomerAccount( { page: visitorPage, ppui } ) ); + }, + customerPaymentMethods: async ( { visitorPage, ppui }, use ) => { + await use( new CustomerPaymentMethods( { page: visitorPage, ppui } ) ); + }, + customerSubscriptions: async ( { visitorPage, ppui }, use ) => { + await use( new CustomerSubscriptions( { page: visitorPage, ppui } ) ); + }, + + // Utils & preconditions + utils: async ( + { + plugins, + wooCommerceUtils, + requestUtils, + wooCommerceApi, + pcpOnboarding, + payForOrder, + checkout, + classicCheckout, + orderReceived, + customerAccount, + customerPaymentMethods, + visitorWooCommerceApi, + }, + use + ) => { + await use( + new Utils( { + plugins, + wooCommerceUtils, + requestUtils, + wooCommerceApi, + pcpOnboarding, + payForOrder, + checkout, + classicCheckout, + orderReceived, + customerAccount, + customerPaymentMethods, + visitorWooCommerceApi, + } ) + ); + }, +} ); + +export { test, expect }; diff --git a/tests/qa/utils/urls.ts b/tests/qa/utils/urls.ts new file mode 100644 index 000000000..dcb655e13 --- /dev/null +++ b/tests/qa/utils/urls.ts @@ -0,0 +1,25 @@ +/** + * External dependencies + */ +import { urls } from '@inpsyde/playwright-utils/build'; + +export default { + ...urls.frontend, + admin: { + ...urls.admin, + }, + pcp: { + onboarding: + 'wp-admin/admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway', + overview: + '/wp-admin/admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway&panel=overview', + paymentMehods: + '/wp-admin/admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway&panel=payment-methods', + settings: + '/wp-admin/admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway&panel=settings', + styling: + '/wp-admin/admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway&panel=styling', + payLaterMessaging: + '/wp-admin/admin.php?page=wc-settings&tab=checkout§ion=ppcp-gateway&panel=pay-later-messaging', + }, +}; diff --git a/tests/qa/utils/utils.ts b/tests/qa/utils/utils.ts new file mode 100644 index 000000000..b9e2f312b --- /dev/null +++ b/tests/qa/utils/utils.ts @@ -0,0 +1,268 @@ +/** + * External dependencies + */ +import { + WooCommerceApi, + RequestUtils, + Plugins, + WooCommerceUtils, + restLogin, +} from '@inpsyde/playwright-utils/build'; +/** + * Internal dependencies + */ +import { PcpOnboarding } from './admin'; +import { + PayForOrder, + Checkout, + ClassicCheckout, + OrderReceived, + CustomerAccount, + CustomerPaymentMethods, +} from './frontend'; +import { + subscriptionsPlugin, + wpDebuggingPlugin, + pcpPlugin, + PcpMerchant, + PcpConfig, +} from '../resources'; +import { getCustomerStorageStateName } from './helpers'; + +export class Utils { + plugins: Plugins; + wooCommerceUtils: WooCommerceUtils; + requestUtils: RequestUtils; + wooCommerceApi: WooCommerceApi; + visitorWooCommerceApi: WooCommerceApi; + pcpOnboarding: PcpOnboarding; + payForOrder: PayForOrder; + checkout: Checkout; + classicCheckout: ClassicCheckout; + orderReceived: OrderReceived; + customerAccount: CustomerAccount; + customerPaymentMethods: CustomerPaymentMethods; + + constructor( { + plugins, + wooCommerceUtils, + requestUtils, + wooCommerceApi, + pcpOnboarding, + payForOrder, + checkout, + classicCheckout, + orderReceived, + customerAccount, + customerPaymentMethods, + visitorWooCommerceApi, + } ) { + this.plugins = plugins; + this.wooCommerceUtils = wooCommerceUtils; + this.requestUtils = requestUtils; + this.wooCommerceApi = wooCommerceApi; + this.pcpOnboarding = pcpOnboarding; + this.payForOrder = payForOrder; + this.checkout = checkout; + this.classicCheckout = classicCheckout; + this.orderReceived = orderReceived; + this.customerAccount = customerAccount; + this.customerPaymentMethods = customerPaymentMethods; + this.visitorWooCommerceApi = visitorWooCommerceApi; + } + + restoreCustomer = async ( customer: WooCommerce.CreateCustomer ) => { + await this.wooCommerceUtils.deleteCustomer( customer ); + await this.wooCommerceUtils.createCustomer( customer ); + const storageStateName = getCustomerStorageStateName( customer ); + const storageStatePath = `${ process.env.STORAGE_STATE_PATH }/${ storageStateName }.json`; + await restLogin( { + baseURL: process.env.WP_BASE_URL, + httpCredentials: { + username: process.env.WP_BASIC_AUTH_USER, + password: process.env.WP_BASIC_AUTH_PASS, + }, + storageStatePath, + user: { + username: customer.username, + password: customer.password, + }, + } ); + }; + + payForApiOrder = async ( + orderId: number, + orderKey: string, + order: WooCommerce.ShopOrder + ) => { + await this.payForOrder.visit( orderId, orderKey ); + await this.payForOrder.ppui.makeClassicPayment( { + merchant: order.merchant, + payment: order.payment, + } ); + return await this.wooCommerceApi.getOrderWithStatus( + orderId, + 'processing' + ); + }; + + /** + * Pays for order on checkout page + * + * @param products + */ + fillVisitorsCart = async ( products: WooCommerce.CreateProduct[] ) => { + const cartProducts = await this.wooCommerceUtils.createCartProducts( + products + ); + await this.visitorWooCommerceApi.clearCart(); + await this.visitorWooCommerceApi.addProductsToCart( cartProducts ); + }; + + /** + * Pays for order on checkout page + * + * @param shopOrder + */ + completeOrderOnCheckout = async ( shopOrder: WooCommerce.ShopOrder ) => { + await this.fillVisitorsCart( shopOrder.products ); + + await this.checkout.makeOrder( shopOrder ); + const orderId = await this.orderReceived.getOrderNumber(); + return await this.wooCommerceApi.getOrderWithStatus( + orderId, + 'processing' + ); + }; + + /** + * Pays for order on classic checkout page + * + * @param shopOrder + */ + completeOrderOnClassicCheckout = async ( + shopOrder: WooCommerce.ShopOrder + ) => { + await this.fillVisitorsCart( shopOrder.products ); + await this.classicCheckout.makeOrder( shopOrder ); + const orderId = await this.orderReceived.getOrderNumber(); + return await this.wooCommerceApi.getOrderWithStatus( + orderId, + 'processing' + ); + }; + + connectMerchant = async ( + merchant: PcpMerchant, + options = { + enablePayUponInvoice: false, + } + ) => {}; + + disconnectMerchant = async () => {}; + + /** + * Enable PayPal funding source + * + * @param method + */ + pcpPaymentMethodIsEnabled = async ( method ) => { + switch ( method ) { + case 'PayPal': + break; + + case 'PayLater': + break; + + case 'Venmo': + break; + + case 'ACDC': + break; + + case 'OXXO': + break; + + case 'DebitOrCreditCard': + break; + + case 'StandardCardButton': + break; + + case 'PayUponInvoice': + break; + } + }; + + /** + * Configures store according to the data provided + * + * @param {Object} data see /resources/woocommerce-config.ts + */ + configureStore = async ( data ) => { + if ( data.wpDebugging === true ) { + await this.requestUtils.activatePlugin( wpDebuggingPlugin.slug ); + } + + if ( data.wpDebugging === false ) { + await this.requestUtils.deactivatePlugin( wpDebuggingPlugin.slug ); + } + + if ( data.subscription === true ) { + await this.requestUtils.activatePlugin( subscriptionsPlugin.slug ); + } + + if ( data.subscription === false ) { + await this.requestUtils.deactivatePlugin( + subscriptionsPlugin.slug + ); + } + + if ( data.classicPages === true ) { + await this.wooCommerceUtils.activateClassicCartPage(); + await this.wooCommerceUtils.activateClassicCheckoutPage(); + } + + if ( data.classicPages === false ) { + await this.wooCommerceUtils.activateBlockCartPage(); + await this.wooCommerceUtils.activateBlockCheckoutPage(); + } + + if ( data.settings?.general ) { + await this.wooCommerceApi.updateGeneralSettings( + data.settings.general + ); + } + + if ( data.taxes ) { + await this.wooCommerceUtils.setTaxes( data.taxes ); + } + + if ( data.customer ) { + await this.restoreCustomer( data.customer ); + } + }; + + configurePcp = async ( data: PcpConfig ) => { + if ( + ! ( await this.requestUtils.isPluginInstalled( pcpPlugin.slug ) ) + ) { + await this.plugins.installPluginFromFile( pcpPlugin.zipFilePath ); + } + await this.requestUtils.activatePlugin( pcpPlugin.slug ); + + if ( data.merchant ) { + if ( data.clearPCPDB ) { + } + + if ( data.disconnectMerchant ) { + await this.disconnectMerchant(); + return; + } + + await this.connectMerchant( data.merchant, { + enablePayUponInvoice: data.enablePayUponInvoice || false, + } ); + } + }; +}