Skip to content

Commit 8a351e3

Browse files
Merge pull request #189 from elgentos/gitlab-main
Sync gitlab-main to main
2 parents 33052cf + 930f0f7 commit 8a351e3

File tree

3 files changed

+203
-15
lines changed

3 files changed

+203
-15
lines changed

CHANGELOG.md

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,34 @@
33
All notable changes to this project will be documented in this file.
44
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).
55

6+
## [3.0.0] - 2025-10-23
7+
8+
### Added
9+
- Added footer tests for subscribing to the newsletter and currency switcher.
10+
- Added a Notification Utility to validate Magento frontend messages.
11+
- Added a console.log utility tool to prevent having to use `console.log`.
12+
13+
### Changed
14+
- Rewritten `setup.spec.ts` for better legibility. Setup will now also check if elements are present and active (e.g., coupon codes) and no longer writes to `.env`.
15+
- Improved the capability of the Magewire loading utility to better handle various states of the Magewire loading element.
16+
- Improved usage of TypeScript `@` path aliases (e.g., `@utils`, `@poms`, `@config`) for easier customization and maintainability.
17+
- Updated `translate-json.js` and `install.js` to class-based files for consistency.
18+
- Updated README to reflect new updates.
19+
- Updated Magento Admin and frontend specs to ensure tests run successfully.
20+
- Fixed GitHub workflow and installation scripts for reliability.
21+
22+
### Removed
23+
- Removed setup toggles to avoid the use of `.only()` as this can easily lead to mistakes. Use `grep` instead.
24+
25+
### Fixed
26+
- Fixed various ‘false negatives’ where tests would fail due to poor hydration methods, including country selection and state selection fields which change based on the selected country.
27+
- Fixed ‘race condition’ in the `change_quantity_in_cart` test which sometimes caused the quantity to not be properly updated.
28+
- Minor fixes for ‘multimatch’ selector errors.
29+
- Fixed typos and minor issues in multiple files for better stability.
30+
31+
### New Contributors
32+
- [@Vinai](https://github.com/Vinai)
33+
634
## [2.2.0] - 2025-08-04
735

836
### Added
@@ -19,13 +47,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
1947
### Removed
2048
- Removed setup toggles to avoid the use of `.only()` as this can easily lead to mistakes. Use ‘grep’ instead.
2149

22-
2350
### Fixed
2451
- Fixed various ‘false negatives’ where tests would fail due to poor hydration methods. Among these is the country selection and the state selection field, which changes based on the selected country.
2552
- Fixed ‘race condition’ in the `change_quantity_in_cart` test which sometimes caused the quantity to not be properly updated.
2653
- Minor fixes for ‘multimatch’ selector errors
2754

28-
2955
## [2.1.0-alpha] - 2025-07-08
3056

3157
### Added
@@ -90,6 +116,9 @@ Due to a difference in releases between the GitHub version and the npm package,
90116

91117
- Various fixes for stability, better adherence to the DRY-principle and separation of concerns.
92118

119+
### New Contributors
120+
- [@LS-Myron](https://github.com/LS-Myron)
121+
93122
## [1.0.0-alpha] - 2025-01-29
94123
The initial Alpha Release!
95124

@@ -99,3 +128,7 @@ The initial Alpha Release!
99128
- Test cases for key features such as creating an account, ordering a product, adding a coupon code, and more.
100129
- Element identifiers, input values, and outcome markers added in JSON files to make customization easier.
101130
- Example GitHub Actions workflow to show how easily our tool can be integrated into the CI/CD pipeline.
131+
132+
### New Contributors
133+
- [@shayfaber](https://github.com/shayfaber)
134+
- [@dheesen](https://github.com/dheesen)

tests/setup.spec.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// @ts-check
22

3-
import { test as base } from '@playwright/test';
3+
import { test } from '@playwright/test';
44
import { inputValues } from '@config';
55
import { requireEnv } from '@utils/env.utils';
66
import { createLogger } from '@utils/logger';
@@ -13,14 +13,14 @@ const logger = createLogger('Setup');
1313
const magentoAdminUsername = requireEnv('MAGENTO_ADMIN_USERNAME');
1414
const magentoAdminPassword = requireEnv('MAGENTO_ADMIN_PASSWORD');
1515

16-
base.beforeEach(async ({ page }, testInfo) => {
16+
test.beforeEach(async ({ page }, testInfo) => {
1717
const magentoAdminPage = new MagentoAdminPage(page);
1818
await magentoAdminPage.login(magentoAdminUsername, magentoAdminPassword);
1919
});
2020

21-
base.describe('Setting up the testing environment', () => {
21+
test.describe('Setting up the testing environment', () => {
2222
// Set tests to serial mode to ensure the order is followed.
23-
base.describe.configure({mode:'serial'});
23+
test.describe.configure({mode:'serial'});
2424

2525
/**
2626
* @feature Magento Admin Configuration (disable login CAPTCHA)
@@ -33,14 +33,13 @@ base.describe('Setting up the testing environment', () => {
3333
* @but if the browser is not Chromium
3434
* @then the test is skipped with an appropriate message
3535
*/
36-
base('Disable_login_captcha', { tag: '@setup' }, async ({ page, browserName }, testInfo) => {
37-
base.skip(browserName !== 'chromium', `Disabling login captcha through Chromium. This is ${browserName}, therefore test is skipped.`);
36+
test('Disable_login_captcha', { tag: '@setup' }, async ({ page, browserName }, testInfo) => {
37+
test.skip(browserName !== 'chromium', `Disabling login captcha through Chromium. This is ${browserName}, therefore test is skipped.`);
3838

3939
const magentoAdminPage = new MagentoAdminPage(page);
4040
await magentoAdminPage.disableLoginCaptcha();
4141
});
4242

43-
4443
/**
4544
* @feature Magento Admin Configuration (Enable multiple admin logins)
4645
* @scenario Enable multiple admin logins only in Chromium browser
@@ -54,13 +53,12 @@ base.describe('Setting up the testing environment', () => {
5453
* @but if the browser is not Chromium
5554
* @then the test is skipped with an appropriate message
5655
*/
57-
base('Enable_multiple_admin_logins', { tag: '@setup' }, async ({ page, browserName }, testInfo) => {
58-
base.skip(browserName !== 'chromium', `Disabling login captcha through Chromium. This is ${browserName}, therefore test is skipped.`);
56+
test('Enable_multiple_admin_logins', { tag: '@setup' }, async ({ page, browserName }, testInfo) => {
57+
test.skip(browserName !== 'chromium', `Disabling login captcha through Chromium. This is ${browserName}, therefore test is skipped.`);
5958

6059
const magentoAdminPage = new MagentoAdminPage(page);
6160
await magentoAdminPage.enableMultipleAdminLogins();
6261
});
63-
6462

6563
/**
6664
* @feature Cart Price Rules Configuration
@@ -70,7 +68,7 @@ base.describe('Setting up the testing environment', () => {
7068
* @and the admin creates a new cart price rule with the specified coupon code
7169
* @then the coupon code is successfully saved and available for use
7270
*/
73-
base('Set_up_coupon_codes', { tag: '@setup'}, async ({page, browserName}, testInfo) => {
71+
test('Set_up_coupon_codes', { tag: '@setup'}, async ({page, browserName}, testInfo) => {
7472
const magentoAdminPage = new MagentoAdminPage(page);
7573
const browserEngine = browserName?.toUpperCase() || "UNKNOWN";
7674
const couponCode = requireEnv(`MAGENTO_COUPON_CODE_${browserEngine}`);
@@ -87,14 +85,14 @@ base.describe('Setting up the testing environment', () => {
8785
* @and submits the registration form with first name, last name, email, and password
8886
* @then a new customer account is successfully created for testing purposes
8987
*/
90-
base('Create_test_accounts', { tag: '@setup'}, async ({page, browserName}, testInfo) => {
88+
test('Create_test_accounts', { tag: '@setup'}, async ({page, browserName}, testInfo) => {
9189
const magentoAdminPage = new MagentoAdminPage(page);
9290
const registerPage = new RegisterPage(page);
9391
const browserEngine = browserName?.toUpperCase() || "UNKNOWN";
9492
const accountEmail = requireEnv(`MAGENTO_EXISTING_ACCOUNT_EMAIL_${browserEngine}`);
9593
const accountPassword = requireEnv('MAGENTO_EXISTING_ACCOUNT_PASSWORD');
9694

97-
await base.step(`Check if ${accountEmail} is already registered`, async () => {
95+
await test.step(`Check if ${accountEmail} is already registered`, async () => {
9896
const customerLookUp = await magentoAdminPage.checkIfCustomerExists(accountEmail);
9997
if(customerLookUp){
10098
testInfo.skip(true, `${accountEmail} was found in user table, this step is skipped. If you think this is incorrect, consider removing user from the table and try running the setup again.`);

tests/utils/apiClient.utils.ts

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// @ts-check
2+
3+
import { request, expect, APIRequestContext, APIResponse } from '@playwright/test';
4+
import { requireEnv } from '@utils/env.utils';
5+
6+
class ApiClient {
7+
private context!: APIRequestContext;
8+
private token: string | undefined;
9+
private tokenExpiry: number | undefined;
10+
11+
constructor() {}
12+
13+
/**
14+
* Initializes the ApiClient by ensuring a valid token and setting up the request context.
15+
* @returns {Promise<ApiClient>} A Promise that resolves to an instance of ApiClient.
16+
*/
17+
async create(): Promise<ApiClient> {
18+
await this.ensureToken();
19+
20+
this.context = await request.newContext({
21+
baseURL: requireEnv('PLAYWRIGHT_BASE_URL'),
22+
extraHTTPHeaders: {
23+
'Content-Type': 'application/json',
24+
Authorization: `Bearer ${this.token}`,
25+
},
26+
});
27+
28+
return this;
29+
}
30+
31+
/**
32+
* Ensures the API token is valid, refreshing it if expired or absent.
33+
* @private
34+
* @returns {Promise<void>}
35+
*/
36+
private async ensureToken(): Promise<void> {
37+
if (!this.token || this.isTokenExpired()) {
38+
this.token = await this.refreshIntegrationToken();
39+
}
40+
}
41+
42+
/**
43+
* Fetches a new API token from the server and sets the expiry time.
44+
* @private
45+
* @returns {Promise<string>} A Promise that resolves to the token string.
46+
* @throws {Error} If token retrieval fails.
47+
*/
48+
private async refreshIntegrationToken(): Promise<string> {
49+
const tempContext = await request.newContext({
50+
baseURL: requireEnv('PLAYWRIGHT_BASE_URL'),
51+
extraHTTPHeaders: {
52+
'Content-Type': 'application/json',
53+
},
54+
});
55+
56+
const response = await tempContext.post('/rest/V1/integration/admin/token', {
57+
data: {
58+
username: requireEnv('MAGENTO_API_USERNAME'),
59+
password: requireEnv('MAGENTO_API_PASSWORD'),
60+
},
61+
});
62+
63+
if (!response.ok()) {
64+
const errorBody = await response.text();
65+
await tempContext.dispose();
66+
throw new Error(`Failed to obtain integration token: ${response.status()} ${errorBody}`);
67+
}
68+
69+
const token = await response.json();
70+
const expiresHeader = response.headers()['expires'];
71+
if (expiresHeader) {
72+
this.tokenExpiry = new Date(expiresHeader).getTime();
73+
} else {
74+
this.tokenExpiry = Date.now() + (3600 * 1000);
75+
}
76+
77+
await tempContext.dispose();
78+
return token;
79+
}
80+
81+
/**
82+
* Determines if the current token is expired.
83+
* @private
84+
* @returns {boolean} True if the token is expired, otherwise false.
85+
*/
86+
private isTokenExpired(): boolean {
87+
return !this.tokenExpiry || Date.now() >= this.tokenExpiry;
88+
}
89+
90+
/**
91+
* Performs a GET request to the specified URL.
92+
* @param {string} url The endpoint URL to send the request to.
93+
* @returns {Promise<any>} A Promise that resolves to the response JSON.
94+
*/
95+
async get(url: string): Promise<any> {
96+
const response = await this.context.get(url);
97+
return this.handleResponse(response);
98+
}
99+
100+
/**
101+
* Performs a POST request with the given payload to the specified URL.
102+
* @param {string} url The endpoint URL to send the request to.
103+
* @param {Record<string, unknown>} payload The data payload to send with the request.
104+
* @returns {Promise<any>} A Promise that resolves to the response JSON.
105+
* @throws {Error} If the response indicates failure.
106+
*/
107+
async post(url: string, payload: Record<string, unknown>): Promise<any> {
108+
const response = await this.context.post(url, { data: payload });
109+
return this.handleResponse(response);
110+
}
111+
112+
/**
113+
* Performs a PUT request with the given payload to the specified URL.
114+
* @param {string} url The endpoint URL to send the request to.
115+
* @param {Record<string, unknown>} payload The data payload to send with the request.
116+
* @returns {Promise<any>} A Promise that resolves to the response JSON.
117+
* @throws {Error} If the response indicates failure.
118+
*/
119+
async put(url: string, payload: Record<string, unknown>): Promise<any> {
120+
const response = await this.context.put(url, { data: payload });
121+
return this.handleResponse(response);
122+
}
123+
124+
/**
125+
* Performs a DELETE request to the specified URL.
126+
* @param {string} url The endpoint URL to send the request to.
127+
* @returns {Promise<void>} A Promise indicating successful deletion.
128+
*/
129+
async delete(url: string): Promise<void> {
130+
const response = await this.context.delete(url);
131+
return this.handleResponse(response);
132+
}
133+
134+
/**
135+
* Handles an API response, checking for success and parsing the JSON body.
136+
* @param {APIResponse} response The response object to handle.
137+
* @returns {Promise<any>} A Promise that resolves to the response JSON.
138+
* @throws {Error} If the response is not successful.
139+
*/
140+
async handleResponse(response: APIResponse): Promise<any> {
141+
if (!response.ok()) {
142+
const body = await response.text();
143+
throw new Error(`API call failed [${response.status()}]: ${body}`);
144+
}
145+
return await response.json();
146+
}
147+
148+
/**
149+
* Disposes of the current request context.
150+
* @returns {Promise<void>}
151+
*/
152+
async dispose(): Promise<void> {
153+
await this.context.dispose();
154+
}
155+
}
156+
157+
export default ApiClient;

0 commit comments

Comments
 (0)