├── .editorconfig
├── .env.example
├── .github
└── workflows
│ ├── auto-merge.yml
│ └── main.yml
├── .gitignore
├── .gitlab-ci.yml
├── .npmignore
├── AGENTS.md
├── CHANGELOG.md
├── LICENSE
├── README.md
├── build.js
├── bypass-captcha.config.example.ts
├── install.js
├── package-lock.json
├── package.json
├── playwright.config.example.ts
├── tests.config.example.ts
├── tests
├── account.spec.ts
├── auth.setup.ts
├── cart.spec.ts
├── category.spec.ts
├── checkout.spec.ts
├── compare.spec.ts
├── config
│ ├── element-identifiers.json
│ ├── input-values.json
│ ├── outcome-markers.json
│ ├── slugs.json
│ └── test-toggles.json
├── contact.spec.ts
├── healthcheck.spec.ts
├── home.spec.ts
├── login.spec.ts
├── mainmenu.spec.ts
├── minicart.spec.ts
├── orderhistory.spec.ts
├── poms
│ ├── adminhtml
│ │ └── magentoAdmin.page.ts
│ └── frontend
│ │ ├── account.page.ts
│ │ ├── cart.page.ts
│ │ ├── category.page.ts
│ │ ├── checkout.page.ts
│ │ ├── compare.page.ts
│ │ ├── contact.page.ts
│ │ ├── home.page.ts
│ │ ├── login.page.ts
│ │ ├── mainmenu.page.ts
│ │ ├── minicart.page.ts
│ │ ├── newsletter.page.ts
│ │ ├── orderhistory.page.ts
│ │ ├── product.page.ts
│ │ ├── register.page.ts
│ │ └── search.page.ts
├── product.spec.ts
├── register.spec.ts
├── search.spec.ts
├── setup.spec.ts
├── types
│ └── magewire.d.ts
└── utils
│ ├── env.utils.ts
│ ├── magewire.utils.ts
│ └── notification.validator.ts
├── translate-json.js
└── tsconfig.example.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_size = 4
7 | indent_style = space
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.{ts,json}]
12 | indent_size = 2
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | PLAYWRIGHT_BASE_URL=https://hyva-demo.elgentos.io/
2 | PLAYWRIGHT_PRODUCTION_URL=https://hyva-demo.elgentos.io/
3 | PLAYWRIGHT_REVIEW_URL=https://hyva-demo.elgentos.io/
4 |
5 | MAGENTO_ADMIN_SLUG=
6 | MAGENTO_ADMIN_USERNAME=
7 | MAGENTO_ADMIN_PASSWORD=
8 | MAGENTO_THEME_LOCALE=
9 |
10 | MAGENTO_NEW_ACCOUNT_PASSWORD=
11 | MAGENTO_EXISTING_ACCOUNT_EMAIL_CHROMIUM=
12 | MAGENTO_EXISTING_ACCOUNT_EMAIL_FIREFOX=
13 | MAGENTO_EXISTING_ACCOUNT_EMAIL_WEBKIT=
14 | MAGENTO_EXISTING_ACCOUNT_PASSWORD=
15 | MAGENTO_EXISTING_ACCOUNT_CHANGED_PASSWORD=
16 |
17 | MAGENTO_COUPON_CODE_CHROMIUM=
18 | MAGENTO_COUPON_CODE_FIREFOX=
19 | MAGENTO_COUPON_CODE_WEBKIT=
20 |
21 | CAPTCHA_BYPASS=
22 |
--------------------------------------------------------------------------------
/.github/workflows/auto-merge.yml:
--------------------------------------------------------------------------------
1 | name: Auto Merge gitlab-main to main
2 |
3 | on:
4 | push:
5 | branches:
6 | - gitlab-main
7 |
8 | jobs:
9 | pr-and-merge:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Checkout repo
14 | uses: actions/checkout@v3
15 |
16 | - name: Authenticate GitHub CLI
17 | run: gh auth setup-git
18 | env:
19 | GH_TOKEN: ${{ secrets.AUTOMERGE_TOKEN }}
20 |
21 | - name: Check or create PR
22 | id: pr
23 | env:
24 | GH_TOKEN: ${{ secrets.AUTOMERGE_TOKEN }}
25 | run: |
26 | set -e
27 | pr_number=$(gh pr list --head gitlab-main --base main --state open --json number -q '.[0].number')
28 |
29 | if [ -z "$pr_number" ]; then
30 | echo "No PR found. Creating new PR..."
31 | gh pr create \
32 | --head gitlab-main \
33 | --base main \
34 | --title "Sync gitlab-main to main" \
35 | --body "Automated PR from GitLab CI via gh CLI" \
36 | --label auto-merge
37 |
38 | # PR is just created; get number again
39 | pr_number=$(gh pr list --head gitlab-main --base main --state open --json number -q '.[0].number')
40 | else
41 | echo "PR already exists with number: $pr_number"
42 | fi
43 |
44 | echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT"
45 |
46 | - name: Approve PR
47 | id: pr_approve
48 | env:
49 | GH_TOKEN: ${{ secrets.AUTOAPPROVE_TOKEN }}
50 | run: |
51 | PR_NUMBER="${{ steps.pr.outputs.pr_number }}"
52 | gh pr review "$PR_NUMBER" --approve
53 | echo "merge_approved=true" >> "$GITHUB_OUTPUT"
54 |
55 | - name: Merge PR
56 | id: pr_merge
57 | if: steps.pr_approve.outputs.merge_approved == 'true'
58 | env:
59 | GH_TOKEN: ${{ secrets.AUTOMERGE_TOKEN }}
60 | run: |
61 | PR_NUMBER="${{ steps.pr.outputs.pr_number }}"
62 | gh pr merge "$PR_NUMBER" --merge --admin
63 | echo "merge_succeeded=true" >> "$GITHUB_OUTPUT"
64 |
65 | - name: Delete branch
66 | if: steps.pr_merge.outputs.merge_succeeded == 'true'
67 | env:
68 | GH_TOKEN: ${{ secrets.AUTOMERGE_TOKEN }}
69 | run: |
70 | echo "Deleting remote branch 'gitlab-main'"
71 | gh auth setup-git
72 | git push origin --delete gitlab-main
73 | echo "branch_deleted=true" >> "$GITHUB_OUTPUT"
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Magento 2 Playwright Self Test
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | build-and-selftest:
10 | if: github.head_ref != 'gitlab-main'
11 | runs-on: windows-latest
12 |
13 | env:
14 | PLAYWRIGHT_BASE_URL: https://hyva-demo.elgentos.io/
15 | PLAYWRIGHT_PRODUCTION_URL: https://hyva-demo.elgentos.io/
16 | PLAYWRIGHT_STAGING_URL: https://hyva-demo.elgentos.io/
17 |
18 | MAGENTO_ADMIN_SLUG: ${{ secrets.MAGENTO_ADMIN_SLUG }}
19 | MAGENTO_ADMIN_USERNAME: ${{ secrets.MAGENTO_ADMIN_USERNAME }}
20 | MAGENTO_ADMIN_PASSWORD: ${{ secrets.MAGENTO_ADMIN_PASSWORD }}
21 |
22 | MAGENTO_NEW_ACCOUNT_PASSWORD: ${{ secrets.MAGENTO_NEW_ACCOUNT_PASSWORD }}
23 | MAGENTO_EXISTING_ACCOUNT_EMAIL_CHROMIUM: ${{ secrets.MAGENTO_EXISTING_ACCOUNT_EMAIL_CHROMIUM }}
24 | MAGENTO_EXISTING_ACCOUNT_EMAIL_FIREFOX: ${{ secrets.MAGENTO_EXISTING_ACCOUNT_EMAIL_FIREFOX }}
25 | MAGENTO_EXISTING_ACCOUNT_EMAIL_WEBKIT: ${{ secrets.MAGENTO_EXISTING_ACCOUNT_EMAIL_WEBKIT }}
26 | MAGENTO_EXISTING_ACCOUNT_PASSWORD: ${{ secrets.MAGENTO_EXISTING_ACCOUNT_PASSWORD }}
27 | MAGENTO_EXISTING_ACCOUNT_CHANGED_PASSWORD: ${{ secrets.MAGENTO_EXISTING_ACCOUNT_CHANGED_PASSWORD }}
28 |
29 | MAGENTO_COUPON_CODE_CHROMIUM: ${{ secrets.MAGENTO_COUPON_CODE_CHROMIUM }}
30 | MAGENTO_COUPON_CODE_FIREFOX: ${{ secrets.MAGENTO_COUPON_CODE_FIREFOX }}
31 | MAGENTO_COUPON_CODE_WEBKIT: ${{ secrets.MAGENTO_COUPON_CODE_WEBKIT }}
32 |
33 | CAPTCHA_BYPASS: true
34 |
35 | steps:
36 | - name: Checkout repository
37 | uses: actions/checkout@v3
38 |
39 | - name: Set up Node.js
40 | uses: actions/setup-node@v3
41 | with:
42 | node-version: 18
43 |
44 | - name: Install dependencies
45 | run: npm install
46 |
47 | - name: Install Playwright browsers
48 | run: npx playwright install --with-deps
49 |
50 | - name: Copy config files
51 | run: |
52 | cp playwright.config.example.ts playwright.config.ts
53 | cp bypass-captcha.config.example.ts bypass-captcha.config.ts
54 | cp tests.config.example.ts tests.config.ts
55 | cp tsconfig.example.json tsconfig.json
56 |
57 | - name: Run Playwright setup test
58 | run: npx playwright test --reporter=line --workers=4 tests/setup.spec.ts
59 | env:
60 | CI: true
61 |
62 | - name: Run Playwright tests
63 | run: npx playwright test --workers=4 --grep-invert "@setup" --max-failures=1
64 | env:
65 | CI: true
66 | - uses: actions/upload-artifact@v4
67 | if: ${{ !cancelled() }}
68 | with:
69 | name: playwright-report
70 | path: playwright-report/
71 | retention-days: 30
72 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | #folders
2 | .idea
3 | .vscode
4 | node_modules/
5 | /test-results/
6 | /auth-storage/
7 | /playwright-report/
8 |
9 | #files
10 | /*.config.ts
11 | /.env
12 | /tsconfig.json
13 |
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | image: node:18-bullseye
2 |
3 | stages:
4 | - mirror
5 |
6 | mirror_to_github:
7 | stage: mirror
8 | only:
9 | - main
10 | script:
11 | - mkdir -p ~/.ssh
12 | - printf "%s\n" "$GIT_SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
13 | - chmod 600 ~/.ssh/id_ed25519
14 | - ssh-keyscan github.com >> ~/.ssh/known_hosts
15 | - git config --global user.name "developer-elgentos"
16 | - git config --global user.email "developer@elgentos.nl"
17 | - git remote remove github || true
18 | - git remote add github git@github.com:elgentos/magento2-playwright.git
19 | - git checkout main
20 | - git push github main:gitlab-main
21 |
22 | test_mirror_pipeline:
23 | stage: mirror
24 | rules:
25 | - if: '$CI_COMMIT_BRANCH != "main"'
26 | script:
27 | - mkdir -p ~/.ssh
28 | - printf "%s\n" "$GIT_SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
29 | - chmod 600 ~/.ssh/id_ed25519
30 | - ssh-keyscan github.com >> ~/.ssh/known_hosts
31 | - git config --global user.name "developer-elgentos"
32 | - git config --global user.email "developer@elgentos.nl"
33 | - git ls-remote git@github.com:elgentos/magento2-playwright.git
34 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | #folders
2 | .idea
3 | .vscode
4 | .github
5 | node_modules/
6 | /test-results/
7 | /auth-storage/
8 | /playwright-report/
9 |
10 | #files
11 | /*.config.ts
12 | /.env
13 | /tsconfig.json
--------------------------------------------------------------------------------
/AGENTS.md:
--------------------------------------------------------------------------------
1 | # Repository Guidelines
2 |
3 | ## Style
4 | - Use two spaces for indentation in TypeScript and JSON files.
5 | - Ensure files end with a newline and no trailing whitespace.
6 | - Never use hardcoded strings in your code. Always add or use variables from the config folder.
7 |
8 | ## Commit Messages
9 | - Write concise commit messages describing the change.
10 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5 |
6 | ## [2.0.0-alpha] - 2025-07-02
7 | The initial NPM Package release!
8 |
9 | Due to a difference in releases between the GitHub version and the npm package, we've now marked both as version 2.0.0-alpha to signify their merger and to avoid confusion in the future.
10 |
11 | ### Added
12 |
13 | - Build scripts to help set up the testing suite through the npm package!
14 | - Configuration helper files that make customization easier by automatically picking the correct import of JSON files.
15 | - Custom files in 'tests' folder will now automatically be used if corresponding spec file is also placed in test folder.
16 | - Notification Validator: A utility for validating Magento frontend messages during testing (e.g. "Add to Cart" success message).
17 | - Environment Variable Utility: Centralized handling of environment variables for consistent configuration across test environments.
18 | - Magewire Utility: Helper functions to interact with Magewire components in tests.
19 | - New tests:
20 | - `search.spec.ts`
21 | - `product.spec.ts`
22 | - `orderhistory.spec.ts`
23 | - `healthcheck.spec.ts`
24 | - `compare.spec.ts`
25 | - `category.spec.ts`
26 |
27 | ### Changed
28 |
29 | - The elgentos testing suite is now a npm package for maintainability: [link to testing suite npm package](https://www.npmjs.com/package/@elgentos/magento2-playwright).
30 | - Removed test toggles to avoid issues where the suite would fail without giving a helpful message.
31 | - Added 'smoke' tags to healthcheck tests to better adhere to industry standards.
32 | - 'Fixtures' folder is now 'poms'.
33 | - Split the magento admin page files and frontend page files in the renamed 'poms' folder.
34 | - .env attributes renamed, and defaults will be suggested during installation.
35 | - Divided the steps in `setup.spec.ts` to make them easier to read.
36 | - Moved and renamed config files
37 | - Readme files
38 |
39 | ### Fixes
40 |
41 | - Various fixes for stability, better adherence to the DRY-principle and separation of concerns.
42 |
43 | ## [1.0.0-alpha] - 2025-01-29
44 | The initial Alpha Release!
45 |
46 | ### Added
47 |
48 | - Setup script to make it easier for users to prepare Magento 2 admin section.
49 | - Test cases for key features such as creating an account, ordering a product, adding a coupon code, and more.
50 | - Element identifiers, input values, and outcome markers added in JSON files to make customization easier.
51 | - Example GitHub Actions workflow to show how easily our tool can be integrated into the CI/CD pipeline.
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Elgentos
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # elgentos Magento 2 Playwright BDD E2E Testing Suite
2 |
3 | This package contains an end-to-end (E2E) testing suite for Magento 2, powered by [Playwright](https://playwright.dev/). It enables you to quickly set up, run, and extend automated browser tests for your Magento 2 store. Installation is simple via npm, allowing you to seamlessly integrate robust testing into your development workflow.
4 |
5 |
6 | ⚠️ Please note: if you’re not sure what each test does, **then you should only run this in a testing environment**! Some tests involve the database, and for the suite to run `setup.spec.ts` will disable the CATPCHA of your webshop.
7 |
8 |
9 | 🏃**Just want to install and get going?**
10 |
11 | If you’re simply looking to install, check the [prerequisites](#prerequisites) and then go to [🧪 Installing the suite](#-installing-the-suite).
12 |
13 |
14 |
15 | ---
16 |
17 | ## Table of contents
18 |
19 | - [Prerequisites](#Prerequisites)
20 | - [Installing the suite](#-installing-the-suite)
21 | - [Before your run](#-before-you-run)
22 | - [Running the setup](#-run-setup-then-you-can-run-the-suite)
23 | - [How to use the testing suite](#-how-to-use-the-testing-suite)
24 | - [Running tests](#running-tests)
25 | - [Skipping specific tests](#skipping-specific-tests)
26 | - [Tags and Annotations](#tags-and-annotations)
27 | - [Customizing the testing suite](#-customizing-the-testing-suite)
28 | - [Examples](#examples)
29 | - [How to help](#how-to-help)
30 |
31 | ---
32 |
33 | ## Prerequisites
34 |
35 | * This testing suite has been designed to work within a Hÿva theme in Magento 2, but can work with other themes.
36 | * **Magento 2 instance:** A running instance of Magento 2 for testing purposes. Elgentos sponsors a [Hyvä demo website](https://hyva-demo.elgentos.io/) for this project.
37 |
38 |
39 |
40 | ---
41 |
42 | ## 🧪 Installing the suite
43 |
44 | 1. **Create a playwright/ directory inside your theme’s** `/web` **folder**
45 |
46 | Navigate to the `web` folder of your theme. This is usually located in `app/design/frontend/{vendor}/{theme}/web`. Within this folder, create a `playwright` folder, then navigate to it:
47 |
48 |
49 | ```bash
50 | cd app/design/frontend/demo-store/demo-theme/web
51 | mkdir playwright
52 | cd playwright
53 | ```
54 |
55 |
56 | 2. **Initialize an npm project:**
57 |
58 |
59 | ```bash
60 | npm init -y
61 | ```
62 |
63 |
64 | 3. **Install the test suite package**
65 |
66 | Lastly, simply run the command to install the elgentos Magento2 Playwright package, and the installation script will set things up for you! You will be prompted to input values for the `.env` variables, but these also come with default values.
67 |
68 |
69 | ```bash
70 | npm install @elgentos/magento2-playwright
71 | ```
72 |
73 |
74 |
75 | ---
76 |
77 | ## ⏸️ Before you run
78 |
79 | After the installation, a variety of folders will have been created. Most notable in these are `base-tests`, which contain the tests without alteration, and `tests`. **You will never have to make changes to the** `base-tests` **folder. Doing so might break things, so please be cautious.**
80 |
81 |
82 |
83 | > If you want to make changes to your iteration of the testing suite such as making changes to how the test works, updating element identifiers etc., see the section ‘Customizing the testing suite’ below.
84 |
85 |
86 |
87 | ---
88 |
89 | ## 🤖 Run setup… then you can run the suite!
90 |
91 | Finally, before running the testing suite, you must run `setup.spec.ts`. This must be done as often as your server resets. You can run this using the following command:
92 |
93 |
94 | ```bash
95 | npx playwright test --grep "@setup" --trace on
96 | ```
97 |
98 |
99 | After that - you’re all set! 🥳 You can run the testing suite - feel free to skip the setup at this point:
100 |
101 | ```bash
102 | npx playrwight test --grep-invert "@setup" --trace on
103 | ```
104 |
105 |
106 |
107 | ---
108 |
109 | ## 🚀 How to use the testing suite
110 |
111 | The Testing Suite offers a variety of tests for your Magento 2 application in Chromium, Firefox, and Webkit.
112 |
113 |
114 | ### Running tests
115 |
116 | To run all tests, run the following command:
117 |
118 | ```bash
119 | npx playwright test --grep-invert "@setup"
120 | ```
121 |
122 |
123 | This command will run all tests located in the `base-tests` directory. If you have custom tests in the `test` folder, these will be used instead of their `base-tests` counterpart.
124 |
125 |
126 | You can also run a specific test file:
127 |
128 | ```bash
129 | npx playwright test tests/example.test.ts
130 | ```
131 |
132 |
133 | The above commands will run your tests, then offer a report. You can also use [the UI mode](https://playwright.dev/docs/running-tests#debug-tests-in-ui-mode) to see what the tests are doing, which is helpful for debugging. To open up UI mode, run this command:
134 |
135 |
136 | ```bash
137 | npx playwright test --ui
138 | ```
139 |
140 |
141 | Playwright also offers a trace view. While using the UI mode is seen as the default for developing and debugging tests, you may want to run the tests and collect a trace instead. This can be done with the following command:
142 |
143 |
144 | ```bash
145 | npx playwright test --trace on
146 | ```
147 |
148 |
149 | ### Skipping specific tests
150 |
151 | Certain `spec` files and specific tests are used as a setup. For example, all setup tests (such as creating an account and setting a coupon code in your Magento 2 environment) have the tag ‘@setup’. Since these only have to be used once (or in the case of our demo website every 24 hours), most of the time you can skip these. These means most of the time, using the following command is best. This command skips both the `user can register an account` test, as well as the whole of `base/setup.spec.ts`.
152 |
153 |
154 | ```bash
155 | npx playwright test –-grep-invert @setup
156 | ```
157 |
158 | ### Tags and Annotations
159 |
160 | Most tests have been provided with a tag. This allows the user to run specific groups of tests, or skip specific tests. For example, tests that check the functionality of coupon codes are provided with the tag ‘@coupon-code’. To run only these tests, use:
161 |
162 |
163 | ```bash
164 | npx playwright test –-grep @coupon-code
165 | ```
166 |
167 |
168 | You can also run multiple tags with logic operators:
169 |
170 |
171 | ```bash
172 | npx playwright test –-grep ”@coupon-code|@cart”
173 | ```
174 |
175 |
176 | Use `--grep-invert` to run all tests **except** the tests with the specified test. Playwright docs offer more information: [Playwright: Tag Annotations](https://playwright.dev/docs/test-annotations#tag-tests). The following command, for example, skips all tests with the tag ‘@coupon-code’.
177 |
178 |
179 | ```bash
180 | npx playwright test –-grep-invert @coupon-code
181 | ```
182 |
183 |
184 |
185 | ---
186 |
187 | ## ✏️ Customizing the testing suite
188 |
189 | The newly created `tests` folder will become your base of operations. In here, you should use the same folder structure that you see in `base-tests`. For example, if your login page works slightly differently from the demo website version, create a copy of `login.page.ts` and place it `tests/config/poms/frontend/` and make your edits in this file. **You will also have to copy the corresponding** `.spec.ts` **file**. The next time you run the testing suite, it will automatically use these custom files.
190 |
191 |
192 | ### Examples
193 |
194 | Below are some example tests to illustrate how to write and structure your tests.
195 |
196 |
197 | **User registration test:**
198 |
199 |
200 | ```javascript
201 | /**
202 | * @feature User Registration
203 | * @scenario User successfully registers on the website
204 | * @given I am on the registration page
205 | * @when I fill in the registration form with valid data
206 | * @and I submit the form
207 | * @then I should see a confirmation message
208 | */
209 | test('User can register an account', async ({ page }) => {
210 | // Implementation details
211 | });
212 | ```
213 |
214 |
215 | **Checkout process test:**
216 |
217 |
218 | ```javascript
219 | /**
220 | * @feature Product Checkout
221 | * @scenario User completes a purchase
222 | * @given I have a product in my cart
223 | * @when I proceed to checkout
224 | * @and I complete the checkout process
225 | * @then I should receive an order confirmation
226 | */
227 | test('User can complete the checkout process', async ({ page }) => {
228 | // Implementation details
229 | });
230 | ```
231 |
232 |
233 |
234 | ---
235 |
236 | ## How to help
237 |
238 | This package, and therefore the testing suite, is part of our open-source initiative to create an extensive library of end-to-end tests for Magento 2 stores. Do you want to help? Check out the [elgentos Magento 2 Playwright repo on Github](https://github.com/elgentos/magento2-playwright).
--------------------------------------------------------------------------------
/build.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 |
4 | class Build {
5 |
6 | pathToBaseDir = '../../../'; // default: when installed via npm
7 | tempDirTests = 'base-tests';
8 | exampleFileName = '.example';
9 |
10 | constructor() {
11 | const isLocalDev = fs.existsSync(path.resolve(__dirname, '.git'));
12 |
13 | if (isLocalDev) {
14 | this.pathToBaseDir = './'; // we're in the root of the dev repo
15 | }
16 |
17 | this.copyExampleFiles();
18 | this.copyTestsToTempFolder();
19 | this.createNewTestsFolderForCustomTests();
20 | }
21 |
22 | /**
23 | * @feature Copy config example files
24 | * @scenario Copy all `.example` files from the current directory to the root directory.
25 | * @given I have `.example` files in this directory
26 | * @when I run the Build script
27 | * @then The `.example` files should be copied to the root directory without the `.example` extension
28 | * @and Existing destination files should NOT be overwritten, but skipped
29 | */
30 | copyExampleFiles() {
31 | // const exampleFiles = new Set();
32 | const exampleFiles = new Set(fs.readdirSync(__dirname).filter(file => file.includes(this.exampleFileName)));
33 |
34 | for (const file of exampleFiles) {
35 | // destination will be created or overwritten by default.
36 | const sourceFile = './' + file;
37 | const destFile = this.pathToBaseDir + file.replace(this.exampleFileName, '');
38 |
39 | try {
40 | fs.copyFileSync(sourceFile, destFile, fs.constants.COPYFILE_EXCL);
41 | console.log(`${path.basename(destFile)} was copied to destination`);
42 | } catch (err) {
43 | if (err.code === 'EEXIST') {
44 | console.log(`${path.basename(destFile)} already exists, skipping copy.`);
45 | } else {
46 | throw err;
47 | }
48 | }
49 | }
50 | }
51 |
52 | /**
53 | * @feature Copy base test files
54 | * @scenario Prepare test suite by copying `tests/` to the root-level `base-tests/` folder.
55 | * @given There is a `tests/` folder in the package directory
56 | * @when I run the Build script
57 | * @and A `base-tests/` folder already exists in the root
58 | * @then The existing `base-tests/` folder should be removed
59 | * @and A fresh copy of `tests/` should be placed in `../../../base-tests`
60 | */
61 | copyTestsToTempFolder() {
62 |
63 | const sourceDir = path.resolve(__dirname, 'tests');
64 | const targetDir = path.resolve(__dirname, this.pathToBaseDir, this.tempDirTests);
65 |
66 | try {
67 | if (fs.existsSync(targetDir)) {
68 | fs.rmSync(targetDir, {recursive: true, force: true});
69 | }
70 |
71 | fs.cpSync(sourceDir, targetDir, {recursive: true});
72 | if (process.env.CI === 'true') {
73 | fs.rmSync(sourceDir, {recursive: true, force: true});
74 | }
75 | console.log(`Copied tests from ${sourceDir} to ${targetDir}`);
76 | } catch (err) {
77 | console.error('Error copying test directory:', err);
78 | }
79 | }
80 |
81 | /**
82 | * @feature Create tests directory
83 | * @scenario Ensure the `tests/` directory exists at the project root level.
84 | * @given There is no `tests/` directory at the project root
85 | * @when I run the `createNewTestsFolderForCustomTests` function
86 | * @then A new `tests/` directory should be created at `../../../tests`
87 | * @and A log message "Created tests directory: " should be output
88 | * @given The `tests/` directory already exists at the project root
89 | * @when I run the `createNewTestsFolderForCustomTests` function
90 | * @then No new directory should be created
91 | * @and A log message "Tests directory already exists: " should be output
92 | */
93 | createNewTestsFolderForCustomTests() {
94 | const testsDir = path.resolve(__dirname, this.pathToBaseDir, 'tests');
95 | if (!fs.existsSync(testsDir)) {
96 | fs.mkdirSync(testsDir);
97 | console.log(`Created tests directory: ${testsDir}`);
98 | } else {
99 | console.log(`Tests directory already exists: ${testsDir}`);
100 | }
101 | }
102 | }
103 |
104 | new Build();
105 |
--------------------------------------------------------------------------------
/bypass-captcha.config.example.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | /**
4 | * This file is used to set up the CAPTCHA bypass for your tests.
5 | * It will set the global cookie to bypass CAPTCHA for Magento 2.
6 | * See: https://github.com/elgentos/magento2-bypass-captcha-cookie
7 | *
8 | */
9 |
10 | import { FullConfig } from '@playwright/test';
11 | import * as playwright from 'playwright';
12 | import dotenv from 'dotenv';
13 |
14 | dotenv.config();
15 |
16 | async function globalSetup(config: FullConfig) {
17 | const bypassCaptcha = process.env.CAPTCHA_BYPASS === 'true';
18 |
19 | for (const project of config.projects) {
20 | const { storageState, browserName = 'chromium' } = project.use || {};
21 | if (storageState) {
22 | const browserType = playwright[browserName];
23 | const browser = await browserType.launch();
24 | const context = await browser.newContext();
25 |
26 | if (bypassCaptcha) {
27 | // Set the global cookie to bypass CAPTCHA
28 | await context.addCookies([{
29 | name: 'disable_captcha', // this cookie will be read by 'magento2-bypass-captcha-cookie' module.
30 | value: '', // Fill with generated token.
31 | domain: 'hyva-demo.elgentos.io', // Replace with your domain
32 | path: '/',
33 | httpOnly: true,
34 | secure: true,
35 | sameSite: 'Lax',
36 | }]);
37 | console.log(`CAPTCHA bypass enabled for browser: ${project.name}`);
38 | } else {
39 | // Do nothing.
40 | }
41 |
42 | await context.storageState({ path: `./auth-storage/${project.name}-storage-state.json` });
43 | await browser.close();
44 | }
45 | }
46 | }
47 |
48 | export default globalSetup;
49 |
--------------------------------------------------------------------------------
/install.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const fs = require('fs');
4 | const path = require('path');
5 | const readline = require('readline');
6 | const { execSync } = require('child_process');
7 |
8 | const rl = readline.createInterface({
9 | input: process.stdin,
10 | output: process.stdout
11 | });
12 |
13 | const question = (query) => new Promise((resolve) => rl.question(query, resolve));
14 |
15 | async function install() {
16 |
17 | // Get current user
18 | const currentUser = execSync('whoami').toString().trim();
19 |
20 | // Environment variables with defaults
21 | const envVars = {
22 | 'PLAYWRIGHT_BASE_URL': { default: 'https://hyva-demo.elgentos.io/' },
23 | 'PLAYWRIGHT_PRODUCTION_URL': { default: 'https://hyva-demo.elgentos.io/' },
24 | 'PLAYWRIGHT_REVIEW_URL': { default: 'https://hyva-demo.elgentos.io/' },
25 |
26 | 'MAGENTO_ADMIN_SLUG': { default: 'admin' },
27 | 'MAGENTO_ADMIN_USERNAME': { default: currentUser },
28 | 'MAGENTO_ADMIN_PASSWORD': { default: 'Test1234!' },
29 | 'MAGENTO_THEME_LOCALE': { default: 'nl_NL' },
30 |
31 | 'MAGENTO_NEW_ACCOUNT_PASSWORD': { default: 'NewTest1234!' },
32 | 'MAGENTO_EXISTING_ACCOUNT_EMAIL_CHROMIUM': { default: 'user-CHROMIUM@elgentos.nl' },
33 | 'MAGENTO_EXISTING_ACCOUNT_EMAIL_FIREFOX': { default: 'user-FIREFOX@elgentos.nl' },
34 | 'MAGENTO_EXISTING_ACCOUNT_EMAIL_WEBKIT': { default: 'user-WEBKIT@elgentos.nl' },
35 | 'MAGENTO_EXISTING_ACCOUNT_PASSWORD': { default: 'Test1234!' },
36 | 'MAGENTO_EXISTING_ACCOUNT_CHANGED_PASSWORD': { default: 'AanpassenKan@0212' },
37 |
38 | 'MAGENTO_COUPON_CODE_CHROMIUM': { default: 'CHROMIUM321' },
39 | 'MAGENTO_COUPON_CODE_FIREFOX': { default: 'FIREFOX321' },
40 | 'MAGENTO_COUPON_CODE_WEBKIT': { default: 'WEBKIT321' }
41 | };
42 |
43 | // Read and update .env file
44 | const envPath = path.join('.env');
45 | let envContent = '';
46 |
47 | for (const [key, value] of Object.entries(envVars)) {
48 | const userInput = await question(`Enter ${ key } (default: ${ value.default }): `);
49 | envContent += `${ key }=${ userInput || value.default }\n`;
50 | }
51 |
52 | fs.writeFileSync(envPath, envContent);
53 |
54 | console.log('\nInstallation completed successfully!');
55 | console.log('\nFor more information, please visit:');
56 | console.log('https://wiki.elgentos.nl/doc/stappenplan-testing-suite-implementeren-voor-klanten-hCGe4hVQvN');
57 |
58 | rl.close();
59 | }
60 |
61 | install();
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@elgentos/magento2-playwright",
3 | "version": "2.0.0-alpha",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "@elgentos/magento2-playwright",
9 | "version": "2.0.0-alpha",
10 | "hasInstallScript": true,
11 | "license": "ISC",
12 | "dependencies": {
13 | "@faker-js/faker": "^9.8.0",
14 | "@playwright/test": "^1.53.1",
15 | "@types/node": "^22.7.4",
16 | "csv-parse": "^5.5.3",
17 | "dotenv": "^16.4.5"
18 | },
19 | "devDependencies": {}
20 | },
21 | "node_modules/@faker-js/faker": {
22 | "version": "9.8.0",
23 | "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.8.0.tgz",
24 | "integrity": "sha512-U9wpuSrJC93jZBxx/Qq2wPjCuYISBueyVUGK7qqdmj7r/nxaxwW8AQDCLeRO7wZnjj94sh3p246cAYjUKuqgfg==",
25 | "funding": [
26 | {
27 | "type": "opencollective",
28 | "url": "https://opencollective.com/fakerjs"
29 | }
30 | ],
31 | "license": "MIT",
32 | "engines": {
33 | "node": ">=18.0.0",
34 | "npm": ">=9.0.0"
35 | }
36 | },
37 | "node_modules/@playwright/test": {
38 | "version": "1.53.1",
39 | "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.1.tgz",
40 | "integrity": "sha512-Z4c23LHV0muZ8hfv4jw6HngPJkbbtZxTkxPNIg7cJcTc9C28N/p2q7g3JZS2SiKBBHJ3uM1dgDye66bB7LEk5w==",
41 | "license": "Apache-2.0",
42 | "dependencies": {
43 | "playwright": "1.53.1"
44 | },
45 | "bin": {
46 | "playwright": "cli.js"
47 | },
48 | "engines": {
49 | "node": ">=18"
50 | }
51 | },
52 | "node_modules/@types/node": {
53 | "version": "22.15.32",
54 | "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.32.tgz",
55 | "integrity": "sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA==",
56 | "license": "MIT",
57 | "dependencies": {
58 | "undici-types": "~6.21.0"
59 | }
60 | },
61 | "node_modules/csv-parse": {
62 | "version": "5.6.0",
63 | "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.6.0.tgz",
64 | "integrity": "sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==",
65 | "license": "MIT"
66 | },
67 | "node_modules/dotenv": {
68 | "version": "16.5.0",
69 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
70 | "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
71 | "license": "BSD-2-Clause",
72 | "engines": {
73 | "node": ">=12"
74 | },
75 | "funding": {
76 | "url": "https://dotenvx.com"
77 | }
78 | },
79 | "node_modules/fsevents": {
80 | "version": "2.3.2",
81 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
82 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
83 | "hasInstallScript": true,
84 | "license": "MIT",
85 | "optional": true,
86 | "os": [
87 | "darwin"
88 | ],
89 | "engines": {
90 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
91 | }
92 | },
93 | "node_modules/playwright": {
94 | "version": "1.53.1",
95 | "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.1.tgz",
96 | "integrity": "sha512-LJ13YLr/ocweuwxyGf1XNFWIU4M2zUSo149Qbp+A4cpwDjsxRPj7k6H25LBrEHiEwxvRbD8HdwvQmRMSvquhYw==",
97 | "license": "Apache-2.0",
98 | "dependencies": {
99 | "playwright-core": "1.53.1"
100 | },
101 | "bin": {
102 | "playwright": "cli.js"
103 | },
104 | "engines": {
105 | "node": ">=18"
106 | },
107 | "optionalDependencies": {
108 | "fsevents": "2.3.2"
109 | }
110 | },
111 | "node_modules/playwright-core": {
112 | "version": "1.53.1",
113 | "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.1.tgz",
114 | "integrity": "sha512-Z46Oq7tLAyT0lGoFx4DOuB1IA9D1TPj0QkYxpPVUnGDqHHvDpCftu1J2hM2PiWsNMoZh8+LQaarAWcDfPBc6zg==",
115 | "license": "Apache-2.0",
116 | "bin": {
117 | "playwright-core": "cli.js"
118 | },
119 | "engines": {
120 | "node": ">=18"
121 | }
122 | },
123 | "node_modules/undici-types": {
124 | "version": "6.21.0",
125 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
126 | "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
127 | "license": "MIT"
128 | }
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@elgentos/magento2-playwright",
3 | "version": "2.0.0-alpha",
4 | "author": "elgentos",
5 | "license": "ISC",
6 | "description": "A Playwright End-To-End (E2E) testing suite for Magento 2 with Hyva that helps you find (potential) issues on your webshop.",
7 | "scripts": {
8 | "postinstall": "node build.js && npx playwright install",
9 | "translate": "node translate-json.js nl_NL",
10 | "preinstall": "node install.js"
11 | },
12 | "dependencies": {
13 | "@faker-js/faker": "^9.8.0",
14 | "@playwright/test": "^1.53.1",
15 | "@types/node": "^22.7.4",
16 | "csv-parse": "^5.5.3",
17 | "dotenv": "^16.4.5"
18 | },
19 | "devDependencies": {}
20 | }
--------------------------------------------------------------------------------
/playwright.config.example.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { defineConfig, devices } from '@playwright/test';
4 | import dotenv from 'dotenv';
5 | import path from 'path';
6 | import fs from "node:fs";
7 |
8 | dotenv.config({ path: path.resolve(__dirname, '.env') });
9 |
10 | function getTestFiles(baseDir: string, customDir?: string): string[] {
11 | const baseFiles = new Set(fs.readdirSync(baseDir).filter(file => file.endsWith('.spec.ts')));
12 |
13 | if (!customDir || !fs.existsSync(customDir)) {
14 | return Array.from(baseFiles);
15 | }
16 |
17 | const customFiles = fs.readdirSync(customDir).filter(file => file.endsWith('.spec.ts'));
18 |
19 | if(customFiles.length === 0) {
20 | return Array.from(baseFiles);
21 | }
22 |
23 | const testFiles = new Set();
24 |
25 | // Get base files that have an override in custom
26 | for (const file of baseFiles) {
27 | const baseFilePath = path.join(baseDir, file);
28 | const customFilePath = path.join(customDir, file);
29 |
30 | testFiles.add(fs.existsSync(customFilePath) ? customFilePath : baseFilePath);
31 | }
32 |
33 | // Add custom tests that aren't in base
34 | for (const file of customFiles) {
35 | if (!baseFiles.has(file)) {
36 | testFiles.add(path.join(customDir, file));
37 | }
38 | }
39 |
40 | return Array.from(testFiles);
41 | }
42 |
43 | const testFiles = getTestFiles(
44 | path.join(__dirname, 'base-tests'),
45 | path.join(__dirname, 'tests'),
46 | );
47 |
48 | /**
49 | * See https://playwright.dev/docs/test-configuration.
50 | */
51 | export default defineConfig({
52 | /* Run tests in files in parallel */
53 | fullyParallel: true,
54 | /* Fail the build on CI if you accidentally left test.only in the source code. */
55 | forbidOnly: !!process.env.CI,
56 | /* Retry on CI only */
57 | retries: process.env.CI ? 2 : 0,
58 | /* Opt out of parallel tests on CI. */
59 | workers: process.env.CI ? 1 : undefined,
60 | /* Increase default timeout */
61 | timeout: 120_000,
62 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */
63 | reporter: 'html',
64 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
65 | use: {
66 | baseURL: process.env.PLAYWRIGHT_BASE_URL || 'https://hyva-demo.elgentos.io/',
67 |
68 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
69 | trace: 'on-first-retry',
70 |
71 | /* Ignore https errors if they apply (should only happen on local) */
72 | ignoreHTTPSErrors: true,
73 | },
74 |
75 | /*
76 | * Setup for global cookie to bypass CAPTCHA, remove '.example' when used.
77 | * If this is disabled remove storageState from all project objects.
78 | */
79 | globalSetup: require.resolve('./bypass-captcha.config.ts'),
80 |
81 | /* Configure projects for major browsers */
82 | projects: [
83 | // Import our auth.setup.ts file
84 | //{ name: 'setup', testMatch: /.*\.setup\.ts/ },
85 |
86 | {
87 | name: 'chromium',
88 | testMatch: testFiles,
89 | use: {
90 | ...devices['Desktop Chrome'],
91 | storageState: './auth-storage/chromium-storage-state.json',
92 | },
93 | },
94 |
95 | {
96 | name: 'firefox',
97 | testMatch: testFiles,
98 | use: {
99 | ...devices['Desktop Firefox'],
100 | storageState: './auth-storage/firefox-storage-state.json',
101 | },
102 | },
103 |
104 | {
105 | name: 'webkit',
106 | testMatch: testFiles,
107 | use: {
108 | ...devices['Desktop Safari'],
109 | storageState: './auth-storage/webkit-storage-state.json',
110 | },
111 | },
112 |
113 | /* Test against mobile viewports. */
114 | // {
115 | // name: 'Mobile Chrome',
116 | // use: { ...devices['Pixel 5'] },
117 | // },
118 | // {
119 | // name: 'Mobile Safari',
120 | // use: { ...devices['iPhone 12'] },
121 | // },
122 |
123 | /* Test against branded browsers. */
124 | // {
125 | // name: 'Microsoft Edge',
126 | // use: { ...devices['Desktop Edge'], channel: 'msedge' },
127 | // },
128 | // {
129 | // name: 'Google Chrome',
130 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
131 | // },
132 | ],
133 |
134 | /* Run your local dev server before starting the tests */
135 | // webServer: {
136 | // command: 'npm run start',
137 | // url: 'http://127.0.0.1:3000',
138 | // reuseExistingServer: !process.env.CI,
139 | // },
140 | });
--------------------------------------------------------------------------------
/tests.config.example.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import fs from 'fs';
4 | import path from 'path';
5 |
6 | const basePath = path.resolve('./base-tests/config');
7 | const overridePath = path.resolve('./tests/config');
8 |
9 | function loadConfig(filename: string) {
10 | const overrideFile = path.join(overridePath, filename);
11 | const baseFile = path.join(basePath, filename);
12 |
13 | if (fs.existsSync(overrideFile)) {
14 | return require(overrideFile);
15 | } else {
16 | return require(baseFile);
17 | }
18 | }
19 |
20 | const UIReference = loadConfig('element-identifiers.json');
21 | const outcomeMarker = loadConfig('outcome-markers.json');
22 | const slugs = loadConfig('slugs.json');
23 | const inputValues = loadConfig('input-values.json');
24 | const toggles = loadConfig('test-toggles.json');
25 |
26 | export { UIReference, outcomeMarker, slugs, inputValues, toggles };
--------------------------------------------------------------------------------
/tests/account.spec.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { test, expect } from '@playwright/test';
4 | import { faker } from '@faker-js/faker';
5 | import { UIReference, outcomeMarker, slugs} from 'config';
6 | import { requireEnv } from './utils/env.utils';
7 |
8 | import AccountPage from './poms/frontend/account.page';
9 | import LoginPage from './poms/frontend/login.page';
10 | import MainMenuPage from './poms/frontend/mainmenu.page';
11 | import NewsletterSubscriptionPage from './poms/frontend/newsletter.page';
12 | import RegisterPage from './poms/frontend/register.page';
13 |
14 | // Before each test, log in
15 | test.beforeEach(async ({ page, browserName }) => {
16 | const browserEngine = browserName?.toUpperCase() || "UNKNOWN";
17 | const emailInputValue = requireEnv(`MAGENTO_EXISTING_ACCOUNT_EMAIL_${browserEngine}`);
18 | const passwordInputValue = requireEnv('MAGENTO_EXISTING_ACCOUNT_PASSWORD');
19 |
20 | const loginPage = new LoginPage(page);
21 | await loginPage.login(emailInputValue, passwordInputValue);
22 | });
23 |
24 | test.describe('Account information actions', {annotation: {type: 'Account Dashboard', description: 'Test for Account Information'},}, () => {
25 |
26 | test.beforeEach(async ({page}) => {
27 | await page.goto(slugs.account.accountOverviewSlug);
28 | await page.waitForLoadState();
29 | });
30 |
31 | /**
32 | * @feature Magento 2 Change Password
33 | * @scenario User changes their password
34 | * @given I am logged in
35 | * @and I am on the Account Dashboard page
36 | * @when I navigate to the Account Information page
37 | * @and I check the 'change password' option
38 | * @when I fill in the new credentials
39 | * @and I click Save
40 | * @then I should see a notification that my password has been updated
41 | * @and I should be able to login with my new credentials.
42 | */
43 | test('Change_password',{ tag: ['@account-credentials', '@hot'] }, async ({page, browserName}, testInfo) => {
44 |
45 | // Create instances and set variables
46 | const mainMenu = new MainMenuPage(page);
47 | const registerPage = new RegisterPage(page);
48 | const accountPage = new AccountPage(page);
49 | const loginPage = new LoginPage(page);
50 |
51 | const browserEngine = browserName?.toUpperCase() || "UNKNOWN";
52 | let randomNumberforEmail = Math.floor(Math.random() * 101);
53 | let emailPasswordUpdatevalue = `passwordupdate-${randomNumberforEmail}-${browserEngine}@example.com`;
54 | let passwordInputValue = requireEnv('MAGENTO_EXISTING_ACCOUNT_PASSWORD');
55 | let changedPasswordValue = requireEnv('MAGENTO_EXISTING_ACCOUNT_CHANGED_PASSWORD');
56 |
57 | // Log out of current account
58 | if(await page.getByRole('link', { name: UIReference.mainMenu.myAccountLogoutItem }).isVisible()){
59 | await mainMenu.logout();
60 | }
61 |
62 | // Create account
63 |
64 | await registerPage.createNewAccount(faker.person.firstName(), faker.person.lastName(), emailPasswordUpdatevalue, passwordInputValue);
65 |
66 | // Update password
67 | await page.goto(slugs.account.changePasswordSlug);
68 | await page.waitForLoadState();
69 | await accountPage.updatePassword(passwordInputValue, changedPasswordValue);
70 |
71 | // If login with changePasswordValue is possible, then password change was succesful.
72 | await loginPage.login(emailPasswordUpdatevalue, changedPasswordValue);
73 |
74 | // Logout again, login with original account
75 | await mainMenu.logout();
76 | const emailInputValue = requireEnv(`MAGENTO_EXISTING_ACCOUNT_EMAIL_${browserEngine}`);
77 | await loginPage.login(emailInputValue, passwordInputValue);
78 | });
79 |
80 | /**
81 | * @feature Magento 2 Update E-mail Address
82 | * @scenario User updates their e-mail address
83 | * @given I am logged in
84 | * @and I am on the Account Dashboard page
85 | * @when I navigate to the Account Information page
86 | * @and I fill in a new e-mail address and my current password
87 | * @and I click Save
88 | * @then I should see a notification that my account has been updated
89 | * @and I should be able to login with my new e-mail address.
90 | */
91 | test('Update_my_e-mail_address',{ tag: ['@account-credentials', '@hot'] }, async ({page, browserName}) => {
92 | const mainMenu = new MainMenuPage(page);
93 | const registerPage = new RegisterPage(page);
94 | const accountPage = new AccountPage(page);
95 | const loginPage = new LoginPage(page);
96 |
97 | const browserEngine = browserName?.toUpperCase() || 'UNKNOWN';
98 | let randomNumberforEmail = Math.floor(Math.random() * 101);
99 | let originalEmail = `emailupdate-${randomNumberforEmail}-${browserEngine}@example.com`;
100 | let updatedEmail = `updated-${randomNumberforEmail}-${browserEngine}@example.com`;
101 | let passwordInputValue = process.env.MAGENTO_EXISTING_ACCOUNT_PASSWORD;
102 |
103 | if(await page.getByRole('link', { name: UIReference.mainMenu.myAccountLogoutItem }).isVisible()) {
104 | await mainMenu.logout();
105 | }
106 |
107 | if(!passwordInputValue) {
108 | throw new Error('MAGENTO_EXISTING_ACCOUNT_PASSWORD in your .env file is not defined or could not be read.');
109 | }
110 |
111 | await registerPage.createNewAccount(faker.person.firstName(), faker.person.lastName(), originalEmail, passwordInputValue);
112 |
113 | await page.goto(slugs.account.accountEditSlug);
114 | await page.waitForLoadState();
115 | await accountPage.updateEmail(passwordInputValue, updatedEmail);
116 |
117 | await mainMenu.logout();
118 | await loginPage.login(updatedEmail, passwordInputValue);
119 |
120 | await mainMenu.logout();
121 | let emailInputValue = process.env[`MAGENTO_EXISTING_ACCOUNT_EMAIL_${browserEngine}`];
122 | if(!emailInputValue) {
123 | throw new Error(`MAGENTO_EXISTING_ACCOUNT_EMAIL_${browserEngine} and/or MAGENTO_EXISTING_ACCOUNT_PASSWORD have not defined in the .env file, or the account hasn't been created yet.`);
124 | }
125 | await loginPage.login(emailInputValue, passwordInputValue);
126 | });
127 | });
128 |
129 | test.describe.serial('Account address book actions', { annotation: {type: 'Account Dashboard', description: 'Tests for the Address Book'},}, () => {
130 |
131 | /**
132 | * @feature Magento 2 Add First Address to Account
133 | * @scenario User adds a first address to their account
134 | * @given I am logged in
135 | * @and I am on the account dashboard page
136 | * @when I go to the page where I can add my address
137 | * @and I haven't added an address yet
138 | * @when I fill in the required information
139 | * @and I click the save button
140 | * @then I should see a notification my address has been updated.
141 | * @and The new address should be selected as default and shipping address
142 | */
143 |
144 | test('Add_first_address',{ tag: ['@account-credentials', '@hot'] }, async ({page}, testInfo) => {
145 | const accountPage = new AccountPage(page);
146 | let addNewAddressTitle = page.getByRole('heading', {level: 1, name: UIReference.newAddress.addNewAddressTitle});
147 |
148 | if(await addNewAddressTitle.isHidden()) {
149 | await accountPage.deleteAllAddresses();
150 | testInfo.annotations.push({ type: 'Notification: deleted addresses', description: `All addresses are deleted to recreate the first address flow.` });
151 | await page.goto(slugs.account.addressNewSlug);
152 | }
153 |
154 | await accountPage.addNewAddress();
155 | });
156 |
157 | /**
158 | * @given I am logged in
159 | * @and I am on the account dashboard page
160 | * @when I go to the page where I can add another address
161 | * @when I fill in the required information
162 | * @and I click the save button
163 | * @then I should see a notification my address has been updated.
164 | * @and The new address should be listed
165 | */
166 | test('Add_another_address',{ tag: ['@account-credentials', '@hot'] }, async ({page}) => {
167 | await page.goto(slugs.account.addressNewSlug);
168 | const accountPage = new AccountPage(page);
169 |
170 | await accountPage.addNewAddress();
171 | });
172 |
173 | /**
174 | * @feature Magento 2 Update Address in Account
175 | * @scenario User updates an existing address to their account
176 | * @given I am logged in
177 | * @and I am on the account dashboard page
178 | * @when I go to the page where I can see my address(es)
179 | * @when I click on the button to edit the address
180 | * @and I fill in the required information correctly
181 | * @then I click the save button
182 | * @then I should see a notification my address has been updated.
183 | * @and The updated address should be visible in the addres book page.
184 | */
185 | test('Edit_existing_address',{ tag: ['@account-credentials', '@hot'] }, async ({page}) => {
186 | const accountPage = new AccountPage(page);
187 | await page.goto(slugs.account.addressNewSlug);
188 | let editAddressButton = page.getByRole('link', {name: UIReference.accountDashboard.editAddressIconButton}).first();
189 |
190 | if(await editAddressButton.isHidden()){
191 | // The edit address button was not found, add another address first.
192 | await accountPage.addNewAddress();
193 | }
194 |
195 | await page.goto(slugs.account.addressBookSlug);
196 | await accountPage.editExistingAddress();
197 | });
198 |
199 | /**
200 | * @feature Magento 2 Delete Address from account
201 | * @scenario User removes an address from their account
202 | * @given I am logged in
203 | * @and I am on the account dashboard page
204 | * @when I go to the page where I can see my address(es)
205 | * @when I click the trash button for the address I want to delete
206 | * @and I click the confirmation button
207 | * @then I should see a notification my address has been deleted.
208 | * @and The address should be removed from the overview.
209 | */
210 | test('Delete_an_address',{ tag: ['@account-credentials', '@hot'] }, async ({page}, testInfo) => {
211 | const accountPage = new AccountPage(page);
212 |
213 | let deleteAddressButton = page.getByRole('link', {name: UIReference.accountDashboard.addressDeleteIconButton}).first();
214 |
215 | if(await deleteAddressButton.isHidden()) {
216 | await page.goto(slugs.account.addressNewSlug);
217 | await accountPage.addNewAddress();
218 | }
219 | await accountPage.deleteFirstAddressFromAddressBook();
220 | });
221 | });
222 |
223 | test.describe('Newsletter actions', { annotation: {type: 'Account Dashboard', description: 'Newsletter tests'},}, () => {
224 |
225 | /**
226 | * @feature Magento 2 newsletter subscriptions
227 | * @scenario User (un)subscribes from a newsletter
228 | * @given I am logged in
229 | * @and I am on the account dashboard page
230 | * @when I click on the newsletter link in the sidebar
231 | * @then I should navigate to the newsletter subscription page
232 | * @when I (un)check the subscription button
233 | * @then I should see a message confirming my action
234 | * @and My subscription option should be updated.
235 | */
236 | test('Update_newsletter_subscription',{ tag: ['@newsletter-actions', '@cold'] }, async ({page, browserName}) => {
237 | test.skip(browserName === 'webkit', '.click() does not work, still searching for a workaround');
238 | const newsletterPage = new NewsletterSubscriptionPage(page);
239 | let newsletterLink = page.getByRole('link', { name: UIReference.accountDashboard.links.newsletterLink });
240 | const newsletterCheckElement = page.getByLabel(UIReference.newsletterSubscriptions.generalSubscriptionCheckLabel);
241 |
242 | await newsletterLink.click();
243 | await expect(page.getByText(outcomeMarker.account.newsletterSubscriptionTitle, { exact: true })).toBeVisible();
244 |
245 | let updateSubscription = await newsletterPage.updateNewsletterSubscription();
246 |
247 | await newsletterLink.click();
248 |
249 | if(updateSubscription){
250 | await expect(newsletterCheckElement).toBeChecked();
251 | } else {
252 | await expect(newsletterCheckElement).not.toBeChecked();
253 | }
254 | });
255 | });
256 |
--------------------------------------------------------------------------------
/tests/auth.setup.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { test as setup, expect } from '@playwright/test';
4 | import path from 'path';
5 | import { UIReference, slugs } from 'config';
6 | import { requireEnv } from './utils/env.utils';
7 |
8 | const authFile = path.join(__dirname, '../playwright/.auth/user.json');
9 |
10 | setup('authenticate', async ({ page, browserName }) => {
11 | const browserEngine = browserName?.toUpperCase() || "UNKNOWN";
12 | const emailInputValue = requireEnv(`MAGENTO_EXISTING_ACCOUNT_EMAIL_${browserEngine}`);
13 | const passwordInputValue = requireEnv('MAGENTO_EXISTING_ACCOUNT_PASSWORD');
14 |
15 | // Perform authentication steps. Replace these actions with your own.
16 | await page.goto(slugs.account.loginSlug);
17 | await page.getByLabel(UIReference.credentials.emailFieldLabel, {exact: true}).fill(emailInputValue);
18 | await page.getByLabel(UIReference.credentials.passwordFieldLabel, {exact: true}).fill(passwordInputValue);
19 | await page.getByRole('button', { name: UIReference.credentials.loginButtonLabel }).click();
20 | // Wait until the page receives the cookies.
21 | //
22 | // Sometimes login flow sets cookies in the process of several redirects.
23 | // Wait for the final URL to ensure that the cookies are actually set.
24 | // await page.waitForURL('');
25 | // Alternatively, you can wait until the page reaches a state where all cookies are set.
26 | await expect(page.getByRole('link', { name: UIReference.mainMenu.myAccountLogoutItem })).toBeVisible();
27 |
28 | // End of authentication steps.
29 |
30 | await page.context().storageState({ path: authFile });
31 | });
32 |
--------------------------------------------------------------------------------
/tests/cart.spec.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { test, expect } from '@playwright/test';
4 | import { UIReference, slugs, outcomeMarker } from 'config';
5 |
6 | import CartPage from './poms/frontend/cart.page';
7 | import LoginPage from './poms/frontend/login.page';
8 | import ProductPage from './poms/frontend/product.page';
9 | import { requireEnv } from './utils/env.utils';
10 | import NotificationValidator from './utils/notification.validator';
11 |
12 | test.describe('Cart functionalities (guest)', () => {
13 | /**
14 | * @feature BeforeEach runs before each test in this group.
15 | * @scenario Add a product to the cart and confirm it's there.
16 | * @given I am on any page
17 | * @when I navigate to a (simple) product page
18 | * @and I add it to my cart
19 | * @then I should see a notification
20 | * @when I click the cart in the main menu
21 | * @then the minicart should become visible
22 | * @and I should see the product in the minicart
23 | */
24 | test.beforeEach(async ({ page }, testInfo) => {
25 | const productPage = new ProductPage(page);
26 | await productPage.addSimpleProductToCart(UIReference.productPage.simpleProductTitle, slugs.productpage.simpleProductSlug);
27 |
28 | const productAddedNotification = `${outcomeMarker.productPage.simpleProductAddedNotification} ${UIReference.productPage.simpleProductTitle}`;
29 | const notificationValidator = new NotificationValidator(page, testInfo);
30 | await notificationValidator.validate(productAddedNotification);
31 |
32 | // await mainMenu.openMiniCart();
33 | // await expect(page.getByText(outcomeMarker.miniCart.simpleProductInCartTitle)).toBeVisible();
34 | await page.goto(slugs.cart.cartSlug);
35 | });
36 |
37 | /**
38 | * @feature Product can be added to cart
39 | * @scenario User adds a product to their cart
40 | * @given I have added a product to my cart
41 | * @and I am on the cart page
42 | * @then I should see the name of the product in my cart
43 | */
44 | test('Add_product_to_cart',{ tag: ['@cart', '@cold'],}, async ({page}) => {
45 | await expect(page.getByRole('strong').getByRole('link', {name: UIReference.productPage.simpleProductTitle}), `Product is visible in cart`).toBeVisible();
46 | });
47 |
48 | /**
49 | * @feature Product permanence after login
50 | * @scenario A product added to the cart should still be there after user has logged in
51 | * @given I have a product in my cart
52 | * @when I log in
53 | * @then I should still have that product in my cart
54 | */
55 | test('Product_remains_in_cart_after_login',{ tag: ['@cart', '@account', '@hot']}, async ({page, browserName}) => {
56 | await test.step('Add another product to cart', async () =>{
57 | const productpage = new ProductPage(page);
58 | await page.goto(slugs.productpage.secondSimpleProductSlug);
59 | await productpage.addSimpleProductToCart(UIReference.productPage.secondSimpleProducTitle, slugs.productpage.secondSimpleProductSlug);
60 | });
61 |
62 | await test.step('Log in with account', async () =>{
63 | const browserEngine = browserName?.toUpperCase() || "UNKNOWN";
64 | const loginPage = new LoginPage(page);
65 | const emailInputValue = requireEnv(`MAGENTO_EXISTING_ACCOUNT_EMAIL_${browserEngine}`);
66 | const passwordInputValue = requireEnv('MAGENTO_EXISTING_ACCOUNT_PASSWORD');
67 |
68 | await loginPage.login(emailInputValue, passwordInputValue);
69 | });
70 |
71 | await page.goto(slugs.cart.cartSlug);
72 | await expect(page.getByRole('strong').getByRole('link', { name: UIReference.productPage.simpleProductTitle }),`${UIReference.productPage.simpleProductTitle} should still be in cart`).toBeVisible();
73 | await expect(page.getByRole('strong').getByRole('link', { name: UIReference.productPage.secondSimpleProducTitle }),`${UIReference.productPage.secondSimpleProducTitle} should still be in cart`).toBeVisible();
74 | });
75 |
76 | /**
77 | * @feature Remove product from cart
78 | * @scenario User has added a product and wants to remove it from the cart page
79 | * @given I have added a product to my cart
80 | * @and I am on the cart page
81 | * @when I click the delete button
82 | * @then I should see a notification that the product has been removed from my cart
83 | * @and I should no longer see the product in my cart
84 | */
85 | test('Remove_product_from_cart',{ tag: ['@cart','@cold'],}, async ({page}) => {
86 | const cart = new CartPage(page);
87 | await cart.removeProduct(UIReference.productPage.simpleProductTitle);
88 | });
89 |
90 | /**
91 | * @feature Change quantity of products in cart
92 | * @scenario User has added a product and changes the quantity
93 | * @given I have a product in my cart
94 | * @and I am on the cart page
95 | * @when I change the quantity of the product
96 | * @and I click the update button
97 | * @then the quantity field should have the new amount
98 | * @and the subtotal/grand total should update
99 | */
100 | test('Change_product_quantity_in_cart',{ tag: ['@cart', '@cold'],}, async ({page}) => {
101 | const cart = new CartPage(page);
102 | await cart.changeProductQuantity('2');
103 | });
104 |
105 | /**
106 | * @feature Discount Code
107 | * @scenario User adds a discount code to their cart
108 | * @given I have a product in my cart
109 | * @and I am on my cart page
110 | * @when I click on the 'add discount code' button
111 | * @then I fill in a code
112 | * @and I click on 'apply code'
113 | * @then I should see a confirmation that my code has been added
114 | * @and the code should be visible in the cart
115 | * @and a discount should be applied to the product
116 | */
117 | test('Add_coupon_code_in_cart',{ tag: ['@cart', '@coupon-code', '@cold']}, async ({page, browserName}) => {
118 | const browserEngine = browserName?.toUpperCase() || "UNKNOWN";
119 | const cart = new CartPage(page);
120 | const discountCode = requireEnv(`MAGENTO_COUPON_CODE_${browserEngine}`);
121 |
122 | await cart.applyDiscountCode(discountCode);
123 | });
124 |
125 | /**
126 | * @feature Remove discount code from cart
127 | * @scenario User has added a discount code, then removes it
128 | * @given I have a product in my cart
129 | * @and I am on my cart page
130 | * @when I add a discount code
131 | * @then I should see a notification
132 | * @and the code should be visible in the cart
133 | * @and a discount should be applied to a product
134 | * @when I click the 'cancel coupon' button
135 | * @then I should see a notification the discount has been removed
136 | * @and the discount should no longer be visible.
137 | */
138 | test('Remove_coupon_code_from_cart',{ tag: ['@cart', '@coupon-code', '@cold'] }, async ({page, browserName}) => {
139 | const browserEngine = browserName?.toUpperCase() || "UNKNOWN";
140 | const cart = new CartPage(page);
141 | const discountCode = requireEnv(`MAGENTO_COUPON_CODE_${browserEngine}`);
142 |
143 | await cart.applyDiscountCode(discountCode);
144 | await cart.removeDiscountCode();
145 | });
146 |
147 | /**
148 | * @feature Incorrect discount code check
149 | * @scenario The user provides an incorrect discount code, the system should reflect that
150 | * @given I have a product in my cart
151 | * @and I am on the cart page
152 | * @when I enter a wrong discount code
153 | * @then I should get a notification that the code did not work.
154 | */
155 |
156 | test('Invalid_coupon_code_is_rejected',{ tag: ['@cart', '@coupon-code', '@cold'] }, async ({page}) => {
157 | const cart = new CartPage(page);
158 | await cart.enterWrongCouponCode("Incorrect Coupon Code");
159 | });
160 | })
161 |
162 | test.describe('Price checking tests', () => {
163 |
164 | // Test: Configurable Product Input check from PDP to checkout
165 | // test.step: add configurable product to cart, return priceOnPDP and productAmount as variables
166 | // test.step: call function retrieveCheckoutPrices() to go to checkout, retrieve values
167 | // test.step: call function compareRetrievedPrices() to compare price on PDP to price in checkout
168 |
169 | /**
170 | * @feature Simple Product price/amount check from PDP to Checkout
171 | * @given none
172 | * @when I go to a (simple) product page
173 | * @and I add one or more to my cart
174 | * @when I go to the checkout
175 | * @then the amount of the product should be the same
176 | * @and the price in the checkout should equal the price of the product * the amount of the product
177 | */
178 | test('Simple_product_cart_data_consistent_from_PDP_to_checkout',{ tag: ['@cart-price-check', '@cold']}, async ({page}) => {
179 | var productPagePrice: string;
180 | var productPageAmount: string;
181 | var checkoutProductDetails: string[];
182 |
183 | const cart = new CartPage(page);
184 |
185 | await test.step('Step: Add simple product to cart', async () =>{
186 | const productPage = new ProductPage(page);
187 | await page.goto(slugs.productpage.simpleProductSlug);
188 | // set quantity to 2 so we can see that the math works
189 | await page.getByLabel(UIReference.productPage.quantityFieldLabel).fill('2');
190 |
191 | productPagePrice = await page.locator(UIReference.productPage.simpleProductPrice).innerText();
192 | productPageAmount = await page.getByLabel(UIReference.productPage.quantityFieldLabel).inputValue();
193 | await productPage.addSimpleProductToCart(UIReference.productPage.simpleProductTitle, slugs.productpage.simpleProductSlug, '2');
194 |
195 | });
196 |
197 | await test.step('Step: go to checkout, get values', async () =>{
198 | await page.goto(slugs.checkout.checkoutSlug);
199 | await page.waitForLoadState();
200 |
201 | // returns productPriceInCheckout and productQuantityInCheckout
202 | checkoutProductDetails = await cart.getCheckoutValues(UIReference.productPage.simpleProductTitle, productPagePrice, productPageAmount);
203 | });
204 |
205 | await test.step('Step: Calculate and check expectations', async () =>{
206 | await cart.calculateProductPricesAndCompare(productPagePrice, productPageAmount, checkoutProductDetails[0], checkoutProductDetails[1]);
207 | });
208 |
209 | });
210 |
211 | /**
212 | * @feature Configurable Product price/amount check from PDP to Checkout
213 | * @given none
214 | * @when I go to a (configurable) product page
215 | * @and I add one or more to my cart
216 | * @when I go to the checkout
217 | * @then the amount of the product should be the same
218 | * @and the price in the checkout should equal the price of the product * the amount of the product
219 | */
220 | test('Configurable_product_cart_data_consistent_from_PDP_to_checkout',{ tag: ['@cart-price-check', '@cold']}, async ({page}) => {
221 | var productPagePrice: string;
222 | var productPageAmount: string;
223 | var checkoutProductDetails: string[];
224 |
225 | const cart = new CartPage(page);
226 |
227 | await test.step('Step: Add configurable product to cart', async () =>{
228 | const productPage = new ProductPage(page);
229 | // Navigate to the configurable product page so we can retrieve price and amount before adding it to cart
230 | await page.goto(slugs.productpage.configurableProductSlug);
231 | // set quantity to 2 so we can see that the math works
232 | await page.getByLabel('Quantity').fill('2');
233 |
234 | productPagePrice = await page.locator(UIReference.productPage.simpleProductPrice).innerText();
235 | productPageAmount = await page.getByLabel(UIReference.productPage.quantityFieldLabel).inputValue();
236 | await productPage.addConfigurableProductToCart(UIReference.productPage.configurableProductTitle, slugs.productpage.configurableProductSlug, '2');
237 |
238 | });
239 |
240 | await test.step('Step: go to checkout, get values', async () =>{
241 | await page.goto(slugs.checkout.checkoutSlug);
242 | await page.waitForLoadState();
243 |
244 | // returns productPriceInCheckout and productQuantityInCheckout
245 | checkoutProductDetails = await cart.getCheckoutValues(UIReference.productPage.configurableProductTitle, productPagePrice, productPageAmount);
246 | });
247 |
248 | await test.step('Step: Calculate and check expectations', async () =>{
249 | await cart.calculateProductPricesAndCompare(productPagePrice, productPageAmount, checkoutProductDetails[0], checkoutProductDetails[1]);
250 | });
251 |
252 | });
253 | });
254 |
--------------------------------------------------------------------------------
/tests/category.spec.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { test } from '@playwright/test';
4 |
5 | import CategoryPage from './poms/frontend/category.page';
6 |
7 | test('Filter_category_on_size',{ tag: ['@category', '@cold']}, async ({page, browserName}) => {
8 | const categoryPage = new CategoryPage(page);
9 | await categoryPage.goToCategoryPage();
10 |
11 | await categoryPage.filterOnSize(browserName);
12 | });
13 |
14 | test('Sort_category_by_price',{ tag: ['@category', '@cold']}, async ({page}) => {
15 | const categoryPage = new CategoryPage(page);
16 | await categoryPage.goToCategoryPage();
17 |
18 | await categoryPage.sortProducts('price');
19 | });
20 |
21 | test('Change_amount_of_products_shown',{ tag: ['@category', '@cold'],}, async ({page}) => {
22 | const categoryPage = new CategoryPage(page);
23 | await categoryPage.goToCategoryPage();
24 |
25 | await categoryPage.showMoreProducts();
26 | // insert your code here
27 | });
28 |
29 | test('Switch_from_grid_to_list_view',{ tag: ['@category', '@cold'],}, async ({page}) => {
30 | const categoryPage = new CategoryPage(page);
31 | await categoryPage.goToCategoryPage();
32 | await categoryPage.switchView();
33 | });
--------------------------------------------------------------------------------
/tests/checkout.spec.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { test, expect } from '@playwright/test';
4 | import { UIReference, slugs } from 'config';
5 |
6 | import LoginPage from './poms/frontend/login.page';
7 | import ProductPage from './poms/frontend/product.page';
8 | import AccountPage from './poms/frontend/account.page';
9 | import { requireEnv } from './utils/env.utils';
10 | import CheckoutPage from './poms/frontend/checkout.page';
11 |
12 |
13 | /**
14 | * @feature BeforeEach runs before each test in this group.
15 | * @scenario Add product to the cart, confirm it's there, then move to checkout.
16 | * @given I am on any page
17 | * @when I navigate to a (simple) product page
18 | * @and I add it to my cart
19 | * @then I should see a notification
20 | * @when I navigate to the checkout
21 | * @then the checkout page should be shown
22 | * @and I should see the product in the minicart
23 | */
24 | test.beforeEach(async ({ page }) => {
25 | const productPage = new ProductPage(page);
26 |
27 | await page.goto(slugs.productpage.simpleProductSlug);
28 | await productPage.addSimpleProductToCart(UIReference.productPage.simpleProductTitle, slugs.productpage.simpleProductSlug);
29 | await page.goto(slugs.checkout.checkoutSlug);
30 | });
31 |
32 |
33 | test.describe('Checkout (login required)', () => {
34 | // Before each test, log in
35 | test.beforeEach(async ({ page, browserName }) => {
36 | const browserEngine = browserName?.toUpperCase() || "UNKNOWN";
37 | const emailInputValue = requireEnv(`MAGENTO_EXISTING_ACCOUNT_EMAIL_${browserEngine}`);
38 | const passwordInputValue = requireEnv('MAGENTO_EXISTING_ACCOUNT_PASSWORD');
39 |
40 | const loginPage = new LoginPage(page);
41 | await loginPage.login(emailInputValue, passwordInputValue);
42 | await page.goto(slugs.checkout.checkoutSlug);
43 | });
44 |
45 | /**
46 | * @feature Automatically fill in certain data in checkout (if user is logged in)
47 | * @scenario When the user navigates to the checkout (with a product), their name and address should be filled in.
48 | * @given I am logged in
49 | * @and I have a product in my cart
50 | * @and I have navigated to the checkout page
51 | * @then My name and address should already be filled in
52 | */
53 | test('Address_is_pre_filled_in_checkout',{ tag: ['@checkout', '@hot']}, async ({page}) => {
54 | let signInLink = page.getByRole('link', { name: UIReference.credentials.loginButtonLabel });
55 | let addressField = page.getByLabel(UIReference.newAddress.streetAddressLabel);
56 | let addressAlreadyAdded = false;
57 |
58 | if(await signInLink.isVisible()) {
59 | throw new Error(`Sign in link found, user is not logged in. Please check the test setup.`);
60 | }
61 |
62 | // name field should NOT be on the page
63 | await expect(page.getByLabel(UIReference.personalInformation.firstNameLabel)).toBeHidden();
64 |
65 | if(await addressField.isVisible()) {
66 | if(!addressAlreadyAdded){
67 | // Address field is visible and addressalreadyAdded is not true, so we need to add an address to the account.
68 | const accountPage = new AccountPage(page);
69 | await accountPage.addNewAddress();
70 | } else {
71 | throw new Error(`Address field is visible even though an address has been added to the account.`);
72 | }
73 | }
74 |
75 | // expect to see radio button to select existing address
76 | let shippingRadioButton = page.locator(UIReference.checkout.shippingAddressRadioLocator).first();
77 | await expect(shippingRadioButton, 'Radio button to select address should be visible').toBeVisible();
78 |
79 | });
80 |
81 |
82 | /**
83 | * @feature Place order for simple product
84 | * @scenario User places an order for a simple product
85 | * @given I have a product in my cart
86 | * @and I am on any page
87 | * @when I navigate to the checkout
88 | * @and I fill in the required fields
89 | * @and I click the button to place my order
90 | * @then I should see a confirmation that my order has been placed
91 | * @and a order number should be created and show to me
92 | */
93 | test('Place_order_for_simple_product',{ tag: ['@simple-product-order', '@hot'],}, async ({page}, testInfo) => {
94 | const checkoutPage = new CheckoutPage(page);
95 | let orderNumber = await checkoutPage.placeOrder();
96 | testInfo.annotations.push({ type: 'Order number', description: `${orderNumber}` });
97 | });
98 | });
99 |
100 | test.describe('Checkout (guest)', () => {
101 | /**
102 | * @feature Discount Code
103 | * @scenario User adds a discount code to their cart
104 | * @given I have a product in my cart
105 | * @and I am on my cart page
106 | * @when I click on the 'add discount code' button
107 | * @then I fill in a code
108 | * @and I click on 'apply code'
109 | * @then I should see a confirmation that my code has been added
110 | * @and the code should be visible in the cart
111 | * @and a discount should be applied to the product
112 | */
113 | test('Add_coupon_code_in_checkout',{ tag: ['@checkout', '@coupon-code', '@cold']}, async ({page, browserName}) => {
114 | const checkout = new CheckoutPage(page);
115 | const browserEngine = browserName?.toUpperCase() || "UNKNOWN";
116 | const discountCode = requireEnv(`MAGENTO_COUPON_CODE_${browserEngine}`);
117 |
118 | await checkout.applyDiscountCodeCheckout(discountCode);
119 | });
120 |
121 | test('Verify_price_calculations_in_checkout', { tag: ['@checkout', '@price-calculation'] }, async ({ page }) => {
122 | const productPage = new ProductPage(page);
123 | const checkoutPage = new CheckoutPage(page);
124 |
125 | // Add product to cart and go to checkout
126 | await productPage.addSimpleProductToCart(UIReference.productPage.simpleProductTitle, slugs.productpage.simpleProductSlug);
127 | await page.goto(slugs.checkout.checkoutSlug);
128 |
129 | // Select shipping method to trigger price calculations
130 | await checkoutPage.shippingMethodOptionFixed.check();
131 |
132 | // Wait for totals to update
133 | await page.waitForFunction(() => {
134 | const element = document.querySelector('.magewire\\.messenger');
135 | return element && getComputedStyle(element).height === '0px';
136 | });
137 |
138 | // Get all price components using the verifyPriceCalculations method from the CheckoutPage fixture
139 | await checkoutPage.verifyPriceCalculations();
140 | });
141 |
142 | /**
143 | * @feature Remove discount code from checkout
144 | * @scenario User has added a discount code, then removes it
145 | * @given I have a product in my cart
146 | * @and I am on the checkout page
147 | * @when I add a discount code
148 | * @then I should see a notification
149 | * @and the code should be visible in the cart
150 | * @and a discount should be applied to a product
151 | * @when I click the 'cancel coupon' button
152 | * @then I should see a notification the discount has been removed
153 | * @and the discount should no longer be visible.
154 | */
155 |
156 | test('Remove_coupon_code_from_checkout',{ tag: ['@checkout', '@coupon-code', '@cold']}, async ({page, browserName}) => {
157 | const checkout = new CheckoutPage(page);
158 | const browserEngine = browserName?.toUpperCase() || "UNKNOWN";
159 | const discountCode = requireEnv(`MAGENTO_COUPON_CODE_${browserEngine}`);
160 |
161 | await checkout.applyDiscountCodeCheckout(discountCode);
162 | await checkout.removeDiscountCode();
163 | });
164 |
165 | /**
166 | * @feature Incorrect discount code check
167 | * @scenario The user provides an incorrect discount code, the system should reflect that
168 | * @given I have a product in my cart
169 | * @and I am on the cart page
170 | * @when I enter a wrong discount code
171 | * @then I should get a notification that the code did not work.
172 | */
173 |
174 | test('Invalid_coupon_code_in_checkout_is_rejected',{ tag: ['@checkout', '@coupon-code', '@cold'] }, async ({page}) => {
175 | const checkout = new CheckoutPage(page);
176 | await checkout.enterWrongCouponCode("incorrect discount code");
177 | });
178 |
179 | /**
180 | * @feature Payment Method Selection
181 | * @scenario Guest user selects different payment methods during checkout
182 | * @given I have a product in my cart
183 | * @and I am on the checkout page as a guest
184 | * @when I select a payment method
185 | * @and I complete the checkout process
186 | * @then I should see a confirmation that my order has been placed
187 | * @and a order number should be created and shown to me
188 | */
189 | test('Guest_can_select_payment_methods', { tag: ['@checkout', '@payment-methods', '@cold'] }, async ({ page }) => {
190 | const checkoutPage = new CheckoutPage(page);
191 |
192 | // Test with check/money order payment
193 | await test.step('Place order with check/money order payment', async () => {
194 | await page.goto(slugs.checkout.checkoutSlug);
195 | await checkoutPage.fillShippingAddress();
196 | await checkoutPage.shippingMethodOptionFixed.check();
197 | await checkoutPage.selectPaymentMethod('check');
198 | let orderNumber = await checkoutPage.placeOrder();
199 | expect(orderNumber, 'Order number should be generated and returned').toBeTruthy();
200 | });
201 | });
202 | });
203 |
--------------------------------------------------------------------------------
/tests/compare.spec.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { test, expect } from '@playwright/test';
4 | import { UIReference, outcomeMarker, slugs } from 'config';
5 |
6 | import ComparePage from './poms/frontend/compare.page';
7 | import LoginPage from './poms/frontend/login.page';
8 | import ProductPage from './poms/frontend/product.page';
9 | import { requireEnv } from './utils/env.utils';
10 |
11 | // TODO: Create a fixture for this
12 | test.beforeEach('Add 2 products to compare, then navigate to comparison page', async ({ page }) => {
13 | await test.step('Add products to compare', async () =>{
14 | const productPage = new ProductPage(page);
15 | await productPage.addProductToCompare(UIReference.productPage.simpleProductTitle, slugs.productpage.simpleProductSlug);
16 | await productPage.addProductToCompare(UIReference.productPage.secondSimpleProducTitle, slugs.productpage.secondSimpleProductSlug);
17 | });
18 |
19 | await test.step('Navigate to product comparison page', async () =>{
20 | await page.goto(slugs.productpage.productComparisonSlug);
21 | await expect(page.getByRole('heading', { name: UIReference.comparePage.comparisonPageTitleText }).locator('span')).toBeVisible();
22 | });
23 | });
24 |
25 | /**
26 | * @feature Add product to cart from the comparison page
27 | * @scenario User adds a product to their cart from the comparison page
28 | * @given I am on the comparison page and have a product in my comparison list
29 | * @when I click the 'add to cart' button
30 | * @then I should see a notification that the product has been added
31 | */
32 | test('Add_product_to_cart_from_comparison_page',{ tag: ['@comparison-page', '@cold']}, async ({page}) => {
33 | const comparePage = new ComparePage(page);
34 | await comparePage.addToCart(UIReference.productPage.simpleProductTitle);
35 | });
36 |
37 | /**
38 | * @feature A product cannot be added to the wishlist without being logged in
39 | * @scenario User attempt to add a product to their wishlist from the comparison page
40 | * @given I am on the comparison page and have a product in my comparison list
41 | * @when I click the 'add to wishlist' button
42 | * @then I should see an error message
43 | */
44 | test('Guests_can_not_add_a_product_to_their_wishlist',{ tag: ['@comparison-page', '@cold']}, async ({page}) => {
45 | const errorMessage = page.locator(UIReference.general.errorMessageLocator);
46 | let productNotWishlistedNotificationText = outcomeMarker.comparePage.productNotWishlistedNotificationText;
47 | let addToWishlistButton = page.getByLabel(`${UIReference.comparePage.addToWishListLabel} ${UIReference.productPage.simpleProductTitle}`);
48 | await addToWishlistButton.click();
49 | await errorMessage.waitFor();
50 | await expect(page.getByText(productNotWishlistedNotificationText)).toBeVisible();
51 |
52 | await expect(page.url()).toContain(slugs.account.loginSlug);
53 | });
54 |
55 | /**
56 | * @feature Add product to wishlist from the comparison page
57 | * @scenario User adds a product to their wishlist from the comparison page
58 | * @given I am on the comparison page and have a product in my comparison list
59 | * @and I am logged in
60 | * @when I click the 'add to wishlist' button
61 | * @then I should see a notification that the product has been added to my wishlist
62 | */
63 | test('Add_product_to_wishlist_from_comparison_page',{ tag: ['@comparison-page', '@hot']}, async ({page, browserName}) => {
64 | await test.step('Log in with account', async () =>{
65 | const loginPage = new LoginPage(page);
66 | const browserEngine = browserName?.toUpperCase() || "UNKNOWN";
67 | const emailInputValue = requireEnv(`MAGENTO_EXISTING_ACCOUNT_EMAIL_${browserEngine}`);
68 | const passwordInputValue = requireEnv('MAGENTO_EXISTING_ACCOUNT_PASSWORD');
69 |
70 | await loginPage.login(emailInputValue, passwordInputValue);
71 | });
72 |
73 | await test.step('Add product to compare', async () =>{
74 | const productPage = new ProductPage(page);
75 | await page.goto(slugs.productpage.productComparisonSlug);
76 | await productPage.addProductToCompare(UIReference.productPage.simpleProductTitle, slugs.productpage.simpleProductSlug);
77 | });
78 |
79 | await test.step('Add product to wishlist', async () =>{
80 | const comparePage = new ComparePage(page);
81 | await comparePage.addToWishList(UIReference.productPage.simpleProductTitle);
82 |
83 | //TODO: Also remove the product for clear testing environment)
84 | });
85 | });
86 |
87 |
88 |
89 | test.afterEach('Remove products from compare', async ({ page }) => {
90 | // ensure we are on the right page
91 | await page.goto(slugs.productpage.productComparisonSlug);
92 |
93 | page.on('dialog', dialog => dialog.accept());
94 | const comparePage = new ComparePage(page);
95 | await comparePage.removeProductFromCompare(UIReference.productPage.simpleProductTitle);
96 | await comparePage.removeProductFromCompare(UIReference.productPage.secondSimpleProducTitle);
97 | });
--------------------------------------------------------------------------------
/tests/config/element-identifiers.json:
--------------------------------------------------------------------------------
1 | {
2 | "accountCreation": {
3 | "createAccountButtonLabel": "Create an Account"
4 | },
5 | "accountDashboard": {
6 | "accountDashboardTitleLabel": "Account Information",
7 | "accountSideBarLabel": "Sidebar Main",
8 | "addAddressButtonLabel": "ADD NEW ADDRESS",
9 | "addressBookArea": ".block-addresses-list",
10 | "addressDeleteIconButton": "trash",
11 | "editAddressIconButton": "pencil-alt",
12 | "accountInformationFieldLocator": ".column > div > div > .flex",
13 | "links": {
14 | "newsletterLink": "Newsletter Subscriptions"
15 | }
16 | },
17 | "newAddress": {
18 | "addNewAddressTitle": "Add New Address",
19 | "companyNameLabel": "Company",
20 | "phoneNumberLabel": "Phone Number",
21 | "streetAddressLabel": "Street Address",
22 | "zipCodeLabel": "Zip/Postal Code",
23 | "cityNameLabel": "City",
24 | "countryLabel": "Country",
25 | "provinceSelectLabel": "State/Province",
26 | "provinceSelectFilterLabel": "Please select a region, state or province.",
27 | "saveAdressButton": "Save Address"
28 | },
29 | "newsletterSubscriptions": {
30 | "generalSubscriptionCheckLabel": "General Subscription",
31 | "saveSubscriptionsButton": "Save"
32 | },
33 | "productPage": {
34 | "addToCartButtonLocator": "shopping-cart Add to Cart",
35 | "addToCompareButtonLabel": "Add to Compare",
36 | "addToWishlistButtonLabel": "Add to Wish List",
37 | "simpleProductTitle": "Push It Messenger Bag",
38 | "secondSimpleProducTitle": "Aim Analog Watch",
39 | "simpleProductPrice": ".final-price .price-wrapper .price",
40 | "configurableProductTitle": "Inez Full Zip Jacket",
41 | "configurableProductSizeLabel": "Size",
42 | "configurableProductColorLabel": "Color",
43 | "configurableProductOptionForm": "#product_addtocart_form",
44 | "configurableProductOptionValue": ".product-option-value-label",
45 | "quantityFieldLabel": "Quantity",
46 | "fullScreenOpenLabel": "Click to view image in",
47 | "fullScreenCloseLabel": "Close fullscreen",
48 | "thumbnailImageLabel": "View larger image",
49 | "reviewCountLabel": "Show items per page"
50 | },
51 | "categoryPage":{
52 | "activeViewLocator": ".active",
53 | "categoryPageTitleText": "Women",
54 | "firstFilterOptionLocator": "#filter-option-0-content",
55 | "itemsOnPageAmountLocator": ".toolbar-number",
56 | "itemsPerPageButtonLabel": "Show items per page",
57 | "productGridLocator": ".products-grid",
58 | "removeActiveFilterButtonLabel": "Remove active",
59 | "sizeFilterButtonLabel": "Size filter",
60 | "sizeLButtonLocator": "a.swatch-option-link-layered[aria-label*=\"Filter Size L\"]",
61 | "sortByButtonLabel": "Sort by",
62 | "sortByButtonLocator": ".form-select.sorter-options",
63 | "viewSwitchLabel": "Products view mode",
64 | "viewGridLabel": "Products view mode - Grid",
65 | "viewListLabel": "Products view mode - List"
66 | },
67 | "cart": {
68 | "applyDiscountButtonLabel": "Apply Discount",
69 | "cancelCouponButtonLabel": "Cancel Coupon",
70 | "cartTitleText": "Shopping Cart",
71 | "cartQuantityLabel": "Qty",
72 | "discountInputFieldLabel": "Enter discount code",
73 | "showDiscountFormButtonLabel": "Apply Discount Code",
74 | "updateItemButtonLabel": "shopping-cart Update item",
75 | "updateShoppingCartButtonLabel": "Update Shopping Cart"
76 | },
77 | "cartPriceRulesPage": {
78 | "actionsSubtitleLabel": "Actions",
79 | "addCartPriceRuleButtonLabel": "Add New Rule",
80 | "couponCodeFieldLabel": "Coupon Code",
81 | "couponTypeSelectField": "select[name='coupon_type']",
82 | "customerGroupsSelectLabel": "Customer Groups",
83 | "discountAmountFieldLabel": "Discount Amount",
84 | "ruleNameFieldLabel": "Rule Name",
85 | "saveRuleButtonLabel": "Save",
86 | "websitesSelectLabel": "Websites"
87 | },
88 | "checkout": {
89 | "applyDiscountButtonLabel": "Apply Coupon",
90 | "applyDiscountCodeLabel": "Apply Discount Code",
91 | "cancelDiscountButtonLabel": "Cancel Coupon",
92 | "cartDetailsLocator": "#checkout-cart-details div",
93 | "continueShoppingLabel": "Continue Shopping",
94 | "discountInputFieldLabel": "Enter discount code",
95 | "openCartButtonLabel": "Cart",
96 | "openCartButtonLabelCont": "item",
97 | "openCartButtonLabelContMultiple": "items",
98 | "openDiscountFormLabel": "Apply Discount Code",
99 | "paymentOptionCheckLabel": "Check / Money order Free",
100 | "paymentOptionCreditCardLabel": "Credit Card",
101 | "paymentOptionPaypalLabel": "PayPal",
102 | "creditCardNumberLabel": "Credit Card Number",
103 | "creditCardExpiryLabel": "Expiration Date",
104 | "creditCardCVVLabel": "Card Verification Number",
105 | "creditCardNameLabel": "Name on Card",
106 | "placeOrderButtonLabel": "Place Order",
107 | "remove": "Remove",
108 | "shippingAddressRadioLocator": "#shipping-details input[type='radio']",
109 | "shippingMethodFixedLabel": "Fixed"
110 | },
111 | "comparePage": {
112 | "removeCompareLabel": "Remove Product",
113 | "addToWishListLabel": "Add to Wish List",
114 | "comparisonPageEmptyText": "You have no items to compare.",
115 | "comparisonPageTitleText": "Compare Products"
116 | },
117 | "configurationPage": {
118 | "advancedAdministrationTabLabel": "Admin",
119 | "advancedTabLabel": "Advanced",
120 | "allowMultipleLoginsSelectField": "#admin_security_admin_account_sharing",
121 | "allowMultipleLoginsSystemCheckbox": "#admin_security_admin_account_sharing_inherit",
122 | "captchaSettingSelectField": "#customer_captcha_enable",
123 | "captchaSettingSystemCheckbox": "#customer_captcha_enable_inherit",
124 | "captchaSectionLabel": "CAPTCHA",
125 | "customerConfigurationTabLabel": "Customer Configuration",
126 | "customersTabLabel": "Customers",
127 | "saveConfigButtonLabel": "Save Config",
128 | "securitySectionLabel": "Security"
129 | },
130 | "contactPage": {
131 | "messageFieldSelector": "#comment"
132 | },
133 | "credentials": {
134 | "currentPasswordFieldLabel": "Current Password",
135 | "emailFieldLabel": "Email",
136 | "emailCheckoutFieldLabel": "Email address",
137 | "loginButtonLabel": "Sign In",
138 | "nameFieldLabel": "Name",
139 | "newPasswordConfirmFieldLabel": "Confirm New Password",
140 | "newPasswordFieldLabel": "New Password",
141 | "passwordConfirmFieldLabel": "Confirm Password",
142 | "passwordFieldLabel": "Password"
143 | },
144 | "general": {
145 | "addToCartLabel": "Add to Cart",
146 | "closeMessageLabel": "Close message",
147 | "errorMessageLocator": "div.message.error",
148 | "genericPriceLabel": "Price",
149 | "genericPriceSymbol": "$",
150 | "genericSaveButtonLabel": "Save",
151 | "genericSubmitButtonLabel": "Submit",
152 | "messageLocator": "div.message",
153 | "removeLabel": "Remove",
154 | "successMessageLocator": "div.message.success"
155 | },
156 | "homePage": {
157 | "homePageTitleText": "Hyvä Themes"
158 | },
159 | "magentoAdminPage": {
160 | "loginButtonLabel": "Sign In",
161 | "navigation": {
162 | "marketingButtonLabel": "Marketing",
163 | "storesButtonLabel": "Stores"
164 | },
165 | "passwordFieldLabel": "Password",
166 | "subNavigation": {
167 | "cartPriceRulesButtonLabel": "Cart Price Rules",
168 | "configurationButtonLabel": "Configuration"
169 | },
170 | "usernameFieldLabel": "Username"
171 | },
172 | "mainMenu": {
173 | "miniCartLabel": "Toggle minicart",
174 | "myAccountButtonLabel": "My Account",
175 | "myAccountLogoutItem": "Sign Out"
176 | },
177 | "miniCart": {
178 | "checkOutButtonLabel": "Checkout",
179 | "editProductIconLabel": "Edit product",
180 | "minicartButtonLocator": "#menu-cart-icon",
181 | "minicartAmountBubbleLocator": "#menu-cart-icon > span",
182 | "minicartPriceFieldClass": ".price-excluding-tax .minicart-price .price",
183 | "miniCartToggleLabelEmpty": "Cart is empty",
184 | "miniCartToggleLabelMultiItem": "items",
185 | "miniCartToggleLabelOneItem": "1 item",
186 | "miniCartToggleLabelPrefix": "Toggle minicart,",
187 | "productQuantityFieldLabel": "Quantity",
188 | "removeProductIconLabel": "Remove product",
189 | "toCartLinkLabel": "View and Edit Cart"
190 | },
191 | "search": {
192 | "searchToggleLocator": "#menu-search-icon",
193 | "searchInputLocator": "#search",
194 | "suggestionBoxLocator": "#search_autocomplete"
195 | },
196 | "personalInformation": {
197 | "changePasswordCheckLabel": "Change Password",
198 | "changeEmailCheckLabel": "Change Email",
199 | "firstNameLabel": "First Name",
200 | "lastNameLabel": "Last Name"
201 | },
202 | "wishListPage": {
203 | "wishListItemGridLabel": "#wishlist-view-form",
204 | "updateCompareListButtonLabel": "Update Wish List"
205 | }
206 | }
207 |
--------------------------------------------------------------------------------
/tests/config/input-values.json:
--------------------------------------------------------------------------------
1 | {
2 | "accountCreation": {
3 | "emailHandleValue":"test-user",
4 | "emailHostValue": "gmail.com",
5 | "firstNameValue": "John",
6 | "lastNameValue": "Doe"
7 | },
8 | "adminLogins": {
9 | "allowMultipleLogins": "Yes"
10 | },
11 | "captcha": {
12 | "captchaDisabled": "No"
13 | },
14 | "contact": {
15 | "contactFormEmailValue": "robertbaratheon@gameofthrones.com",
16 | "contactFormMessage": "Hello! I am filling out this form as a test only. Feel free to ignore this message."
17 | },
18 | "coupon": {
19 | "couponCodeRuleName": "Test coupon",
20 | "couponType": "Specific Coupon"
21 | },
22 | "addressCountries": [
23 | "Netherlands",
24 | "United Kingdom"
25 | ],
26 | "editedAddress": {
27 | "editCityValue": "Pallet Town",
28 | "editCompanyNameValue": "Pokémon",
29 | "editfirstNameValue": "Ash",
30 | "editLastNameValue": "Ketchum",
31 | "editStateValue": "Kansas",
32 | "editStreetAddressValue": "House on the left",
33 | "editZipCodeValue": "00151"
34 | },
35 | "firstAddress": {
36 | "firstCityValue": "Testing Valley",
37 | "firstCompanyNameValue": "ACME Company",
38 | "firstNonDefaultCountry": "Netherlands",
39 | "firstPhoneNumberValue": "0622000000",
40 | "firstProvinceValue": "Idaho",
41 | "firstStreetAddressValue": "Testingstreet 1",
42 | "firstZipCodeValue": "12345"
43 | },
44 | "payment": {
45 | "creditCard": {
46 | "number": "4111111111111111",
47 | "expiry": "12/25",
48 | "cvv": "123",
49 | "name": "Test User"
50 | }
51 | },
52 | "secondAddress": {
53 | "secondCityValue": "Little Whinging",
54 | "secondCompanyNameValue": "Hogwarts",
55 | "secondNonDefaultCountry": "United Kingdom",
56 | "secondPhoneNumberValue": "0620081998",
57 | "secondProvinceValue": "South Dakota",
58 | "secondStreetAddressValue": "Under the Stairs, 4 Privet Drive",
59 | "secondZipCodeValue": "67890"
60 | },
61 | "search": {
62 | "queryMultipleResults": "bag",
63 | "querySpecificProduct": "Push It Messenger Bag",
64 | "queryNoResults": "sdfasdfasddd"
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/tests/config/outcome-markers.json:
--------------------------------------------------------------------------------
1 | {
2 | "account": {
3 | "accountCreatedNotificationText": "Thank you for registering with Main Website Store.",
4 | "accountPageTitle": "Account Information",
5 | "addressBookTitle": "Customer Address",
6 | "changedPasswordNotificationText": "You saved the account",
7 | "newsletterRemovedNotification": "We have removed your newsletter subscription.",
8 | "newsletterSavedNotification": "We have saved your subscription.",
9 | "newsletterSubscriptionTitle": "Newsletter Subscription",
10 | "newsletterUpdatedNotification": "We have updated your subscription."
11 | },
12 | "address": {
13 | "addressDeletedNotification": "You deleted the address.",
14 | "newAddressAddedNotifcation": "You saved the address."
15 | },
16 | "cart": {
17 | "discountAppliedNotification": "You used coupon code",
18 | "discountRemovedNotification": "You canceled the coupon code.",
19 | "incorrectCouponCodeNotificationOne": "The coupon code",
20 | "incorrectCouponCodeNotificationTwo": "is not valid.",
21 | "priceReducedSymbols": "- $"
22 | },
23 | "checkout": {
24 | "checkoutPriceReducedSymbol": "-$",
25 | "couponAppliedNotification": "Your coupon was successfully applied",
26 | "couponRemovedNotification": "Your coupon was successfully removed",
27 | "incorrectDiscountNotification": "The coupon code isn't valid. Verify the code and try again.",
28 | "orderPlacedNotification": "Thank you for your purchase!",
29 | "orderPlacedNumberText": "Your order number is"
30 | },
31 | "comparePage": {
32 | "productRemovedNotificationTextOne": "You removed product",
33 | "productRemovedNotificationTextTwo": "from the comparison list.",
34 | "productNotWishlistedNotificationText": "You must login or register to add items to your wishlist."
35 | },
36 | "contactPage": {
37 | "messageSentConfirmationText": "Thanks for contacting us with"
38 | },
39 | "homePage": {
40 | "firstProductName": "Aim Analog Watch"
41 | },
42 | "logout": {
43 | "logoutConfirmationText": "You have signed out"
44 | },
45 | "miniCart": {
46 | "configurableProductMinicartTitle": "x Inez Full Zip Jacket",
47 | "miniCartTitle": "My Cart",
48 | "productQuantityChangedConfirmation": "was updated in your shopping cart",
49 | "productRemovedConfirmation": "You removed the item.",
50 | "simpleProductInCartTitle": "x Push It Messenger Bag"
51 | },
52 | "productPage": {
53 | "borderClassRegex": ".* border-primary$",
54 | "simpleProductAddedNotification": "You added"
55 | },
56 | "login": {
57 | "invalidCredentialsMessage": "The account sign-in was incorrect or your account is disabled temporarily. Please wait and try again later."
58 | },
59 | "search": {
60 | "noResultsMessage": "Your search returned no results."
61 | },
62 | "wishListPage": {
63 | "wishListAddedNotification": "has been added to your Wish List."
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/tests/config/slugs.json:
--------------------------------------------------------------------------------
1 | {
2 | "account": {
3 | "accountOverviewSlug": "/customer/account/",
4 | "addressBookSlug": "/customer/address",
5 | "addressIndexSlug": "/customer/address/index",
6 | "addressNewSlug": "customer/address/new",
7 | "accountEditSlug": "/customer/account/edit/",
8 | "changePasswordSlug": "/customer/account/edit/changepass/1/",
9 | "createAccountSlug": "/customer/account/create",
10 | "loginSlug": "/customer/account/login",
11 | "orderHistorySlug": "/sales/order/history/"
12 | },
13 | "cart": {
14 | "cartProductChangeSlug": "/cart/configure/",
15 | "cartSlug": "/checkout/cart/"
16 | },
17 | "categoryPage": {
18 | "categorySlug": "/women.html"
19 | },
20 | "checkout": {
21 | "checkoutSlug": "/checkout/"
22 | },
23 | "contact": {
24 | "contactSlug": "/contact"
25 | },
26 | "productpage": {
27 | "configurableProductSlug": "/inez-full-zip-jacket.html",
28 | "productComparisonSlug": "/catalog/product_compare/index/",
29 | "secondSimpleProductSlug": "/aim-analog-watch.html",
30 | "simpleProductSlug": "/push-it-messenger-bag.html"
31 | },
32 | "search": {
33 | "resultsSlug": "/catalogsearch/result/"
34 | },
35 | "wishlist": {
36 | "wishListRegex": ".*wishlist.*"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/tests/config/test-toggles.json:
--------------------------------------------------------------------------------
1 | {
2 | "general": {
3 | "setup": false
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/tests/contact.spec.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { test } from '@playwright/test';
4 | import ContactPage from './poms/frontend/contact.page';
5 |
6 | /**
7 | * @feature Magento 2 Contact Form
8 | * @scenario User fills in the contact form and sends a message
9 | * @given I om any Magento 2 page
10 | * @when I navigate to the contact page
11 | * @and I fill in the required fields
12 | * @when I click the button to send the form
13 | * @then I should see a notification my message has been sent
14 | * @and the fields should be empty again.
15 | */
16 | test('Send_message_through_contact_form',{ tag: ['@contact-form', '@cold']}, async ({page}) => {
17 | const contactPage = new ContactPage(page);
18 | await contactPage.fillOutForm();
19 | });
20 |
--------------------------------------------------------------------------------
/tests/healthcheck.spec.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { test, expect } from '@playwright/test';
4 | import { UIReference, slugs } from 'config';
5 |
6 | test.describe('Page health checks', () => {
7 | test('Homepage_returns_200', { tag: ['@smoke', '@cold'] }, async ({page}) => {
8 | const homepageURL = process.env.PLAYWRIGHT_BASE_URL || process.env.BASE_URL;
9 | if (!homepageURL) {
10 | throw new Error("PLAYWRIGHT_BASE_URL has not been defined in the .env file.");
11 | }
12 |
13 | const homepageResponsePromise = page.waitForResponse(homepageURL);
14 | await page.goto(homepageURL);
15 | const homepageResponse = await homepageResponsePromise;
16 | expect(homepageResponse.status(), 'Homepage should return 200').toBe(200);
17 |
18 | await expect(
19 | page.getByRole('heading', {name: UIReference.homePage.homePageTitleText}),
20 | 'Homepage has a visible title'
21 | ).toBeVisible();
22 | });
23 |
24 | test('Plp_returns_200', { tag: ['@smoke', '@cold'] }, async ({page}) => {
25 | const plpResponsePromise = page.waitForResponse(slugs.categoryPage.categorySlug);
26 | await page.goto(slugs.categoryPage.categorySlug);
27 | const plpResponse = await plpResponsePromise;
28 | expect(plpResponse.status(), 'PLP should return 200').toBe(200);
29 |
30 | await expect(
31 | page.getByRole('heading', {name: UIReference.categoryPage.categoryPageTitleText}),
32 | 'PLP has a visible title'
33 | ).toBeVisible();
34 | });
35 |
36 | test('Pdp_returns_200', { tag: ['@smoke', '@cold'] }, async ({page}) => {
37 | const pdpResponsePromise = page.waitForResponse(slugs.productpage.simpleProductSlug);
38 | await page.goto(slugs.productpage.simpleProductSlug);
39 | const pdpResponse = await pdpResponsePromise;
40 | expect(pdpResponse.status(), 'PDP should return 200').toBe(200);
41 |
42 | await expect(
43 | page.getByRole('heading', {level: 1, name: UIReference.productPage.simpleProductTitle}),
44 | 'PDP has a visible title'
45 | ).toBeVisible();
46 | });
47 |
48 | test('Checkout_returns_200', { tag: ['@smoke', '@cold'] }, async ({page}) => {
49 | const responsePromise = page.waitForResponse(slugs.checkout.checkoutSlug);
50 |
51 | await page.goto(slugs.checkout.checkoutSlug);
52 | const response = await responsePromise;
53 |
54 | expect(response.status(), 'Cart empty, checkout should return 302').toBe(302);
55 | expect(page.url(), 'Cart empty, checkout should redirect to cart').toContain(slugs.cart.cartSlug);
56 |
57 | await expect(
58 | page.getByRole('heading', {name: UIReference.cart.cartTitleText}),
59 | 'Cart has a visible title'
60 | ).toBeVisible();
61 |
62 | expect((await page.request.head(page.url())).status(), `Current page (${page.url()}) should return 200`).toBe(200);
63 | });
64 | });
65 |
--------------------------------------------------------------------------------
/tests/home.spec.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { test, expect } from '@playwright/test';
4 | import { outcomeMarker } from 'config';
5 |
6 | import MainMenuPage from './poms/frontend/mainmenu.page';
7 | import HomePage from './poms/frontend/home.page';
8 |
9 | test('Add_product_on_homepage_to_cart',{ tag: ['@homepage', '@cold']}, async ({page}) => {
10 | const homepage = new HomePage(page);
11 | const mainmenu = new MainMenuPage(page);
12 |
13 | await page.goto('');
14 | await homepage.addHomepageProductToCart();
15 | await mainmenu.openMiniCart();
16 | await expect(page.getByText('x ' + outcomeMarker.homePage.firstProductName), 'product should be visible in cart').toBeVisible();
17 | });
18 |
--------------------------------------------------------------------------------
/tests/login.spec.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { test as base, expect } from '@playwright/test';
4 | import { outcomeMarker, inputValues } from 'config';
5 | import { requireEnv } from './utils/env.utils';
6 |
7 | import LoginPage from './poms/frontend/login.page';
8 |
9 | base('User_logs_in_with_valid_credentials', {tag: '@hot'}, async ({page, browserName}) => {
10 | const browserEngine = browserName?.toUpperCase() || "UNKNOWN";
11 | // We can't move this browser specific check inside LoginPage because the
12 | // variable name differs per browser engine.
13 | const emailInputValue = requireEnv(`MAGENTO_EXISTING_ACCOUNT_EMAIL_${browserEngine}`);
14 | const passwordInputValue = requireEnv('MAGENTO_EXISTING_ACCOUNT_PASSWORD');
15 |
16 | const loginPage = new LoginPage(page)
17 | await loginPage.login(emailInputValue, passwordInputValue);
18 | await page.waitForLoadState('networkidle');
19 |
20 | // Check customer section data in localStorage and verify name
21 | const customerData = await page.evaluate(() => {
22 | const data = localStorage.getItem('mage-cache-storage');
23 | return data ? data : null;
24 | });
25 |
26 | expect(customerData, 'Customer data should exist in localStorage').toBeTruthy();
27 | expect(customerData, 'Customer data should contain customer information').toContain('customer');
28 |
29 | // Parse the JSON and verify firstname and lastname
30 | const parsedData = await page.evaluate(() => {
31 | const data = localStorage.getItem('mage-cache-storage');
32 | return data ? JSON.parse(data) : null;
33 | });
34 |
35 | expect(parsedData.customer.firstname, 'Customer firstname should match').toBe(inputValues.accountCreation.firstNameValue);
36 | expect(parsedData.customer.fullname, 'Customer lastname should match').toContain(inputValues.accountCreation.lastNameValue);
37 | });
38 |
39 | base('Invalid_credentials_are_rejected', async ({page}) => {
40 | const loginPage = new LoginPage(page);
41 | await loginPage.loginExpectError('invalid@example.com', 'wrongpassword', outcomeMarker.login.invalidCredentialsMessage);
42 | });
43 |
44 | base('Login_fails_with_missing_password', async ({page}) => {
45 | const loginPage = new LoginPage(page);
46 | await loginPage.loginExpectError('invalid@example.com', '', '');
47 | });
48 |
--------------------------------------------------------------------------------
/tests/mainmenu.spec.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { test } from '@playwright/test';
4 | import { UIReference, slugs } from 'config';
5 |
6 | import LoginPage from './poms/frontend/login.page';
7 | import MainMenuPage from './poms/frontend/mainmenu.page';
8 | import ProductPage from './poms/frontend/product.page';
9 | import { requireEnv } from './utils/env.utils';
10 |
11 | // no resetting storageState, mainmenu has more functionalities when logged in.
12 |
13 | // Before each test, log in
14 | test.beforeEach(async ({ page, browserName }) => {
15 | const browserEngine = browserName?.toUpperCase() || "UNKNOWN";
16 | const emailInputValue = requireEnv(`MAGENTO_EXISTING_ACCOUNT_EMAIL_${browserEngine}`);
17 | const passwordInputValue = requireEnv('MAGENTO_EXISTING_ACCOUNT_PASSWORD');
18 |
19 | const loginPage = new LoginPage(page);
20 | await loginPage.login(emailInputValue, passwordInputValue);
21 | });
22 |
23 | /**
24 | * @feature Logout
25 | * @scenario The user can log out
26 | * @given I am logged in
27 | * @and I am on any Magento 2 page
28 | * @when I open the account menu
29 | * @and I click the Logout option
30 | * @then I should see a message confirming I am logged out
31 | */
32 | test('User_logs_out', { tag: ['@mainmenu', '@hot'] }, async ({page}) => {
33 | const mainMenu = new MainMenuPage(page);
34 | await mainMenu.logout();
35 | });
36 |
37 |
38 | test('Navigate_to_account_page', { tag: ['@mainmenu', '@hot'] }, async ({page}) => {
39 | const mainMenu = new MainMenuPage(page);
40 | await mainMenu.gotoMyAccount();
41 | });
42 |
43 | test('Open_the_minicart', { tag: ['@mainmenu', '@cold'] }, async ({page}, testInfo) => {
44 | testInfo.annotations.push({ type: 'WARNING (FIREFOX)', description: `The minicart icon does not lose its aria-disabled=true flag when the first product is added. This prevents Playwright from clicking it. A fix will be added in the future.`});
45 |
46 | const mainMenu = new MainMenuPage(page);
47 | const productPage = new ProductPage(page);
48 |
49 | await productPage.addSimpleProductToCart(UIReference.productPage.simpleProductTitle, slugs.productpage.simpleProductSlug);
50 | await mainMenu.openMiniCart();
51 | });
52 |
--------------------------------------------------------------------------------
/tests/minicart.spec.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import {test, expect} from '@playwright/test';
4 | import {UIReference, outcomeMarker, slugs} from 'config';
5 |
6 | import MainMenuPage from './poms/frontend/mainmenu.page';
7 | import ProductPage from './poms/frontend/product.page';
8 | import MiniCartPage from './poms/frontend/minicart.page';
9 |
10 | test.describe('Minicart Actions', {annotation: {type: 'Minicart', description: 'Minicart simple product tests'},}, () => {
11 |
12 | /**
13 | * @feature BeforeEach runs before each test in this group.
14 | * @scenario Add a product to the cart and confirm it's there.
15 | * @given I am on any page
16 | * @when I navigate to a (simple) product page
17 | * @and I add it to my cart
18 | * @then I should see a notification
19 | * @when I click the cart in the main menu
20 | * @then the minicart should become visible
21 | * @and I should see the product in the minicart
22 | */
23 | test.beforeEach(async ({ page }) => {
24 | const mainMenu = new MainMenuPage(page);
25 | const productPage = new ProductPage(page);
26 |
27 | await page.goto(slugs.productpage.simpleProductSlug);
28 | await productPage.addSimpleProductToCart(UIReference.productPage.simpleProductTitle, slugs.productpage.simpleProductSlug);
29 | await mainMenu.openMiniCart();
30 | await expect(page.getByText(outcomeMarker.miniCart.simpleProductInCartTitle)).toBeVisible();
31 | });
32 |
33 | /**
34 | * @feature Magento 2 Minicart to Checkout
35 | * @scenario User adds a product to cart, then uses minicart to navigate to checkout
36 | * @given I have added a (simple) product to the cart and opened the minicart
37 | * @when I click on the 'to checkout' button
38 | * @then I should navigate to the checkout page
39 | */
40 |
41 | test('Add_product_to_minicart_and_go_to_checkout',{ tag: ['@minicart-simple-product', '@cold']}, async ({page}) => {
42 | const miniCart = new MiniCartPage(page);
43 | await miniCart.goToCheckout();
44 | });
45 |
46 | /**
47 | * @feature Magento 2 Minicart to Cart
48 | * @scenario User adds a product to cart, then uses minicart to navigate to their cart
49 | * @given I have added a (simple) product to the cart and opened the minicart
50 | * @when I click on the 'to cart' link
51 | * @then I should be navigated to the cart page
52 | */
53 |
54 | test('Add_product_to_minicart_and_go_to_cart',{ tag: ['@minicart-simple-product', '@cold']}, async ({page}) => {
55 | const miniCart = new MiniCartPage(page);
56 | await miniCart.goToCart();
57 | });
58 |
59 | /**
60 | * @feature Magento 2 Minicart quantity change
61 | * @scenario User adds a product to the minicart, then changes the quantity using the minicart
62 | * @given I have added a (simple) product to the cart and opened the minicart
63 | * @when I click on the pencil for the product I want to update
64 | * @then I should navigate to a product page that is in my cart
65 | * @when I change the amount
66 | * @and I click the 'update item' button
67 | * @then I should see a confirmation
68 | * @and the new amount should be shown in the minicart
69 | */
70 | test('Change_product_quantity_in_minicart',{ tag: ['@minicart-simple-product', '@cold']}, async ({page}) => {
71 | const miniCart = new MiniCartPage(page);
72 | await miniCart.updateProduct('3');
73 | });
74 |
75 | /**
76 | * @feature Magento 2 minicart product deletion
77 | * @scenario User adds product to cart, then removes from minicart
78 | * @given I have added a (simple) product to the cart and opened the minicart
79 | * @when I click on the delete button
80 | * @then The product should not be in my cart anymore
81 | * @and I should see a notification that the product was removed
82 | */
83 | test('Delete_product_from_minicart',{ tag: ['@minicart-simple-product', '@cold']}, async ({page}, testInfo) => {
84 | testInfo.annotations.push({ type: 'WARNING (FIREFOX)', description: `The minicart icon does not lose its aria-disabled=true flag when the first product is added. This prevents Playwright from clicking it. A fix will be added in the future.`});
85 | const miniCart = new MiniCartPage(page);
86 | await miniCart.removeProductFromMinicart(UIReference.productPage.simpleProductTitle);
87 | });
88 |
89 | /**
90 | * @feature Price Check: Simple Product on Product Detail Page (PDP) and Minicart
91 | * @scenario The price on a PDP should be the same as the price in the minicart
92 | * @given I have added a (simple) product to the cart and opened the minicart
93 | * @then the price listed in the minicart (per product) should be the same as the price on the PDP
94 | */
95 | test('Pdp_price_matches_minicart_price',{ tag: ['@minicart-simple-product', '@cold']}, async ({page}) => {
96 | const miniCart = new MiniCartPage(page);
97 | await miniCart.checkPriceWithProductPage();
98 | });
99 | });
100 |
101 | test.describe('Minicart Actions', {annotation: {type: 'Minicart', description: 'Minicart configurable product tests'},}, () => {
102 | /**
103 | * @feature BeforeEach runs before each test in this group.
104 | * @scenario Add a configurable product to the cart and confirm it's there.
105 | * @given I am on any page
106 | * @when I navigate to a (simple) product page
107 | * @and I add it to my cart
108 | * @then I should see a notification
109 | * @when I click the cart in the main menu
110 | * @then the minicart should become visible
111 | * @and I should see the product in the minicart
112 | */
113 | test.beforeEach(async ({ page }) => {
114 | const mainMenu = new MainMenuPage(page);
115 | const productPage = new ProductPage(page);
116 |
117 | await page.goto(slugs.productpage.configurableProductSlug);
118 | await productPage.addConfigurableProductToCart(UIReference.productPage.configurableProductTitle, slugs.productpage.configurableProductSlug, '2');
119 | await mainMenu.openMiniCart();
120 | await expect(page.getByText(outcomeMarker.miniCart.configurableProductMinicartTitle)).toBeVisible();
121 | });
122 |
123 | /**
124 | * @feature Price Check: Configurable Product on Product Detail Page (PDP) and Minicart
125 | * @scenario The price on a PDP should be the same as the price in the minicart
126 | * @given I have added a (configurable) product to the cart and opened the minicart
127 | * @then the price listed in the minicart (per product) should be the same as the price on the PDP
128 | */
129 | test('Configurable_pdp_price_matches_minicart_price',{ tag: ['@minicart-simple-product', '@cold']}, async ({page}) => {
130 | const miniCart = new MiniCartPage(page);
131 | await miniCart.checkPriceWithProductPage();
132 | });
133 | });
134 |
--------------------------------------------------------------------------------
/tests/orderhistory.spec.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { test } from '@playwright/test';
4 | import { UIReference, slugs } from 'config';
5 | import { requireEnv } from './utils/env.utils';
6 |
7 | import LoginPage from './poms/frontend/login.page';
8 | import ProductPage from './poms/frontend/product.page';
9 | import CheckoutPage from './poms/frontend/checkout.page';
10 | import OrderHistoryPage from './poms/frontend/orderhistory.page';
11 |
12 | test('Recent_order_is_visible_in_history', async ({ page, browserName }) => {
13 | const browserEngine = browserName?.toUpperCase() || 'UNKNOWN';
14 | const emailInputValue = requireEnv(`MAGENTO_EXISTING_ACCOUNT_EMAIL_${browserEngine}`);
15 | const passwordInputValue = requireEnv('MAGENTO_EXISTING_ACCOUNT_PASSWORD');
16 |
17 | const loginPage = new LoginPage(page);
18 | const productPage = new ProductPage(page);
19 | const checkoutPage = new CheckoutPage(page);
20 | const orderHistoryPage = new OrderHistoryPage(page);
21 |
22 | await loginPage.login(emailInputValue, passwordInputValue);
23 |
24 | await page.goto(slugs.productpage.simpleProductSlug);
25 | await productPage.addSimpleProductToCart(UIReference.productPage.simpleProductTitle, slugs.productpage.simpleProductSlug);
26 | await page.goto(slugs.checkout.checkoutSlug);
27 | const orderNumberLocator = await checkoutPage.placeOrder();
28 | const orderNumberText = await orderNumberLocator.innerText();
29 | const orderNumber = orderNumberText.replace(/\D/g, '');
30 |
31 | await orderHistoryPage.open();
32 | await orderHistoryPage.verifyOrderPresent(orderNumber);
33 | });
34 |
--------------------------------------------------------------------------------
/tests/poms/adminhtml/magentoAdmin.page.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { expect, type Locator, type Page } from '@playwright/test';
4 | import { UIReference, inputValues } from 'config';
5 |
6 | class MagentoAdminPage {
7 | readonly page: Page;
8 | readonly adminLoginEmailField: Locator;
9 | readonly adminLoginPasswordField: Locator;
10 | readonly adminLoginButton: Locator;
11 |
12 | constructor(page: Page) {
13 | this.page = page;
14 | this.adminLoginEmailField = page.getByLabel(UIReference.magentoAdminPage.usernameFieldLabel);
15 | this.adminLoginPasswordField = page.getByLabel(UIReference.magentoAdminPage.passwordFieldLabel);
16 | this.adminLoginButton = page.getByRole('button', {name: UIReference.magentoAdminPage.loginButtonLabel});
17 | }
18 |
19 | async login(username: string, password: string){
20 | if(!process.env.MAGENTO_ADMIN_SLUG) {
21 | throw new Error("MAGENTO_ADMIN_SLUG is not defined in your .env file.");
22 | }
23 |
24 | await this.page.goto(process.env.MAGENTO_ADMIN_SLUG);
25 | await this.page.waitForLoadState('networkidle');
26 | await this.adminLoginEmailField.fill(username);
27 | await this.adminLoginPasswordField.fill(password);
28 | await this.adminLoginButton.click();
29 | await this.page.waitForLoadState('networkidle');
30 | }
31 |
32 | async addCartPriceRule(magentoCouponCode: string){
33 | if(!process.env.MAGENTO_COUPON_CODE_CHROMIUM || !process.env.MAGENTO_COUPON_CODE_FIREFOX || !process.env.MAGENTO_COUPON_CODE_WEBKIT) {
34 | throw new Error("MAGENTO_COUPON_CODE_CHROMIUM, MAGENTO_COUPON_CODE_FIREFOX or MAGENTO_COUPON_CODE_WEBKIT is not defined in your .env file.");
35 | }
36 |
37 | await this.page.getByRole('link', {name: UIReference.magentoAdminPage.navigation.marketingButtonLabel}).click();
38 | await this.page.waitForLoadState('networkidle');
39 | //await this.page.getByRole('link', {name: UIReference.magentoAdminPage.subNavigation.cartPriceRulesButtonLabel}).waitFor();
40 | await expect(this.page.getByRole('link', {name: UIReference.magentoAdminPage.subNavigation.cartPriceRulesButtonLabel})).toBeVisible();
41 | await this.page.getByRole('link', {name: UIReference.magentoAdminPage.subNavigation.cartPriceRulesButtonLabel}).click();
42 | await this.page.waitForLoadState('networkidle');
43 | await this.page.getByRole('button', {name: UIReference.cartPriceRulesPage.addCartPriceRuleButtonLabel}).click();
44 | await this.page.getByLabel(UIReference.cartPriceRulesPage.ruleNameFieldLabel).fill(inputValues.coupon.couponCodeRuleName);
45 |
46 | const websiteSelector = this.page.getByLabel(UIReference.cartPriceRulesPage.websitesSelectLabel);
47 | await websiteSelector.evaluate(select => {
48 | for (const option of select.options) {
49 | option.selected = true;
50 | }
51 | select.dispatchEvent(new Event('change'));
52 | });
53 |
54 | const customerGroupsSelector = this.page.getByLabel(UIReference.cartPriceRulesPage.customerGroupsSelectLabel, { exact: true });
55 | await customerGroupsSelector.evaluate(select => {
56 | for (const option of select.options) {
57 | option.selected = true;
58 | }
59 | select.dispatchEvent(new Event('change'));
60 | });
61 |
62 | await this.page.locator(UIReference.cartPriceRulesPage.couponTypeSelectField).selectOption({ label: inputValues.coupon.couponType });
63 | await this.page.getByLabel(UIReference.cartPriceRulesPage.couponCodeFieldLabel).fill(magentoCouponCode);
64 |
65 | await this.page.getByText(UIReference.cartPriceRulesPage.actionsSubtitleLabel, { exact: true }).click();
66 | await this.page.getByLabel(UIReference.cartPriceRulesPage.discountAmountFieldLabel).fill('10');
67 |
68 | await this.page.getByRole('button', { name: 'Save', exact: true }).click();
69 | }
70 |
71 | async enableMultipleAdminLogins() {
72 | await this.page.waitForLoadState('networkidle');
73 | await this.page.getByRole('link', { name: UIReference.magentoAdminPage.navigation.storesButtonLabel }).click();
74 | await this.page.getByRole('link', { name: UIReference.magentoAdminPage.subNavigation.configurationButtonLabel }).first().click();
75 | await this.page.getByRole('tab', { name: UIReference.configurationPage.advancedTabLabel }).click();
76 | await this.page.getByRole('link', { name: UIReference.configurationPage.advancedAdministrationTabLabel, exact: true }).click();
77 |
78 | if (!await this.page.locator(UIReference.configurationPage.allowMultipleLoginsSystemCheckbox).isVisible()) {
79 | await this.page.getByRole('link', { name: UIReference.configurationPage.securitySectionLabel }).click();
80 | }
81 |
82 | await this.page.locator(UIReference.configurationPage.allowMultipleLoginsSystemCheckbox).uncheck();
83 | await this.page.locator(UIReference.configurationPage.allowMultipleLoginsSelectField).selectOption({ label: inputValues.adminLogins.allowMultipleLogins });
84 | await this.page.getByRole('button', { name: UIReference.configurationPage.saveConfigButtonLabel }).click();
85 | }
86 |
87 | async disableLoginCaptcha() {
88 | await this.page.waitForLoadState('networkidle');
89 | await this.page.getByRole('link', { name: UIReference.magentoAdminPage.navigation.storesButtonLabel }).click();
90 | await this.page.getByRole('link', { name: UIReference.magentoAdminPage.subNavigation.configurationButtonLabel }).first().click();
91 | await this.page.waitForLoadState('networkidle');
92 | await this.page.getByRole('tab', { name: UIReference.configurationPage.customersTabLabel }).click();
93 | await this.page.getByRole('link', { name: UIReference.configurationPage.customerConfigurationTabLabel }).click();
94 | await this.page.waitForLoadState('networkidle');
95 |
96 | if (!await this.page.locator(UIReference.configurationPage.captchaSettingSystemCheckbox).isVisible()) {
97 | // await this.page.getByRole('link', { name: new RegExp(UIReference.configurationPage.captchaSectionLabel) }).click();
98 | await this.page.getByRole('link', { name: UIReference.configurationPage.captchaSectionLabel }).click();
99 | }
100 |
101 | await this.page.locator(UIReference.configurationPage.captchaSettingSystemCheckbox).uncheck();
102 | await this.page.locator(UIReference.configurationPage.captchaSettingSelectField).selectOption({ label: inputValues.captcha.captchaDisabled });
103 | await this.page.getByRole('button', { name: UIReference.configurationPage.saveConfigButtonLabel }).click();
104 | await this.page.waitForLoadState('networkidle');
105 | }
106 | }
107 |
108 | export default MagentoAdminPage;
109 |
--------------------------------------------------------------------------------
/tests/poms/frontend/account.page.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { expect, type Locator, type Page } from '@playwright/test';
4 | import { faker } from '@faker-js/faker';
5 | import { UIReference, outcomeMarker, inputValues } from 'config';
6 | import LoginPage from './login.page';
7 |
8 | class AccountPage {
9 | readonly page: Page;
10 | readonly accountDashboardTitle: Locator;
11 | readonly firstNameField: Locator;
12 | readonly lastNameField: Locator;
13 | readonly phoneNumberField: Locator;
14 | readonly loginPage: LoginPage;
15 | readonly streetAddressField: Locator;
16 | readonly zipCodeField: Locator;
17 | readonly cityField: Locator;
18 | readonly countrySelectorField: Locator;
19 | readonly stateSelectorField: Locator;
20 | readonly stateInputField: Locator;
21 | readonly saveAddressButton: Locator;
22 | readonly addNewAddressButton: Locator;
23 | readonly deleteAddressButton: Locator;
24 | readonly editAddressButton: Locator;
25 | readonly changePasswordCheck: Locator;
26 | readonly changeEmailCheck: Locator;
27 | readonly currentPasswordField: Locator;
28 | readonly newPasswordField: Locator;
29 | readonly confirmNewPasswordField: Locator;
30 | readonly genericSaveButton: Locator;
31 | readonly accountCreationFirstNameField: Locator;
32 | readonly accountCreationLastNameField: Locator;
33 | readonly accountCreationEmailField: Locator;
34 | readonly accountCreationPasswordField: Locator;
35 | readonly accountCreationPasswordRepeatField: Locator;
36 | readonly accountCreationConfirmButton: Locator;
37 | readonly accountInformationField: Locator;
38 |
39 |
40 | constructor(page: Page){
41 | this.page = page;
42 | this.loginPage = new LoginPage(page);
43 | this.accountDashboardTitle = page.getByRole('heading', { name: UIReference.accountDashboard.accountDashboardTitleLabel });
44 | this.firstNameField = page.getByLabel(UIReference.personalInformation.firstNameLabel);
45 | this.lastNameField = page.getByLabel(UIReference.personalInformation.lastNameLabel);
46 | this.phoneNumberField = page.getByLabel(UIReference.newAddress.phoneNumberLabel);
47 | this.streetAddressField = page.getByLabel(UIReference.newAddress.streetAddressLabel, {exact:true});
48 | this.zipCodeField = page.getByLabel(UIReference.newAddress.zipCodeLabel);
49 | this.cityField = page.getByLabel(UIReference.newAddress.cityNameLabel);
50 | this.countrySelectorField = page.getByLabel(UIReference.newAddress.countryLabel);
51 | this.stateInputField = page.getByLabel(UIReference.newAddress.provinceSelectLabel);
52 | this.stateSelectorField = this.stateInputField.filter({hasText: UIReference.newAddress.provinceSelectFilterLabel});
53 | this.saveAddressButton = page.getByRole('button',{name: UIReference.newAddress.saveAdressButton});
54 |
55 | // Account Information elements
56 | this.changePasswordCheck = page.getByRole('checkbox', {name: UIReference.personalInformation.changePasswordCheckLabel});
57 | this.changeEmailCheck = page.getByRole('checkbox', {name: UIReference.personalInformation.changeEmailCheckLabel});
58 | this.currentPasswordField = page.getByLabel(UIReference.credentials.currentPasswordFieldLabel);
59 | this.newPasswordField = page.getByLabel(UIReference.credentials.newPasswordFieldLabel, {exact:true});
60 | this.confirmNewPasswordField = page.getByLabel(UIReference.credentials.newPasswordConfirmFieldLabel);
61 | this.genericSaveButton = page.getByRole('button', { name: UIReference.general.genericSaveButtonLabel });
62 |
63 | // Account Creation elements
64 | this.accountCreationFirstNameField = page.getByLabel(UIReference.personalInformation.firstNameLabel);
65 | this.accountCreationLastNameField = page.getByLabel(UIReference.personalInformation.lastNameLabel);
66 | this.accountCreationEmailField = page.getByLabel(UIReference.credentials.emailFieldLabel, { exact: true});
67 | this.accountCreationPasswordField = page.getByLabel(UIReference.credentials.passwordFieldLabel, { exact: true });
68 | this.accountCreationPasswordRepeatField = page.getByLabel(UIReference.credentials.passwordConfirmFieldLabel);
69 | this.accountCreationConfirmButton = page.getByRole('button', {name: UIReference.accountCreation.createAccountButtonLabel});
70 |
71 | this.accountInformationField = page.locator(UIReference.accountDashboard.accountInformationFieldLocator).first();
72 |
73 | // Address Book elements
74 | this.addNewAddressButton = page.getByRole('button',{name: UIReference.accountDashboard.addAddressButtonLabel});
75 | this.deleteAddressButton = page.getByRole('link', {name: UIReference.accountDashboard.addressDeleteIconButton}).first();
76 | this.editAddressButton = page.getByRole('link', {name: UIReference.accountDashboard.editAddressIconButton}).first();
77 | }
78 |
79 | async addNewAddress(){
80 | let addressAddedNotification = outcomeMarker.address.newAddressAddedNotifcation;
81 | let streetName = faker.location.streetAddress();
82 |
83 | // Name should be filled in automatically.
84 | await expect(this.firstNameField).not.toBeEmpty();
85 | await expect(this.lastNameField).not.toBeEmpty();
86 |
87 | await this.phoneNumberField.fill(faker.phone.number());
88 | await this.streetAddressField.fill(streetName);
89 | await this.zipCodeField.fill(faker.location.zipCode());
90 | await this.cityField.fill(faker.location.city());
91 |
92 | const country = faker.helpers.arrayElement(inputValues.addressCountries);
93 | await this.countrySelectorField.selectOption({ label: country });
94 |
95 | const stateValue = faker.location.state();
96 | if (await this.stateSelectorField.count()) {
97 | await this.stateSelectorField.selectOption(stateValue);
98 | } else {
99 | await this.stateInputField.fill(stateValue);
100 | }
101 |
102 | await this.saveAddressButton.click();
103 | await this.page.waitForLoadState();
104 |
105 | await expect.soft(this.page.getByText(addressAddedNotification)).toBeVisible();
106 | await expect(this.page.getByText(streetName).last()).toBeVisible();
107 | }
108 |
109 |
110 | async editExistingAddress(){
111 | // the notification for a modified address is the same as the notification for a new address.
112 | let addressModifiedNotification = outcomeMarker.address.newAddressAddedNotifcation;
113 | let streetName = faker.location.streetAddress();
114 |
115 | await this.editAddressButton.click();
116 |
117 | // Name should be filled in automatically, but editable.
118 | await expect(this.firstNameField).not.toBeEmpty();
119 | await expect(this.lastNameField).not.toBeEmpty();
120 |
121 | await this.firstNameField.fill(faker.person.firstName());
122 | await this.lastNameField.fill(faker.person.lastName());
123 | await this.streetAddressField.fill(streetName);
124 | await this.zipCodeField.fill(faker.location.zipCode());
125 | await this.cityField.fill(faker.location.city());
126 | const stateEdit = faker.location.state();
127 | if (await this.stateSelectorField.count()) {
128 | await this.stateSelectorField.selectOption(stateEdit);
129 | } else {
130 | await this.stateInputField.fill(stateEdit);
131 | }
132 |
133 | await this.saveAddressButton.click();
134 | await this.page.waitForLoadState();
135 |
136 | await expect.soft(this.page.getByText(addressModifiedNotification)).toBeVisible();
137 | await expect(this.page.getByText(streetName).last()).toBeVisible();
138 | }
139 |
140 |
141 | async deleteFirstAddressFromAddressBook(){
142 | let addressDeletedNotification = outcomeMarker.address.addressDeletedNotification;
143 | let addressBookSection = this.page.locator(UIReference.accountDashboard.addressBookArea);
144 |
145 | // Dialog function to click confirm
146 | this.page.on('dialog', async (dialog) => {
147 | if (dialog.type() === 'confirm') {
148 | await dialog.accept();
149 | }
150 | });
151 |
152 | // Grab addresses from the address book, split the string and grab the address to be deleted.
153 | let addressBookArray = await addressBookSection.allInnerTexts();
154 | let arraySplit = addressBookArray[0].split('\n');
155 | let addressToBeDeleted = arraySplit[7];
156 |
157 | await this.deleteAddressButton.click();
158 | await this.page.waitForLoadState();
159 |
160 | await expect(this.page.getByText(addressDeletedNotification)).toBeVisible();
161 | await expect(addressBookSection, `${addressToBeDeleted} should not be visible`).not.toContainText(addressToBeDeleted);
162 | }
163 |
164 | async updatePassword(currentPassword:string, newPassword: string){
165 | let passwordUpdatedNotification = outcomeMarker.account.changedPasswordNotificationText;
166 | await this.changePasswordCheck.check();
167 |
168 | await this.currentPasswordField.fill(currentPassword);
169 | await this.newPasswordField.fill(newPassword);
170 | await this.confirmNewPasswordField.fill(newPassword);
171 |
172 | await this.genericSaveButton.click();
173 | await this.page.waitForLoadState();
174 |
175 | await expect(this.page.getByText(passwordUpdatedNotification)).toBeVisible();
176 | }
177 |
178 | async updateEmail(currentPassword: string, newEmail: string) {
179 | let accountUpdatedNotification = outcomeMarker.account.changedPasswordNotificationText;
180 |
181 | await this.changeEmailCheck.check();
182 | await this.accountCreationEmailField.fill(newEmail);
183 | await this.currentPasswordField.fill(currentPassword);
184 | await this.genericSaveButton.click();
185 | await this.page.waitForLoadState();
186 | await this.loginPage.login(newEmail, currentPassword);
187 |
188 | await expect(this.accountInformationField, `Account information should contain email: ${newEmail}`).toContainText(newEmail);
189 | }
190 |
191 | async deleteAllAddresses() {
192 | let addressDeletedNotification = outcomeMarker.address.addressDeletedNotification;
193 |
194 | this.page.on('dialog', async (dialog) => {
195 | if (dialog.type() === 'confirm') {
196 | await dialog.accept();
197 | }
198 | });
199 |
200 | while (await this.deleteAddressButton.isVisible()) {
201 | await this.deleteAddressButton.click();
202 | await this.page.waitForLoadState();
203 |
204 | await expect.soft(this.page.getByText(addressDeletedNotification)).toBeVisible();
205 | }
206 | }
207 | }
208 |
209 | export default AccountPage;
210 |
--------------------------------------------------------------------------------
/tests/poms/frontend/cart.page.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { expect, type Locator, type Page } from '@playwright/test';
4 | import { UIReference, outcomeMarker } from 'config';
5 |
6 | class CartPage {
7 | readonly page: Page;
8 | readonly showDiscountButton: Locator;
9 | productQuantityInCheckout: string | undefined;
10 | productPriceInCheckout: string | undefined;
11 |
12 | constructor(page: Page) {
13 | this.page = page;
14 | this.showDiscountButton = this.page.getByRole('button', { name: UIReference.cart.showDiscountFormButtonLabel });
15 | }
16 |
17 | async changeProductQuantity(amount: string){
18 | const productRow = this.page.getByRole('row', {name: UIReference.productPage.simpleProductTitle});
19 | let currentQuantity = await productRow.getByLabel(UIReference.cart.cartQuantityLabel).inputValue();
20 |
21 | if(currentQuantity == amount){
22 | // quantity is the same as amount, therefore we change amount to ensure test can continue.
23 | amount = '3';
24 | }
25 |
26 | await productRow.getByLabel(UIReference.cart.cartQuantityLabel).fill(amount);
27 | let subTotalBeforeUpdate = await productRow.getByText(UIReference.general.genericPriceSymbol).last().innerText();
28 |
29 | await this.page.getByRole('button', { name: UIReference.cart.updateShoppingCartButtonLabel }).click();
30 | await this.page.reload();
31 |
32 | currentQuantity = await productRow.getByLabel(UIReference.cart.cartQuantityLabel).inputValue();
33 |
34 | // Last $ to get the Subtotal
35 | let subTotalAfterUpdate = await productRow.getByText(UIReference.general.genericPriceSymbol).last().innerText();
36 |
37 | // Assertions: subtotals are different, and quantity field is still the new amount.
38 | expect(subTotalAfterUpdate, `Subtotals should not be the same`).not.toEqual(subTotalBeforeUpdate);
39 | expect(currentQuantity, `quantity should be the new value`).toEqual(amount);
40 | }
41 |
42 | // ==============================================
43 | // Product-related methods
44 | // ==============================================
45 |
46 | async removeProduct(productTitle: string){
47 | let removeButton = this.page.getByLabel(`${UIReference.general.removeLabel} ${productTitle}`);
48 | await removeButton.click();
49 | await this.page.waitForLoadState();
50 | await expect(removeButton,`Button to remove specified product is not visible in the cart`).toBeHidden();
51 |
52 | // Expect product to no longer be visible in the cart
53 | await expect (this.page.getByRole('cell', { name: productTitle }), `Product is not visible in cart`).toBeHidden();
54 | }
55 |
56 | // ==============================================
57 | // Discount-related methods
58 | // ==============================================
59 | async applyDiscountCode(code: string){
60 | if(await this.page.getByPlaceholder(UIReference.cart.discountInputFieldLabel).isHidden()){
61 | // discount field is not open.
62 | await this.showDiscountButton.click();
63 | }
64 |
65 | let applyDiscoundButton = this.page.getByRole('button', {name: UIReference.cart.applyDiscountButtonLabel, exact:true});
66 | let discountField = this.page.getByPlaceholder(UIReference.cart.discountInputFieldLabel);
67 | await discountField.fill(code);
68 | await applyDiscoundButton.click();
69 | await this.page.waitForLoadState();
70 |
71 | await expect.soft(this.page.getByText(`${outcomeMarker.cart.discountAppliedNotification} "${code}"`),`Notification that discount code ${code} has been applied`).toBeVisible();
72 | await expect(this.page.getByText(outcomeMarker.cart.priceReducedSymbols),`'- $' should be visible on the page`).toBeVisible();
73 | //Close message to prevent difficulties with other tests.
74 | await this.page.getByLabel(UIReference.general.closeMessageLabel).click();
75 | }
76 |
77 | async removeDiscountCode(){
78 | if(await this.page.getByPlaceholder(UIReference.cart.discountInputFieldLabel).isHidden()){
79 | // discount field is not open.
80 | await this.showDiscountButton.click();
81 | }
82 |
83 | let cancelCouponButton = this.page.getByRole('button', {name: UIReference.cart.cancelCouponButtonLabel});
84 | await cancelCouponButton.click();
85 | await this.page.waitForLoadState();
86 |
87 | await expect.soft(this.page.getByText(outcomeMarker.cart.discountRemovedNotification),`Notification should be visible`).toBeVisible();
88 | await expect(this.page.getByText(outcomeMarker.cart.priceReducedSymbols),`'- $' should not be on the page`).toBeHidden();
89 | }
90 |
91 | async enterWrongCouponCode(code: string){
92 | if(await this.page.getByPlaceholder(UIReference.cart.discountInputFieldLabel).isHidden()){
93 | // discount field is not open.
94 | await this.showDiscountButton.click();
95 | }
96 |
97 | let applyDiscoundButton = this.page.getByRole('button', {name: UIReference.cart.applyDiscountButtonLabel, exact:true});
98 | let discountField = this.page.getByPlaceholder(UIReference.cart.discountInputFieldLabel);
99 | await discountField.fill(code);
100 | await applyDiscoundButton.click();
101 | await this.page.waitForLoadState();
102 |
103 | let incorrectNotification = `${outcomeMarker.cart.incorrectCouponCodeNotificationOne} "${code}" ${outcomeMarker.cart.incorrectCouponCodeNotificationTwo}`;
104 |
105 | //Assertions: notification that code was incorrect & discount code field is still editable
106 | await expect.soft(this.page.getByText(incorrectNotification), `Code should not work`).toBeVisible();
107 | await expect(discountField).toBeEditable();
108 | }
109 |
110 |
111 | // ==============================================
112 | // Additional methods
113 | // ==============================================
114 |
115 | async getCheckoutValues(productName:string, pricePDP:string, amountPDP:string){
116 | // Open minicart based on amount of products in cart
117 | let cartItemAmount = await this.page.locator(UIReference.miniCart.minicartAmountBubbleLocator).count();
118 | if(cartItemAmount == 1) {
119 | await this.page.getByLabel(`${UIReference.checkout.openCartButtonLabel} ${cartItemAmount} ${UIReference.checkout.openCartButtonLabelCont}`).click();
120 | } else {
121 | await this.page.getByLabel(`${UIReference.checkout.openCartButtonLabel} ${cartItemAmount} ${UIReference.checkout.openCartButtonLabelContMultiple}`).click();
122 | }
123 |
124 | // Get values from checkout page
125 | let productInCheckout = this.page.locator(UIReference.checkout.cartDetailsLocator).filter({ hasText: productName }).nth(1);
126 | this.productPriceInCheckout = await productInCheckout.getByText(UIReference.general.genericPriceSymbol).innerText();
127 | this.productPriceInCheckout = this.productPriceInCheckout.trim();
128 | let productImage = this.page.locator(UIReference.checkout.cartDetailsLocator)
129 | .filter({ has: this.page.getByRole('img', { name: productName })});
130 | this.productQuantityInCheckout = await productImage.locator('> span').innerText();
131 |
132 | return [this.productPriceInCheckout, this.productQuantityInCheckout];
133 | }
134 |
135 | async calculateProductPricesAndCompare(pricePDP: string, amountPDP:string, priceCheckout:string, amountCheckout:string){
136 | // perform magic to calculate price * amount and mold it into the correct form again
137 | pricePDP = pricePDP.replace(UIReference.general.genericPriceSymbol,'');
138 | let pricePDPInt = Number(pricePDP);
139 | let quantityPDPInt = +amountPDP;
140 | let calculatedPricePDP = `${UIReference.general.genericPriceSymbol}` + (pricePDPInt * quantityPDPInt).toFixed(2);
141 |
142 | expect(amountPDP,`Amount on PDP (${amountPDP}) equals amount in checkout (${amountCheckout})`).toEqual(amountCheckout);
143 | expect(calculatedPricePDP, `Price * qty on PDP (${calculatedPricePDP}) equals price * qty in checkout (${priceCheckout})`).toEqual(priceCheckout);
144 | }
145 | }
146 |
147 | export default CartPage;
148 |
--------------------------------------------------------------------------------
/tests/poms/frontend/category.page.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { expect, type Locator, type Page } from '@playwright/test';
4 | import { UIReference, slugs } from 'config';
5 |
6 | class CategoryPage {
7 | readonly page:Page;
8 | categoryPageTitle: Locator;
9 |
10 | constructor(page: Page) {
11 | this.page = page;
12 | this.categoryPageTitle = this.page.getByRole('heading', { name: UIReference.categoryPage.categoryPageTitleText });
13 | }
14 |
15 | async goToCategoryPage(){
16 | await this.page.goto(slugs.categoryPage.categorySlug);
17 | // Wait for the first filter option to be visible
18 | const firstFilterOption = this.page.locator(UIReference.categoryPage.firstFilterOptionLocator);
19 | await firstFilterOption.waitFor();
20 |
21 | this.page.waitForLoadState();
22 | await expect(this.categoryPageTitle).toBeVisible();
23 | }
24 |
25 | async filterOnSize(browser:string){
26 | const sizeFilterButton = this.page.getByRole('button', {name: UIReference.categoryPage.sizeFilterButtonLabel});
27 | const sizeLButton = this.page.locator(UIReference.categoryPage.sizeLButtonLocator);
28 | const removeActiveFilterLink = this.page.getByRole('link', {name: UIReference.categoryPage.removeActiveFilterButtonLabel}).first();
29 | const amountOfItemsBeforeFilter = parseInt(await this.page.locator(UIReference.categoryPage.itemsOnPageAmountLocator).last().innerText());
30 |
31 |
32 | /**
33 | * BROWSER_WORKAROUND
34 | * The size filter seems to auto-close in Firefox and Webkit.
35 | * Therefore, we open it manually.
36 | */
37 | if(browser !== 'chromium'){
38 | await sizeFilterButton.click();
39 | }
40 |
41 | await sizeLButton.click();
42 |
43 | const sizeFilterRegex = new RegExp(`\\?size=L$`);
44 | await this.page.waitForURL(sizeFilterRegex);
45 |
46 | const amountOfItemsAfterFilter = parseInt(await this.page.locator(UIReference.categoryPage.itemsOnPageAmountLocator).last().innerText());
47 | await expect(removeActiveFilterLink, 'Trash button to remove filter is visible').toBeVisible();
48 | expect(amountOfItemsAfterFilter, `Amount of items shown with filter (${amountOfItemsAfterFilter}) is less than without (${amountOfItemsBeforeFilter})`).toBeLessThan(amountOfItemsBeforeFilter);
49 |
50 | }
51 |
52 | async sortProducts(attribute:string){
53 | const sortButton = this.page.getByLabel(UIReference.categoryPage.sortByButtonLabel);
54 | await sortButton.selectOption(attribute);
55 | const sortRegex = new RegExp(`\\?product_list_order=${attribute}$`);
56 | await this.page.waitForURL(sortRegex);
57 |
58 | const selectedValue = await this.page.$eval(UIReference.categoryPage.sortByButtonLocator, sel => sel.value);
59 |
60 | // sortButton should now display attribute
61 | expect(selectedValue, `Sort button should now display ${attribute}`).toEqual(attribute);
62 | // URL now has ?product_list_order=${attribute}
63 | expect(this.page.url(), `URL should contain ?product_list_order=${attribute}`).toContain(`product_list_order=${attribute}`);
64 | }
65 |
66 |
67 | async showMoreProducts(){
68 | const itemsPerPageButton = this.page.getByLabel(UIReference.categoryPage.itemsPerPageButtonLabel);
69 | const productGrid = this.page.locator(UIReference.categoryPage.productGridLocator);
70 |
71 | await itemsPerPageButton.selectOption('36');
72 | const itemsRegex = /\?product_list_limit=36$/;
73 | await this.page.waitForURL(itemsRegex);
74 |
75 | const amountOfItems = await productGrid.locator('li').count();
76 |
77 | expect(this.page.url(), `URL should contain ?product_list_limit=36`).toContain(`?product_list_limit=36`);
78 | expect(amountOfItems, `Amount of items on the page should be 36`).toBe(36);
79 | }
80 |
81 | async switchView(){
82 | const viewSwitcher = this.page.getByLabel(UIReference.categoryPage.viewSwitchLabel, {exact: true}).locator(UIReference.categoryPage.activeViewLocator);
83 | const activeView = await viewSwitcher.getAttribute('title');
84 |
85 | if(activeView == 'Grid'){
86 | await this.page.getByLabel(UIReference.categoryPage.viewListLabel).click();
87 | } else {
88 | await this.page.getByLabel(UIReference.categoryPage.viewGridLabel).click();
89 | }
90 |
91 | const viewRegex = /\?product_list_mode=list$/;
92 | await this.page.waitForURL(viewRegex);
93 |
94 | const newActiveView = await viewSwitcher.getAttribute('title');
95 | expect(newActiveView, `View (now ${newActiveView}) should be switched (old: ${activeView})`).not.toEqual(activeView);
96 | expect(this.page.url(),`URL should contain ?product_list_mode=${newActiveView?.toLowerCase()}`).toContain(`?product_list_mode=${newActiveView?.toLowerCase()}`);
97 | }
98 | }
99 |
100 | export default CategoryPage;
101 |
--------------------------------------------------------------------------------
/tests/poms/frontend/checkout.page.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { expect, type Locator, type Page } from '@playwright/test';
4 | import { faker } from '@faker-js/faker';
5 | import { UIReference, outcomeMarker, slugs, inputValues } from 'config';
6 |
7 | import MagewireUtils from '../../utils/magewire.utils';
8 |
9 | class CheckoutPage extends MagewireUtils {
10 |
11 | readonly shippingMethodOptionFixed: Locator;
12 | readonly paymentMethodOptionCheck: Locator;
13 | readonly showDiscountFormButton: Locator;
14 | readonly placeOrderButton: Locator;
15 | readonly continueShoppingButton: Locator;
16 | readonly subtotalElement: Locator;
17 | readonly shippingElement: Locator;
18 | readonly taxElement: Locator;
19 | readonly grandTotalElement: Locator;
20 | readonly paymentMethodOptionCreditCard: Locator;
21 | readonly paymentMethodOptionPaypal: Locator;
22 | readonly creditCardNumberField: Locator;
23 | readonly creditCardExpiryField: Locator;
24 | readonly creditCardCVVField: Locator;
25 | readonly creditCardNameField: Locator;
26 |
27 | constructor(page: Page){
28 | super(page);
29 | this.shippingMethodOptionFixed = this.page.getByLabel(UIReference.checkout.shippingMethodFixedLabel);
30 | this.paymentMethodOptionCheck = this.page.getByLabel(UIReference.checkout.paymentOptionCheckLabel);
31 | this.showDiscountFormButton = this.page.getByRole('button', {name: UIReference.checkout.openDiscountFormLabel});
32 | this.placeOrderButton = this.page.getByRole('button', { name: UIReference.checkout.placeOrderButtonLabel });
33 | this.continueShoppingButton = this.page.getByRole('link', { name: UIReference.checkout.continueShoppingLabel });
34 | this.subtotalElement = page.getByText('Subtotal $');
35 | this.shippingElement = page.getByText('Shipping & Handling (Flat Rate - Fixed) $');
36 | this.taxElement = page.getByText('Tax $');
37 | this.grandTotalElement = page.getByText('Grand Total $');
38 | this.paymentMethodOptionCreditCard = this.page.getByLabel(UIReference.checkout.paymentOptionCreditCardLabel);
39 | this.paymentMethodOptionPaypal = this.page.getByLabel(UIReference.checkout.paymentOptionPaypalLabel);
40 | this.creditCardNumberField = this.page.getByLabel(UIReference.checkout.creditCardNumberLabel);
41 | this.creditCardExpiryField = this.page.getByLabel(UIReference.checkout.creditCardExpiryLabel);
42 | this.creditCardCVVField = this.page.getByLabel(UIReference.checkout.creditCardCVVLabel);
43 | this.creditCardNameField = this.page.getByLabel(UIReference.checkout.creditCardNameLabel);
44 | }
45 |
46 | // ==============================================
47 | // Order-related methods
48 | // ==============================================
49 |
50 | async placeOrder(){
51 | let orderPlacedNotification = outcomeMarker.checkout.orderPlacedNotification;
52 |
53 | // If we're not already on the checkout page, go there
54 | if (!this.page.url().includes(slugs.checkout.checkoutSlug)) {
55 | await this.page.goto(slugs.checkout.checkoutSlug);
56 | }
57 |
58 | // If shipping method is not selected, select it
59 | if (!(await this.shippingMethodOptionFixed.isChecked())) {
60 | await this.shippingMethodOptionFixed.check();
61 | await this.waitForMagewireRequests();
62 | }
63 |
64 | await this.paymentMethodOptionCheck.check();
65 | await this.waitForMagewireRequests();
66 |
67 | await this.placeOrderButton.click();
68 | await this.waitForMagewireRequests();
69 |
70 | await expect.soft(this.page.getByText(orderPlacedNotification)).toBeVisible();
71 | let orderNumber = await this.page.locator('p').filter({ hasText: outcomeMarker.checkout.orderPlacedNumberText });
72 |
73 | await expect(this.continueShoppingButton, `${outcomeMarker.checkout.orderPlacedNumberText} ${orderNumber}`).toBeVisible();
74 | return orderNumber;
75 | }
76 |
77 |
78 | // ==============================================
79 | // Discount-related methods
80 | // ==============================================
81 |
82 | async applyDiscountCodeCheckout(code: string){
83 | if(await this.page.getByPlaceholder(UIReference.cart.discountInputFieldLabel).isHidden()){
84 | // discount field is not open.
85 | await this.showDiscountFormButton.click();
86 | await this.waitForMagewireRequests();
87 | }
88 |
89 | if(await this.page.getByText(outcomeMarker.cart.priceReducedSymbols).isVisible()){
90 | // discount is already active.
91 | let cancelCouponButton = this.page.getByRole('button', { name: UIReference.checkout.cancelDiscountButtonLabel });
92 | await cancelCouponButton.click();
93 | await this.waitForMagewireRequests();
94 | }
95 |
96 | let applyCouponCheckoutButton = this.page.getByRole('button', { name: UIReference.checkout.applyDiscountButtonLabel });
97 | let checkoutDiscountField = this.page.getByPlaceholder(UIReference.checkout.discountInputFieldLabel);
98 |
99 | await checkoutDiscountField.fill(code);
100 | await applyCouponCheckoutButton.click();
101 | await this.waitForMagewireRequests();
102 |
103 | await expect.soft(this.page.getByText(`${outcomeMarker.checkout.couponAppliedNotification}`),`Notification that discount code ${code} has been applied`).toBeVisible({timeout: 30000});
104 | await expect(this.page.getByText(outcomeMarker.checkout.checkoutPriceReducedSymbol),`'-$' should be visible on the page`).toBeVisible();
105 | }
106 |
107 | async enterWrongCouponCode(code: string){
108 | if(await this.page.getByPlaceholder(UIReference.cart.discountInputFieldLabel).isHidden()){
109 | // discount field is not open.
110 | await this.showDiscountFormButton.click();
111 | await this.waitForMagewireRequests();
112 | }
113 |
114 | let applyCouponCheckoutButton = this.page.getByRole('button', { name: UIReference.checkout.applyDiscountButtonLabel });
115 | let checkoutDiscountField = this.page.getByPlaceholder(UIReference.checkout.discountInputFieldLabel);
116 | await checkoutDiscountField.fill(code);
117 | await applyCouponCheckoutButton.click();
118 | await this.waitForMagewireRequests();
119 |
120 | await expect.soft(this.page.getByText(outcomeMarker.checkout.incorrectDiscountNotification), `Code should not work`).toBeVisible();
121 | await expect(checkoutDiscountField).toBeEditable();
122 | }
123 |
124 | async removeDiscountCode(){
125 | if(await this.page.getByPlaceholder(UIReference.cart.discountInputFieldLabel).isHidden()){
126 | // discount field is not open.
127 | await this.showDiscountFormButton.click();
128 | await this.waitForMagewireRequests();
129 | }
130 |
131 | let cancelCouponButton = this.page.getByRole('button', {name: UIReference.cart.cancelCouponButtonLabel});
132 | await cancelCouponButton.click();
133 | await this.waitForMagewireRequests();
134 |
135 | await expect.soft(this.page.getByText(outcomeMarker.checkout.couponRemovedNotification),`Notification should be visible`).toBeVisible();
136 | await expect(this.page.getByText(outcomeMarker.checkout.checkoutPriceReducedSymbol),`'-$' should not be on the page`).toBeHidden();
137 |
138 | let checkoutDiscountField = this.page.getByPlaceholder(UIReference.checkout.discountInputFieldLabel);
139 | await expect(checkoutDiscountField).toBeEditable();
140 | }
141 |
142 | // ==============================================
143 | // Price summary methods
144 | // ==============================================
145 |
146 | async getPriceValue(element: Locator): Promise {
147 | const priceText = await element.innerText();
148 | // Extract just the price part after the $ symbol
149 | const match = priceText.match(/\$\s*([\d.]+)/);
150 | return match ? parseFloat(match[1]) : 0;
151 | }
152 |
153 | async verifyPriceCalculations() {
154 | const subtotal = await this.getPriceValue(this.subtotalElement);
155 | const shipping = await this.getPriceValue(this.shippingElement);
156 | const tax = await this.getPriceValue(this.taxElement);
157 | const grandTotal = await this.getPriceValue(this.grandTotalElement);
158 |
159 | const calculatedTotal = +(subtotal + shipping + tax).toFixed(2);
160 |
161 | expect(subtotal, `Subtotal (${subtotal}) should be greater than 0`).toBeGreaterThan(0);
162 | expect(shipping, `Shipping cost (${shipping}) should be greater than 0`).toBeGreaterThan(0);
163 | // Enable when tax settings are set.
164 | //expect(tax, `Tax (${tax}) should be greater than 0`).toBeGreaterThan(0);
165 | expect(grandTotal, `Grand total (${grandTotal}) should equal calculated total (${calculatedTotal})`).toBe(calculatedTotal);
166 | }
167 |
168 | async selectPaymentMethod(method: 'check' | 'creditcard' | 'paypal'): Promise {
169 | switch(method) {
170 | case 'check':
171 | await this.paymentMethodOptionCheck.check();
172 | break;
173 | case 'creditcard':
174 | await this.paymentMethodOptionCreditCard.check();
175 | // Fill credit card details
176 | await this.creditCardNumberField.fill(inputValues.payment?.creditCard?.number || '4111111111111111');
177 | await this.creditCardExpiryField.fill(inputValues.payment?.creditCard?.expiry || '12/25');
178 | await this.creditCardCVVField.fill(inputValues.payment?.creditCard?.cvv || '123');
179 | await this.creditCardNameField.fill(inputValues.payment?.creditCard?.name || 'Test User');
180 | break;
181 | case 'paypal':
182 | await this.paymentMethodOptionPaypal.check();
183 | break;
184 | }
185 |
186 | await this.waitForMagewireRequests();
187 | }
188 |
189 | async fillShippingAddress() {
190 | // Fill required shipping address fields
191 | await this.page.getByLabel(UIReference.credentials.emailCheckoutFieldLabel, { exact: true }).fill(faker.internet.email());
192 | await this.page.getByLabel(UIReference.personalInformation.firstNameLabel).fill(faker.person.firstName());
193 | await this.page.getByLabel(UIReference.personalInformation.lastNameLabel).fill(faker.person.lastName());
194 | await this.page.getByLabel(UIReference.newAddress.streetAddressLabel).first().fill(faker.location.streetAddress());
195 | await this.page.getByLabel(UIReference.newAddress.zipCodeLabel).fill(faker.location.zipCode());
196 | await this.page.getByLabel(UIReference.newAddress.cityNameLabel).fill(faker.location.city());
197 | await this.page.getByLabel(UIReference.newAddress.phoneNumberLabel).fill(faker.phone.number());
198 |
199 | // Select country (if needed)
200 | // await this.page.getByLabel('Country').selectOption('US');
201 |
202 | // Select state
203 | await this.page.getByLabel('State/Province').selectOption(faker.location.state());
204 |
205 | // Wait for any Magewire updates
206 | await this.waitForMagewireRequests();
207 | }
208 | }
209 |
210 | export default CheckoutPage;
211 |
--------------------------------------------------------------------------------
/tests/poms/frontend/compare.page.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { expect, type Page } from '@playwright/test';
4 | import { UIReference, outcomeMarker } from 'config';
5 |
6 | class ComparePage {
7 | page: Page;
8 |
9 | constructor(page: Page) {
10 | this.page = page;
11 | }
12 |
13 | async removeProductFromCompare(product:string){
14 | let comparisonPageEmptyText = this.page.getByText(UIReference.comparePage.comparisonPageEmptyText);
15 | // if the comparison page is empty, we can't remove anything
16 | if (await comparisonPageEmptyText.isVisible()) {
17 | return;
18 | }
19 |
20 | let removeFromCompareButton = this.page.getByLabel(`${UIReference.comparePage.removeCompareLabel} ${product}`);
21 | let productRemovedNotification = this.page.getByText(`${outcomeMarker.comparePage.productRemovedNotificationTextOne} ${product} ${outcomeMarker.comparePage.productRemovedNotificationTextTwo}`);
22 | await removeFromCompareButton.click();
23 | await expect(productRemovedNotification).toBeVisible();
24 | }
25 |
26 | async addToCart(product:string){
27 | const successMessage = this.page.locator(UIReference.general.successMessageLocator);
28 | let productAddedNotification = this.page.getByText(`${outcomeMarker.productPage.simpleProductAddedNotification} ${product}`);
29 | let addToCartbutton = this.page.getByLabel(`${UIReference.general.addToCartLabel} ${product}`);
30 |
31 | await addToCartbutton.click();
32 | await successMessage.waitFor();
33 | await expect(productAddedNotification).toBeVisible();
34 | }
35 |
36 | async addToWishList(product:string){
37 | const successMessage = this.page.locator(UIReference.general.successMessageLocator);
38 | let addToWishlistButton = this.page.getByLabel(`${UIReference.comparePage.addToWishListLabel} ${product}`);
39 | let productAddedNotification = this.page.getByText(`${product} ${outcomeMarker.wishListPage.wishListAddedNotification}`);
40 |
41 | await addToWishlistButton.click();
42 | await successMessage.waitFor();
43 | }
44 | }
45 | export default ComparePage;
46 |
--------------------------------------------------------------------------------
/tests/poms/frontend/contact.page.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { expect, type Locator, type Page } from '@playwright/test';
4 | import { faker } from '@faker-js/faker';
5 | import { UIReference, outcomeMarker, slugs } from 'config';
6 |
7 | class ContactPage {
8 | readonly page: Page;
9 | readonly nameField: Locator;
10 | readonly emailField: Locator;
11 | readonly messageField: Locator;
12 | readonly sendFormButton: Locator;
13 |
14 | constructor(page: Page){
15 | this.page = page;
16 | this.nameField = this.page.getByLabel(UIReference.credentials.nameFieldLabel);
17 | this.emailField = this.page.getByPlaceholder('Email', { exact: true });
18 | this.messageField = this.page.locator(UIReference.contactPage.messageFieldSelector);
19 | this.sendFormButton = this.page.getByRole('button', { name: UIReference.general.genericSubmitButtonLabel });
20 | }
21 |
22 | async fillOutForm(){
23 | await this.page.goto(slugs.contact.contactSlug);
24 | let messageSentConfirmationText = outcomeMarker.contactPage.messageSentConfirmationText;
25 |
26 | // Add a wait for the form to be visible
27 | await this.nameField.waitFor({state: 'visible', timeout: 10000});
28 |
29 | await this.nameField.fill(faker.person.firstName());
30 | await this.emailField.fill(faker.internet.email());
31 | await this.messageField.fill(faker.lorem.paragraph());
32 |
33 | await this.sendFormButton.click();
34 |
35 | await expect(this.page.getByText(messageSentConfirmationText)).toBeVisible();
36 | await expect(this.nameField, 'name should be empty now').toBeEmpty();
37 | await expect(this.emailField, 'email should be empty now').toBeEmpty();
38 | await expect(this.messageField, 'message should be empty now').toBeEmpty();
39 | }
40 | }
41 |
42 | export default ContactPage;
--------------------------------------------------------------------------------
/tests/poms/frontend/home.page.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { type Page } from '@playwright/test';
4 | import { UIReference } from 'config';
5 |
6 | class HomePage {
7 |
8 | readonly page: Page;
9 |
10 | constructor(page: Page) {
11 | this.page = page;
12 | }
13 |
14 | async addHomepageProductToCart(){
15 | let buyProductButton = this.page.getByRole('button').filter({hasText: UIReference.general.addToCartLabel}).first();
16 |
17 | if(await buyProductButton.isVisible()) {
18 | await buyProductButton.click();
19 | } else {
20 | throw new Error(`No 'Add to Cart' button found on homepage`);
21 | }
22 | }
23 | }
24 |
25 | export default HomePage;
26 |
--------------------------------------------------------------------------------
/tests/poms/frontend/login.page.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { expect, type Locator, type Page } from '@playwright/test';
4 | import { UIReference, slugs } from 'config';
5 |
6 | class LoginPage {
7 | readonly page: Page;
8 | readonly loginEmailField: Locator;
9 | readonly loginPasswordField: Locator;
10 | readonly loginButton: Locator;
11 |
12 | constructor(page: Page) {
13 | this.page = page;
14 | this.loginEmailField = page.getByLabel(UIReference.credentials.emailFieldLabel, {exact: true});
15 | this.loginPasswordField = page.getByLabel(UIReference.credentials.passwordFieldLabel, {exact: true});
16 | this.loginButton = page.getByRole('button', { name: UIReference.credentials.loginButtonLabel });
17 | }
18 |
19 | async login(email: string, password: string){
20 | await this.page.goto(slugs.account.loginSlug);
21 | await this.loginEmailField.fill(email);
22 | await this.loginPasswordField.fill(password);
23 | // usage of .press("Enter") to prevent webkit issues with button.click();
24 | await this.loginButton.press("Enter");
25 | }
26 |
27 | async loginExpectError(email: string, password: string, errorMessage: string) {
28 | await this.page.goto(slugs.account.loginSlug);
29 | await this.loginEmailField.fill(email);
30 | await this.loginPasswordField.fill(password);
31 | await this.loginButton.press('Enter');
32 | await this.page.waitForLoadState('networkidle');
33 |
34 | await expect(this.page, 'Should stay on login page').toHaveURL(new RegExp(slugs.account.loginSlug));
35 | }
36 | }
37 |
38 | export default LoginPage;
39 |
--------------------------------------------------------------------------------
/tests/poms/frontend/mainmenu.page.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { expect, type Locator, type Page } from '@playwright/test';
4 | import { UIReference, outcomeMarker, slugs } from 'config';
5 |
6 | class MainMenuPage {
7 | readonly page: Page;
8 | readonly mainMenuAccountButton: Locator;
9 | readonly mainMenuMiniCartButton: Locator;
10 | readonly mainMenuMyAccountItem: Locator;
11 | readonly mainMenuLogoutItem: Locator;
12 |
13 | constructor(page: Page) {
14 | this.page = page;
15 | this.mainMenuAccountButton = page.getByLabel(UIReference.mainMenu.myAccountButtonLabel);
16 | this.mainMenuMiniCartButton = page.getByLabel(UIReference.mainMenu.miniCartLabel);
17 | this.mainMenuLogoutItem = page.getByTitle(UIReference.mainMenu.myAccountLogoutItem);
18 | this.mainMenuMyAccountItem = page.getByTitle(UIReference.mainMenu.myAccountButtonLabel);
19 | }
20 |
21 | async gotoMyAccount(){
22 | await this.page.goto(slugs.productpage.simpleProductSlug);
23 | await this.mainMenuAccountButton.click();
24 | await this.mainMenuMyAccountItem.click();
25 |
26 | await expect(this.page.getByRole('heading', { name: UIReference.accountDashboard.accountDashboardTitleLabel })).toBeVisible();
27 | }
28 |
29 | async gotoAddressBook() {
30 | // create function to navigate to Address Book through the header menu links
31 | }
32 |
33 | async openMiniCart() {
34 | // await this.page.reload();
35 | // FIREFOX_WORKAROUND: wait for 3 seconds to allow minicart to be updated.
36 | await this.page.waitForTimeout(3000);
37 | const cartAmountBubble = this.mainMenuMiniCartButton.locator('span');
38 | cartAmountBubble.waitFor();
39 | const amountInCart = await cartAmountBubble.innerText();
40 |
41 | // waitFor is added to ensure the minicart button is visible before clicking, mostly as a fix for Firefox.
42 | // await this.mainMenuMiniCartButton.waitFor();
43 |
44 | await this.mainMenuMiniCartButton.click();
45 |
46 | let miniCartDrawer = this.page.locator("#cart-drawer-title");
47 | await expect(miniCartDrawer.getByText(outcomeMarker.miniCart.miniCartTitle)).toBeVisible();
48 | }
49 |
50 | async logout(){
51 | await this.page.goto(slugs.account.accountOverviewSlug);
52 | await this.mainMenuAccountButton.click();
53 | await this.mainMenuLogoutItem.click();
54 |
55 | //assertions: notification that user is logged out & logout button no longer visible
56 | await expect(this.page.getByText(outcomeMarker.logout.logoutConfirmationText, { exact: true })).toBeVisible();
57 | await expect(this.mainMenuLogoutItem).toBeHidden();
58 | }
59 | }
60 |
61 | export default MainMenuPage;
62 |
--------------------------------------------------------------------------------
/tests/poms/frontend/minicart.page.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { expect, type Locator, type Page } from '@playwright/test';
4 | import { UIReference, outcomeMarker, slugs } from 'config';
5 |
6 | class MiniCartPage {
7 | readonly page: Page;
8 | readonly toCheckoutButton: Locator;
9 | readonly toCartButton: Locator;
10 | readonly editProductButton: Locator;
11 | readonly productQuantityField: Locator;
12 | readonly updateItemButton: Locator;
13 | readonly priceOnPDP: Locator;
14 | readonly priceInMinicart: Locator;
15 |
16 | constructor(page: Page) {
17 | this.page = page;
18 | this.toCheckoutButton = page.getByRole('link', { name: UIReference.miniCart.checkOutButtonLabel });
19 | this.toCartButton = page.getByRole('link', { name: UIReference.miniCart.toCartLinkLabel });
20 | this.editProductButton = page.getByLabel(UIReference.miniCart.editProductIconLabel);
21 | this.productQuantityField = page.getByLabel(UIReference.miniCart.productQuantityFieldLabel);
22 | this.updateItemButton = page.getByRole('button', { name: UIReference.cart.updateItemButtonLabel });
23 | this.priceOnPDP = page.getByLabel(UIReference.general.genericPriceLabel).getByText(UIReference.general.genericPriceSymbol);
24 | this.priceInMinicart = page.getByText(UIReference.general.genericPriceSymbol).first();
25 | }
26 |
27 | async goToCheckout(){
28 | await this.toCheckoutButton.click();
29 | await expect(this.page).toHaveURL(new RegExp(`${slugs.checkout.checkoutSlug}.*`));
30 | }
31 |
32 | async goToCart(){
33 | await this.toCartButton.click();
34 | await expect(this.page).toHaveURL(new RegExp(`${slugs.cart.cartSlug}.*`));
35 | }
36 |
37 | async removeProductFromMinicart(product: string) {
38 | let productRemovedNotification = outcomeMarker.miniCart.productRemovedConfirmation;
39 | let removeProductMiniCartButton = this.page.getByLabel(`${UIReference.miniCart.removeProductIconLabel} "${UIReference.productPage.simpleProductTitle}"`);
40 | await removeProductMiniCartButton.click();
41 | await expect.soft(this.page.getByText(productRemovedNotification)).toBeVisible();
42 | await expect(removeProductMiniCartButton).toBeHidden();
43 | }
44 |
45 | async updateProduct(amount: string){
46 | let productQuantityChangedNotification = outcomeMarker.miniCart.productQuantityChangedConfirmation;
47 | await this.editProductButton.click();
48 | await expect(this.page).toHaveURL(new RegExp(`${slugs.cart.cartProductChangeSlug}.*`));
49 |
50 | await this.productQuantityField.fill(amount);
51 | await this.updateItemButton.click();
52 | await expect.soft(this.page.getByText(productQuantityChangedNotification)).toBeVisible();
53 |
54 | let productQuantityInCart = await this.page.getByLabel(UIReference.cart.cartQuantityLabel).first().inputValue();
55 | expect(productQuantityInCart).toBe(amount);
56 | }
57 |
58 | async checkPriceWithProductPage() {
59 | const priceOnPage = await this.page.locator(UIReference.productPage.simpleProductPrice).first().innerText();
60 | const productTitle = await this.page.getByRole('heading', { level : 1}).innerText();
61 | const productListing = this.page.locator('div').filter({hasText: productTitle});
62 | const priceInMinicart = await productListing.locator(UIReference.miniCart.minicartPriceFieldClass).first().textContent();
63 | //expect(priceOnPage).toBe(priceInMinicart);
64 | expect(priceOnPage, `Expect these prices to be the same: priceOnpage: ${priceOnPage} and priceInMinicart: ${priceInMinicart}`).toBe(priceInMinicart);
65 | }
66 | }
67 |
68 | export default MiniCartPage;
69 |
--------------------------------------------------------------------------------
/tests/poms/frontend/newsletter.page.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { expect, type Locator, type Page } from '@playwright/test';
4 | import { UIReference, outcomeMarker } from 'config';
5 |
6 | class NewsletterSubscriptionPage {
7 | readonly page: Page;
8 | readonly newsletterCheckElement: Locator;
9 | readonly saveSubscriptionsButton: Locator;
10 |
11 | constructor(page: Page) {
12 | this.page = page;
13 | this.newsletterCheckElement = page.getByLabel(UIReference.newsletterSubscriptions.generalSubscriptionCheckLabel);
14 | this.saveSubscriptionsButton = page.getByRole('button', {name:UIReference.newsletterSubscriptions.saveSubscriptionsButton});
15 | }
16 |
17 | async updateNewsletterSubscription(){
18 |
19 | if(await this.newsletterCheckElement.isChecked()) {
20 | // user is already subscribed, test runs unsubscribe
21 | var subscriptionUpdatedNotification = outcomeMarker.account.newsletterRemovedNotification;
22 |
23 | await this.newsletterCheckElement.uncheck();
24 | await this.saveSubscriptionsButton.click();
25 |
26 | var subscribed = false;
27 |
28 | } else {
29 | // user is not yet subscribed, test runs subscribe
30 | subscriptionUpdatedNotification = outcomeMarker.account.newsletterSavedNotification;
31 |
32 | await this.newsletterCheckElement.check();
33 | await this.saveSubscriptionsButton.click();
34 |
35 | subscribed = true;
36 | }
37 |
38 | await expect(this.page.getByText(subscriptionUpdatedNotification)).toBeVisible();
39 | return subscribed;
40 | }
41 | }
42 |
43 | export default NewsletterSubscriptionPage;
44 |
--------------------------------------------------------------------------------
/tests/poms/frontend/orderhistory.page.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { expect, type Page } from '@playwright/test';
4 | import { slugs } from 'config';
5 |
6 | class OrderHistoryPage {
7 | readonly page: Page;
8 |
9 | constructor(page: Page) {
10 | this.page = page;
11 | }
12 |
13 | async open() {
14 | await this.page.goto(slugs.account.orderHistorySlug);
15 | await this.page.waitForLoadState();
16 | }
17 |
18 | async verifyOrderPresent(orderNumber: string) {
19 | await expect(this.page.getByText(orderNumber)).toBeVisible();
20 | }
21 | }
22 |
23 | export default OrderHistoryPage;
24 |
--------------------------------------------------------------------------------
/tests/poms/frontend/product.page.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { expect, type Locator, type Page } from '@playwright/test';
4 | import { UIReference, outcomeMarker, slugs } from 'config';
5 |
6 | class ProductPage {
7 | readonly page: Page;
8 | simpleProductTitle: Locator | undefined;
9 | configurableProductTitle: Locator | undefined;
10 | addToCartButton: Locator;
11 | addToCompareButton: Locator;
12 | addToWishlistButton: Locator;
13 |
14 | constructor(page: Page) {
15 | this.page = page;
16 | this.addToCartButton = page.getByRole('button', { name: UIReference.productPage.addToCartButtonLocator });
17 | this.addToCompareButton = page.getByLabel(UIReference.productPage.addToCompareButtonLabel, { exact: true });
18 | this.addToWishlistButton = page.getByLabel(UIReference.productPage.addToWishlistButtonLabel, { exact: true });
19 | }
20 |
21 | // ==============================================
22 | // Productpage-related methods
23 | // ==============================================
24 |
25 | async addProductToCompare(product:string, url: string){
26 | let productAddedNotification = `${outcomeMarker.productPage.simpleProductAddedNotification} product`;
27 | const successMessage = this.page.locator(UIReference.general.successMessageLocator);
28 |
29 | await this.page.goto(url);
30 | await this.addToCompareButton.click();
31 | await successMessage.waitFor();
32 | await expect(this.page.getByText(productAddedNotification)).toBeVisible();
33 |
34 | await this.page.goto(slugs.productpage.productComparisonSlug);
35 |
36 | // Assertion: a cell with the product name inside a cell with the product name should be visible
37 | await expect(this.page.getByRole('cell', {name: product}).getByText(product, {exact: true})).toBeVisible();
38 | }
39 |
40 | async addProductToWishlist(product:string, url: string){
41 | let addedToWishlistNotification = `${product} ${outcomeMarker.wishListPage.wishListAddedNotification}`;
42 | await this.page.goto(url);
43 | await this.addToWishlistButton.click();
44 |
45 | await this.page.waitForLoadState();
46 |
47 | let productNameInWishlist = this.page.locator(UIReference.wishListPage.wishListItemGridLabel).getByText(UIReference.productPage.simpleProductTitle, {exact: true});
48 |
49 | await expect(this.page).toHaveURL(new RegExp(slugs.wishlist.wishListRegex));
50 | await expect(this.page.getByText(addedToWishlistNotification)).toBeVisible();
51 | await expect(productNameInWishlist).toContainText(product);
52 | }
53 |
54 | async leaveProductReview(product:string, url: string){
55 | await this.page.goto(url);
56 |
57 | //TODO: Uncomment this and fix test once website is fixed
58 | /*
59 | await page.locator('#Rating_5_label path').click();
60 | await page.getByPlaceholder('Nickname*').click();
61 | await page.getByPlaceholder('Nickname*').fill('John');
62 | await page.getByPlaceholder('Nickname*').press('Tab');
63 | await page.getByPlaceholder('Summary*').click();
64 | await page.getByPlaceholder('Summary*').fill('A short paragraph');
65 | await page.getByPlaceholder('Review*').click();
66 | await page.getByPlaceholder('Review*').fill('Review message!');
67 | await page.getByRole('button', { name: 'Submit Review' }).click();
68 | await page.getByRole('img', { name: 'loader' }).click();
69 | */
70 | }
71 |
72 | async openLightboxAndScrollThrough(url: string){
73 | await this.page.goto(url);
74 | let fullScreenOpener = this.page.getByLabel(UIReference.productPage.fullScreenOpenLabel);
75 | let fullScreenCloser = this.page.getByLabel(UIReference.productPage.fullScreenCloseLabel);
76 | let thumbnails = this.page.getByRole('button', {name: UIReference.productPage.thumbnailImageLabel});
77 |
78 | await fullScreenOpener.click();
79 | await expect(fullScreenCloser).toBeVisible();
80 |
81 | for (const img of await thumbnails.all()) {
82 | await img.click();
83 | // wait for transition animation
84 | await this.page.waitForTimeout(500);
85 | await expect(img, `CSS class 'border-primary' appended to button`).toHaveClass(new RegExp(outcomeMarker.productPage.borderClassRegex));
86 | }
87 |
88 | await fullScreenCloser.click();
89 | await expect(fullScreenCloser).toBeHidden();
90 |
91 | }
92 |
93 | async changeReviewCountAndVerify(url: string) {
94 | await this.page.goto(url);
95 |
96 | // Get the default review count from URL or UI
97 | const initialUrl = this.page.url();
98 |
99 | // Find and click the review count selector
100 | const reviewCountSelector = this.page.getByLabel(UIReference.productPage.reviewCountLabel);
101 | await expect(reviewCountSelector).toBeVisible();
102 |
103 | // Select 20 reviews per page
104 | await reviewCountSelector.selectOption('20');
105 | await this.page.waitForURL(/.*limit=20.*/);
106 |
107 | // Verify URL contains the new limit
108 | const urlAfterFirstChange = this.page.url();
109 | expect(urlAfterFirstChange, 'URL should contain limit=20 parameter').toContain('limit=20');
110 | expect(urlAfterFirstChange, 'URL should have changed after selecting 20 items per page').not.toEqual(initialUrl);
111 |
112 | // Select 50 reviews per page
113 | await reviewCountSelector.selectOption('50');
114 | await this.page.waitForURL(/.*limit=50.*/);
115 |
116 | // Verify URL contains the new limit
117 | const urlAfterSecondChange = this.page.url();
118 | expect(urlAfterSecondChange, 'URL should contain limit=50 parameter').toContain('limit=50');
119 | expect(urlAfterSecondChange, 'URL should have changed after selecting 50 items per page').not.toEqual(urlAfterFirstChange);
120 | }
121 |
122 | // ==============================================
123 | // Cart-related methods
124 | // ==============================================
125 |
126 | async addSimpleProductToCart(product: string, url: string, quantity?: string) {
127 |
128 | await this.page.goto(url);
129 | this.simpleProductTitle = this.page.getByRole('heading', {name: product, exact:true});
130 | expect(await this.simpleProductTitle.innerText()).toEqual(product);
131 | await expect(this.simpleProductTitle.locator('span')).toBeVisible();
132 |
133 | if(quantity){
134 | // set quantity
135 | await this.page.getByLabel(UIReference.productPage.quantityFieldLabel).fill('2');
136 | }
137 |
138 | await this.addToCartButton.click();
139 |
140 | await expect(this.page.locator(UIReference.general.messageLocator)).toBeVisible();
141 | return ;
142 | }
143 |
144 | async addConfigurableProductToCart(product: string, url:string, quantity?:string){
145 | await this.page.goto(url);
146 | this.configurableProductTitle = this.page.getByRole('heading', {name: product, exact:true});
147 | let productAddedNotification = `${outcomeMarker.productPage.simpleProductAddedNotification} ${product}`;
148 | const productOptions = this.page.locator(UIReference.productPage.configurableProductOptionForm);
149 |
150 | // loop through each radiogroup (product option) within the form
151 | for (const option of await productOptions.getByRole('radiogroup').all()) {
152 | await option.locator(UIReference.productPage.configurableProductOptionValue).first().check();
153 | }
154 |
155 | if(quantity){
156 | // set quantity
157 | await this.page.getByLabel(UIReference.productPage.quantityFieldLabel).fill('2');
158 | }
159 |
160 | await this.addToCartButton.click();
161 | let successMessage = this.page.locator(UIReference.general.successMessageLocator);
162 | await successMessage.waitFor();
163 | await expect(this.page.getByText(productAddedNotification)).toBeVisible();
164 | }
165 | }
166 |
167 | export default ProductPage;
168 |
--------------------------------------------------------------------------------
/tests/poms/frontend/register.page.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { expect, type Locator, type Page } from '@playwright/test';
4 | import { UIReference, outcomeMarker, slugs} from 'config';
5 |
6 | class RegisterPage {
7 | readonly page: Page;
8 | readonly accountCreationFirstNameField: Locator;
9 | readonly accountCreationLastNameField: Locator;
10 | readonly accountCreationEmailField: Locator;
11 | readonly accountCreationPasswordField: Locator;
12 | readonly accountCreationPasswordRepeatField: Locator;
13 | readonly accountCreationConfirmButton: Locator;
14 |
15 | constructor(page: Page){
16 | this.page = page;
17 | this.accountCreationFirstNameField = page.getByLabel(UIReference.personalInformation.firstNameLabel);
18 | this.accountCreationLastNameField = page.getByLabel(UIReference.personalInformation.lastNameLabel);
19 | this.accountCreationEmailField = page.getByLabel(UIReference.credentials.emailFieldLabel, { exact: true});
20 | this.accountCreationPasswordField = page.getByLabel(UIReference.credentials.passwordFieldLabel, { exact: true });
21 | this.accountCreationPasswordRepeatField = page.getByLabel(UIReference.credentials.passwordConfirmFieldLabel);
22 | this.accountCreationConfirmButton = page.getByRole('button', {name: UIReference.accountCreation.createAccountButtonLabel});
23 | }
24 |
25 |
26 | async createNewAccount(firstName: string, lastName: string, email: string, password: string, muted: boolean = false){
27 | let accountInformationField = this.page.locator(UIReference.accountDashboard.accountInformationFieldLocator).first();
28 | await this.page.goto(slugs.account.createAccountSlug);
29 |
30 | await this.accountCreationFirstNameField.fill(firstName);
31 | await this.accountCreationLastNameField.fill(lastName);
32 | await this.accountCreationEmailField.fill(email);
33 | await this.accountCreationPasswordField.fill(password);
34 | await this.accountCreationPasswordRepeatField.fill(password);
35 | await this.accountCreationConfirmButton.click();
36 |
37 | if(!muted) {
38 | // Assertions: Account created notification, navigated to account page, email visible on page
39 | await expect(this.page.getByText(outcomeMarker.account.accountCreatedNotificationText), 'Account creation notification should be visible').toBeVisible();
40 | await expect(this.page, 'Should be redirected to account overview page').toHaveURL(new RegExp('.+' + slugs.account.accountOverviewSlug));
41 | await expect(accountInformationField, `Account information should contain email: ${email}`).toContainText(email);
42 | }
43 | }
44 | }
45 |
46 | export default RegisterPage;
47 |
--------------------------------------------------------------------------------
/tests/poms/frontend/search.page.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { expect, type Locator, type Page } from '@playwright/test';
4 | import { UIReference } from 'config';
5 |
6 | class SearchPage {
7 | readonly page: Page;
8 | readonly searchToggle: Locator;
9 | readonly searchInput: Locator;
10 | readonly suggestionBox: Locator;
11 |
12 | constructor(page: Page) {
13 | this.page = page;
14 | this.searchToggle = page.locator(UIReference.search.searchToggleLocator);
15 | this.searchInput = page.locator(UIReference.search.searchInputLocator);
16 | this.suggestionBox = page.locator(UIReference.search.suggestionBoxLocator);
17 | }
18 |
19 | async openSearch() {
20 | await this.searchToggle.waitFor({ state: 'visible' });
21 | await this.searchToggle.click();
22 | await expect(this.searchInput).toBeVisible();
23 | }
24 |
25 | async search(query: string) {
26 | await this.openSearch();
27 | await this.searchInput.fill(query);
28 | await this.searchInput.press('Enter');
29 | await this.page.waitForLoadState('networkidle');
30 | }
31 | }
32 |
33 | export default SearchPage;
34 |
--------------------------------------------------------------------------------
/tests/product.spec.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { test } from '@playwright/test';
4 | import { UIReference ,slugs } from 'config';
5 |
6 | import ProductPage from './poms/frontend/product.page';
7 | import LoginPage from './poms/frontend/login.page';
8 | import { requireEnv } from './utils/env.utils';
9 |
10 | test.describe('Product page tests',{ tag: '@product',}, () => {
11 | test('Add_product_to_compare',{ tag: '@cold'}, async ({page}) => {
12 | const productPage = new ProductPage(page);
13 | await productPage.addProductToCompare(UIReference.productPage.simpleProductTitle, slugs.productpage.simpleProductSlug);
14 | });
15 |
16 | test('Add_product_to_wishlist',{ tag: '@cold'}, async ({page, browserName}) => {
17 | await test.step('Log in with account', async () =>{
18 | const browserEngine = browserName?.toUpperCase() || "UNKNOWN";
19 | const emailInputValue = requireEnv(`MAGENTO_EXISTING_ACCOUNT_EMAIL_${browserEngine}`);
20 | const passwordInputValue = requireEnv('MAGENTO_EXISTING_ACCOUNT_PASSWORD');
21 |
22 | const loginPage = new LoginPage(page);
23 | await loginPage.login(emailInputValue, passwordInputValue);
24 | });
25 |
26 | await test.step('Add product to wishlist', async () =>{
27 | const productPage = new ProductPage(page);
28 | await productPage.addProductToWishlist(UIReference.productPage.simpleProductTitle, slugs.productpage.simpleProductSlug);
29 | });
30 | });
31 |
32 |
33 | test.fixme('Leave a product review (Test currently fails due to error on website)',{ tag: '@cold'}, async ({page}) => {
34 | // const productPage = new ProductPage(page);
35 | // await productPage.leaveProductReview(UIReference.productPage.simpleProductTitle, slugs.productpage.simpleProductSlug);
36 | });
37 |
38 | test('Open_pictures_in_lightbox_and_scroll', async ({page}) => {
39 | const productPage = new ProductPage(page);
40 | await productPage.openLightboxAndScrollThrough(slugs.productpage.configurableProductSlug);
41 | });
42 |
43 | test('Change_number_of_reviews_shown_on_product_page', async ({page}) => {
44 | const productPage = new ProductPage(page);
45 | await productPage.changeReviewCountAndVerify(slugs.productpage.simpleProductSlug);
46 | });
47 | });
48 |
49 | test.describe('Simple product tests',{ tag: '@simple-product',}, () => {
50 | test.fixme('Simple tests will be added later', async ({ page }) => {});
51 | });
52 |
53 | test.describe('Configurable product tests',{ tag: '@conf-product',}, () => {
54 | test.fixme('Configurable tests will be added later', async ({ page }) => {});
55 | });
56 |
--------------------------------------------------------------------------------
/tests/register.spec.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import {test} from '@playwright/test';
4 | import {faker} from '@faker-js/faker';
5 | import { inputValues } from 'config';
6 |
7 | import RegisterPage from './poms/frontend/register.page';
8 | import { requireEnv } from './utils/env.utils';
9 |
10 | // Reset storageState to ensure we're not logged in before running these tests.
11 | test.use({ storageState: { cookies: [], origins: [] } });
12 |
13 | /**
14 | * @feature Magento 2 Account Creation
15 | * @scenario The user creates an account on the website
16 | * @given I am on any Magento 2 page
17 | * @when I go to the account creation page
18 | * @and I fill in the required information correctly
19 | * @then I click the 'Create account' button
20 | * @then I should see a messsage confirming my account was created
21 | */
22 | test('User_registers_an_account', { tag: ['@setup', '@hot'] }, async ({page, browserName}, testInfo) => {
23 | const registerPage = new RegisterPage(page);
24 |
25 | // Retrieve desired password from .env file
26 | const existingAccountPassword = requireEnv('MAGENTO_EXISTING_ACCOUNT_PASSWORD');
27 | var firstName = faker.person.firstName();
28 | var lastName = faker.person.lastName();
29 |
30 | const browserEngine = browserName?.toUpperCase() || "UNKNOWN";
31 | let randomNumber = Math.floor(Math.random() * 100);
32 | let emailHandle = inputValues.accountCreation.emailHandleValue;
33 | let emailHost = inputValues.accountCreation.emailHostValue;
34 | const accountEmail = `${emailHandle}${randomNumber}-${browserEngine}@${emailHost}`;
35 | //const accountEmail = process.env[`MAGENTO_EXISTING_ACCOUNT_EMAIL_${browserEngine}`];
36 |
37 | if (!accountEmail) {
38 | throw new Error(`Generated account email is invalid.`);
39 | }
40 | // end of browserNameEmailSection
41 |
42 | await registerPage.createNewAccount(firstName, lastName, accountEmail, existingAccountPassword);
43 | testInfo.annotations.push({ type: 'Notification: account created!', description: `Credentials used: ${accountEmail}, password: ${existingAccountPassword}` });
44 | });
45 |
--------------------------------------------------------------------------------
/tests/search.spec.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { test, expect } from '@playwright/test';
4 | import { UIReference, outcomeMarker, inputValues, slugs } from 'config';
5 |
6 | import SearchPage from './poms/frontend/search.page';
7 |
8 | test.describe('Search functionality', () => {
9 | test('Search_query_returns_multiple_results', async ({ page }) => {
10 | await page.goto('');
11 | const searchPage = new SearchPage(page);
12 | await searchPage.search(inputValues.search.queryMultipleResults);
13 | await expect(page).toHaveURL(new RegExp(slugs.search.resultsSlug));
14 | const results = page.locator(`${UIReference.categoryPage.productGridLocator} li`);
15 | const resultCount = await results.count();
16 | expect(resultCount).toBeGreaterThan(1);
17 | });
18 |
19 | test('User_can_find_a_specific_product_and_navigate_to_its_page', async ({ page }) => {
20 | await page.goto('');
21 | const searchPage = new SearchPage(page);
22 | await searchPage.search(inputValues.search.querySpecificProduct);
23 | await expect(page).toHaveURL(slugs.productpage.simpleProductSlug);
24 | });
25 |
26 | test('No_results_message_is_shown_for_unknown_query', async ({ page }) => {
27 | await page.goto('');
28 | const searchPage = new SearchPage(page);
29 | await searchPage.search(inputValues.search.queryNoResults);
30 | await expect(page.getByText(outcomeMarker.search.noResultsMessage)).toBeVisible();
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/tests/setup.spec.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { test as base } from '@playwright/test';
4 | import fs from 'fs';
5 | import path from 'path';
6 | import { toggles, inputValues } from 'config';
7 |
8 | import MagentoAdminPage from './poms/adminhtml/magentoAdmin.page';
9 | import RegisterPage from './poms/frontend/register.page';
10 | import { requireEnv } from './utils/env.utils';
11 |
12 | /**
13 | * NOTE:
14 | * The first if-statement checks if we are running in CI.
15 | * If so, we always run the setup.
16 | * Else, we check if the 'setup' test toggle in test-toggles.json has been set to true.
17 | */
18 |
19 | const runSetupTests = (describeFn: typeof base.describe | typeof base.describe.only) => {
20 | describeFn('Setting up the testing environment', () => {
21 | base('Enable_multiple_admin_logins', { tag: '@setup' }, async ({ page, browserName }, testInfo) => {
22 | const magentoAdminUsername = requireEnv('MAGENTO_ADMIN_USERNAME');
23 | const magentoAdminPassword = requireEnv('MAGENTO_ADMIN_PASSWORD');
24 |
25 | const browserEngine = browserName?.toUpperCase() || "UNKNOWN";
26 |
27 | if (browserEngine === "CHROMIUM") {
28 | const magentoAdminPage = new MagentoAdminPage(page);
29 | await magentoAdminPage.login(magentoAdminUsername, magentoAdminPassword);
30 | await magentoAdminPage.enableMultipleAdminLogins();
31 | } else {
32 | testInfo.skip(true, `Skipping because configuration is only needed once.`);
33 | }
34 | });
35 |
36 | base('Disable_login_captcha', { tag: '@setup' }, async ({ page, browserName }, testInfo) => {
37 | const magentoAdminUsername = requireEnv('MAGENTO_ADMIN_USERNAME');
38 | const magentoAdminPassword = requireEnv('MAGENTO_ADMIN_PASSWORD');
39 |
40 | const browserEngine = browserName?.toUpperCase() || "UNKNOWN";
41 |
42 | if (browserEngine === "CHROMIUM") {
43 | const magentoAdminPage = new MagentoAdminPage(page);
44 | await magentoAdminPage.login(magentoAdminUsername, magentoAdminPassword);
45 | await magentoAdminPage.disableLoginCaptcha();
46 | } else {
47 | testInfo.skip(true, `Skipping because configuration is only needed once.`);
48 | }
49 | });
50 |
51 | base('Setup_environment_for_tests', { tag: '@setup' }, async ({ page, browserName }, testInfo) => {
52 | const browserEngine = browserName?.toUpperCase() || "UNKNOWN";
53 | const setupCompleteVar = `SETUP_COMPLETE_${browserEngine}`;
54 | const isSetupComplete = process.env[setupCompleteVar];
55 |
56 | if (isSetupComplete === 'DONE') {
57 | testInfo.skip(true, `Skipping because configuration is only needed once.`);
58 | }
59 |
60 | await base.step(`Step 1: Perform actions`, async () => {
61 | const magentoAdminUsername = requireEnv('MAGENTO_ADMIN_USERNAME');
62 | const magentoAdminPassword = requireEnv('MAGENTO_ADMIN_PASSWORD');
63 |
64 | const magentoAdminPage = new MagentoAdminPage(page);
65 | await magentoAdminPage.login(magentoAdminUsername, magentoAdminPassword);
66 |
67 | const couponCode = requireEnv(`MAGENTO_COUPON_CODE_${browserEngine}`);
68 | await magentoAdminPage.addCartPriceRule(couponCode);
69 |
70 | const registerPage = new RegisterPage(page);
71 | const accountEmail = requireEnv(`MAGENTO_EXISTING_ACCOUNT_EMAIL_${browserEngine}`);
72 | const accountPassword = requireEnv('MAGENTO_EXISTING_ACCOUNT_PASSWORD');
73 |
74 | await registerPage.createNewAccount(
75 | inputValues.accountCreation.firstNameValue,
76 | inputValues.accountCreation.lastNameValue,
77 | accountEmail,
78 | accountPassword,
79 | true
80 | );
81 | });
82 |
83 | await base.step(`Step 2: (optional) Update env file`, async () => {
84 | if (process.env.CI === 'true') {
85 | console.log("Running in CI environment. Skipping .env update.");
86 | base.skip();
87 | }
88 |
89 | const envPath = path.resolve(__dirname, '../.env');
90 | try {
91 | if (fs.existsSync(envPath)) {
92 | const envContent = fs.readFileSync(envPath, 'utf-8');
93 | if (!envContent.includes(`SETUP_COMPLETE_${browserEngine}='DONE'`)) {
94 | fs.appendFileSync(envPath, `\nSETUP_COMPLETE_${browserEngine}='DONE'`);
95 | console.log(`Environment setup completed. Added SETUP_COMPLETE_${browserEngine}='DONE' to .env`);
96 | }
97 | } else {
98 | throw new Error('.env file not found. Please ensure it exists in the root directory.');
99 | }
100 | } catch (error) {
101 | const err = error as Error;
102 | throw new Error(`Failed to update .env file: ${err.message}`);
103 | }
104 | });
105 | });
106 | });
107 | };
108 |
109 | if (process.env.CI) {
110 | runSetupTests(base.describe);
111 | } else if (toggles.general.setup) {
112 | runSetupTests(base.describe.only);
113 | }
114 |
--------------------------------------------------------------------------------
/tests/types/magewire.d.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | interface Window {
4 | magewire?: {
5 | processing: boolean;
6 | [key: string]: any;
7 | };
8 | }
9 |
--------------------------------------------------------------------------------
/tests/utils/env.utils.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | /**
4 | * Utility to retrieve required environment variables.
5 | * Throws an error when the variable is not set.
6 | */
7 | export function requireEnv(varName: string): string {
8 | const value = process.env[varName];
9 | if (!value) {
10 | throw new Error(`${varName} is not defined in the .env file.`);
11 | }
12 | return value;
13 | }
14 |
15 |
--------------------------------------------------------------------------------
/tests/utils/magewire.utils.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { Page } from '@playwright/test';
4 |
5 | class MagewireUtils {
6 | readonly page: Page;
7 |
8 | constructor(page: Page) {
9 | this.page = page;
10 | }
11 |
12 | /**
13 | * Waits for all Magewire requests to complete
14 | * This includes both UI indicators and actual network requests
15 | */
16 | async waitForMagewireRequests(): Promise {
17 | // Wait for the Magewire messenger element to disappear or have 0 height
18 | await this.page.waitForFunction(() => {
19 | const element = document.querySelector('.magewire\\.messenger');
20 | return element && getComputedStyle(element).height === '0px';
21 | }, { timeout: 30000 });
22 |
23 | // Additionally wait for any pending Magewire network requests to complete
24 | await this.page.waitForFunction(() => {
25 | return !window.magewire || !(window.magewire as any).processing;
26 | }, { timeout: 30000 });
27 |
28 | // Small additional delay to ensure DOM updates are complete
29 | await this.page.waitForTimeout(500);
30 | }
31 |
32 | /**
33 | * Waits for a specific network request to complete
34 | * @param urlPattern - URL pattern to match (e.g., '*/magewire/*')
35 | */
36 | async waitForNetworkRequest(urlPattern: string): Promise {
37 | await this.page.waitForResponse(
38 | response => response.url().includes(urlPattern) && response.status() === 200,
39 | { timeout: 30000 }
40 | );
41 |
42 | // Small additional delay to ensure DOM updates are complete
43 | await this.page.waitForTimeout(500);
44 | }
45 | }
46 |
47 | export default MagewireUtils;
48 |
--------------------------------------------------------------------------------
/tests/utils/notification.validator.ts:
--------------------------------------------------------------------------------
1 | // @ts-check
2 |
3 | import { expect, Page, TestInfo } from "@playwright/test";
4 | import { UIReference } from 'config';
5 |
6 | class NotificationValidator {
7 |
8 | private page : Page;
9 | private testInfo: TestInfo;
10 |
11 | constructor(page: Page, testInfo: TestInfo) {
12 | this.page = page;
13 | this.testInfo = testInfo;
14 | }
15 |
16 | /**
17 | * @param value
18 | * @return json object
19 | */
20 | async validate(value: string) {
21 | await this.page.locator(UIReference.general.messageLocator).waitFor({ state: 'visible' });
22 | const notificationText = await this.page.locator(UIReference.general.messageLocator).textContent();
23 | let message = { success: true, message: ''};
24 |
25 | if (
26 | ! expect.soft(this.page.locator(UIReference.general.messageLocator)).toContainText(value)
27 | ) {
28 | message = { success: false, message: `Notification text not found: ${value}. Found notification text: ${notificationText}` };
29 | }
30 |
31 | this.testInfo.annotations.push({ type: 'Notification: beforeEach add product to cart', description: message.message });
32 | }
33 | }
34 |
35 | export default NotificationValidator;
--------------------------------------------------------------------------------
/translate-json.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const fs = require('fs');
4 | const path = require('path');
5 | const csv = require('csv-parse/sync');
6 |
7 | // Check if locale argument is provided
8 | if (process.argv.length < 3) {
9 | console.error('Please provide a locale argument (e.g., nl_NL)');
10 | process.exit(1);
11 | }
12 |
13 | const locale = process.argv[2];
14 |
15 | // Function to find CSV files recursively
16 | function findCsvFiles(dir, locale) {
17 | let results = [];
18 | const files = fs.readdirSync(dir);
19 |
20 | for (const file of files) {
21 | const filePath = path.join(dir, file);
22 | const stat = fs.statSync(filePath);
23 |
24 | if (stat.isDirectory()) {
25 | results = results.concat(findCsvFiles(filePath, locale));
26 | } else if (file === `${locale}.csv`) {
27 | results.push(filePath);
28 | }
29 | }
30 |
31 | return results;
32 | }
33 |
34 | // Function to parse CSV file
35 | function parseCsvFile(filePath) {
36 | const content = fs.readFileSync(filePath, 'utf-8');
37 | const records = csv.parse(content, {
38 | skip_empty_lines: true,
39 | trim: true
40 | });
41 |
42 | const translations = {};
43 | for (const [key, value] of records) {
44 | translations[key] = value;
45 | }
46 |
47 | return translations;
48 | }
49 |
50 | // Function to merge translations with precedence
51 | function mergeTranslations(appTranslations, vendorTranslations) {
52 | return { ...vendorTranslations, ...appTranslations };
53 | }
54 |
55 | // Function to translate values in an object recursively
56 | function translateObject(obj, translations) {
57 | if (typeof obj === 'string') {
58 | return translations[obj] || obj;
59 | }
60 |
61 | if (Array.isArray(obj)) {
62 | return obj.map(item => translateObject(item, translations));
63 | }
64 |
65 | if (typeof obj === 'object' && obj !== null) {
66 | const result = {};
67 | for (const [key, value] of Object.entries(obj)) {
68 | result[key] = translateObject(value, translations);
69 | }
70 | return result;
71 | }
72 |
73 | return obj;
74 | }
75 |
76 | // Main execution
77 | try {
78 | // Find and parse CSV files
79 | const appCsvFiles = findCsvFiles('../../../../../../../app', locale);
80 | const vendorCsvFiles = findCsvFiles('../../../../../../../vendor', locale);
81 |
82 | let appTranslations = {};
83 | let vendorTranslations = {};
84 |
85 | // Parse app translations
86 | for (const file of appCsvFiles) {
87 | const translations = parseCsvFile(file);
88 | appTranslations = { ...appTranslations, ...translations };
89 | }
90 |
91 | // Parse vendor translations
92 | for (const file of vendorCsvFiles) {
93 | const translations = parseCsvFile(file);
94 | vendorTranslations = { ...vendorTranslations, ...translations };
95 | }
96 |
97 | // Merge translations with app taking precedence
98 | const translations = mergeTranslations(appTranslations, vendorTranslations);
99 |
100 | // Process JSON files
101 | const jsonFiles = [
102 | 'element-identifiers.json',
103 | 'outcome-markers.json'
104 | ];
105 |
106 | for (const fileName of jsonFiles) {
107 | const sourcePath = path.resolve('base-tests/config', fileName);
108 | const destPath = path.resolve('tests/config', fileName);
109 |
110 | const content = JSON.parse(fs.readFileSync(sourcePath, 'utf-8'));
111 | const translatedContent = translateObject(content, translations);
112 |
113 | // Ensure target directory exists
114 | fs.mkdirSync(path.dirname(destPath), { recursive: true });
115 |
116 | fs.writeFileSync(destPath, JSON.stringify(translatedContent, null, 2));
117 | console.log(`Translated file written: ${destPath}`);
118 | }
119 |
120 | console.log('Translation completed successfully!');
121 | } catch (error) {
122 | console.error('Error:', error.message);
123 | process.exit(1);
124 | }
--------------------------------------------------------------------------------
/tsconfig.example.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "module": "commonjs",
5 | "strict": true,
6 | "esModuleInterop": true,
7 | "allowSyntheticDefaultImports": true,
8 | "resolveJsonModule": true,
9 | "baseUrl": ".",
10 | "paths": {
11 | "config": ["tests.config.ts"],
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------