@roopesh.yadava/qa-pack 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,237 @@
1
+ # BDD Templates & Reference
2
+
3
+ > Load this file at the start of Phase 1 (Gherkin generation) and Phase 3 (POM).
4
+ > Contains Gherkin format examples, step definition templates, POM class template,
5
+ > Faker.js patterns, file upload guide, and BDD API hook reference.
6
+
7
+ ---
8
+
9
+ ## Gherkin Format Example
10
+
11
+ ```gherkin
12
+ Feature: <feature title from card>
13
+
14
+ Background: (optional — only if 2+ scenarios share the same Given)
15
+ Given ...
16
+
17
+ Rule: <business rule — taken verbatim from AC/COS>
18
+
19
+ @add_user
20
+ Example: <Persona> as <role> <scenario description>
21
+ Given <persona> is on the <page name> page
22
+ When <persona> <action described as intent, not mechanics>
23
+ Then <persona> should <observable outcome>
24
+
25
+ @add_user
26
+ Example: <Different persona or variation under same Rule>
27
+ Given ...
28
+ When ...
29
+ Then ...
30
+
31
+ @add_subscription
32
+ @add_subscription_offer
33
+ Scenario Outline: <persona> <flow description> with multiple <data type>
34
+ Given <persona> is on the <page name> page
35
+ When <persona> enters credentials "<email>" and "<password>"
36
+ Then <persona> should see "<expected_message>"
37
+
38
+ Examples:
39
+ | persona | email | password | expected_message |
40
+ | John | john@test.com | Admin@1234 | Verification passed |
41
+ | Maria | maria@test.com | Branch@1234 | Verification passed |
42
+ ```
43
+
44
+ ---
45
+
46
+ ## Step Definition Template
47
+
48
+ ```javascript
49
+ const { Given, When, Then, Before, After } = require('@cucumber/cucumber');
50
+ const { faker } = require('@faker-js/faker');
51
+ const AddUserPage = require('../../Pages/UserManagement/add-user.cjs');
52
+
53
+ // API imports — only what this feature needs
54
+ const { createUserViaApi } = require('../../BDDUtilies/bdd_api/addUserApi.cjs');
55
+
56
+ let addUserPage;
57
+
58
+ // ── Unconditional Before — restore globals ────────────────────────────────────
59
+ Before(async function () {
60
+ if (global.lastCreatedUserName) {
61
+ this.lastCreatedUserName = global.lastCreatedUserName;
62
+ }
63
+ });
64
+
65
+ // ── Tagged Before hooks (API seeding) ────────────────────────────────────────
66
+ // Check Driver.cjs first — do not duplicate existing hooks.
67
+
68
+ Before({ tags: '@add_user' }, async function () {
69
+ await waitForToken();
70
+ const result = await createUserViaApi();
71
+ this.createdUser = result.data.user;
72
+ this.createdUserEmail = result.payload.email;
73
+ this.username = result.payload.email;
74
+ this.createdUserData = result.createdUserData;
75
+ this.viewAdminData = result.viewAdminData;
76
+ this.lastCreatedUserName = result.payload.name;
77
+ global.lastCreatedUserName = result.payload.name;
78
+ });
79
+
80
+ // ── Steps ────────────────────────────────────────────────────────────────────
81
+
82
+ Given('{word} is on the add user page', async function (persona) {
83
+ addUserPage = new AddUserPage(this.page);
84
+ await addUserPage.navigate();
85
+ });
86
+
87
+ When('{word} fills in the new user details', async function (persona) {
88
+ await addUserPage.fillUserForm();
89
+ });
90
+
91
+ Then('{word} should see the user created successfully', async function (persona) {
92
+ await addUserPage.verifyUserCreated();
93
+ });
94
+ ```
95
+
96
+ ---
97
+
98
+ ## POM Class Template
99
+
100
+ ```javascript
101
+ const { expect } = require('@playwright/test');
102
+ const { faker } = require('@faker-js/faker');
103
+
104
+ class AddUserPage {
105
+ /**
106
+ * @param {import('@playwright/test').Page} page
107
+ */
108
+ constructor(page) {
109
+ this.page = page;
110
+
111
+ // ── Locators (constructor ONLY — never inside methods) ───────────────
112
+ // All locators confirmed via Playwright MCP on live DOM
113
+ // Priority: data-testid → getByRole → getByLabel/getByText → id → CSS → XPath
114
+ this.nameInput = page.getByTestId('add-user-name');
115
+ this.emailInput = page.getByTestId('add-user-email');
116
+ this.roleSelect = page.getByTestId('add-user-role');
117
+ this.submitButton = page.getByTestId('add-user-submit');
118
+ this.successMessage = page.getByTestId('add-user-success');
119
+ this.errorMessage = page.getByTestId('add-user-error');
120
+ }
121
+
122
+ // ── Navigation ───────────────────────────────────────────────────────
123
+
124
+ async navigate() {
125
+ await this.page.goto('/users/add');
126
+ }
127
+
128
+ // ── Actions ──────────────────────────────────────────────────────────
129
+
130
+ async fillUserForm() {
131
+ await this.nameInput.fill(faker.string.alphanumeric(15));
132
+ await this.emailInput.fill(`test+${faker.string.alphanumeric(8)}@yourdomain.com`);
133
+ await this.roleSelect.selectOption('admin');
134
+ await this.submitButton.click();
135
+ }
136
+
137
+ // ── Assertions ───────────────────────────────────────────────────────
138
+
139
+ async verifyUserCreated() {
140
+ await expect(this.successMessage).toBeVisible();
141
+ }
142
+
143
+ async verifyErrorMessage(expectedText) {
144
+ await this.page.getByText(expectedText, { exact: false })
145
+ .waitFor({ state: 'visible', timeout: 60000 });
146
+ }
147
+
148
+ async verifyAllAdminData(apiData) {
149
+ expect(await this.page.getByTestId('user-email').textContent()).toBe(apiData.email);
150
+ expect(await this.page.getByTestId('user-role').textContent()).toBe(apiData.role);
151
+ }
152
+ }
153
+
154
+ module.exports = AddUserPage;
155
+ ```
156
+
157
+ ---
158
+
159
+ ## Faker.js Patterns
160
+
161
+ ```javascript
162
+ const { faker } = require('@faker-js/faker');
163
+
164
+ faker.string.alphanumeric(15) // random name
165
+ `test+${faker.string.alphanumeric(8)}@yourdomain.com` // email
166
+ '999999999' // fixed test phone
167
+ faker.location.streetAddress()
168
+ faker.location.city()
169
+ faker.location.zipCode('##-###')
170
+ faker.number.int({ min: 1, max: 100 })
171
+ faker.date.future().toISOString().split('T')[0] // 'YYYY-MM-DD'
172
+ ```
173
+
174
+ Generate inside the method — never in the constructor or at module scope.
175
+
176
+ ---
177
+
178
+ ## File Upload — Decision Guide
179
+
180
+ | Situation | Method |
181
+ |-----------|--------|
182
+ | Visible or hidden `<input type="file">` | `setInputFiles()` directly on locator |
183
+ | Styled button that opens OS file picker | `uploadViaButton()` — `waitForEvent('filechooser')` |
184
+ | Drag-and-drop zone, no visible input | `dragAndDropFile()` — tiny buffer trick (default) |
185
+
186
+ Use `test/support/` fixture files only when the user explicitly says so.
187
+
188
+ ---
189
+
190
+ ## BDD API Hooks — Existing Tags
191
+
192
+ Check Driver.cjs before creating new hooks — do not duplicate.
193
+
194
+ | Tag | Type | API | Requires |
195
+ |-----|------|-----|---------|
196
+ | `@Logout` | Before | clears storage | — |
197
+ | `@Logout1` | After | clears storage | — |
198
+ | `@add_subscription` | Before | POST subscription | logged-in admin |
199
+ | `@add_subscription_10` | Before | POST subscription ×10 | logged-in admin |
200
+ | `@add_subscription_offer` | Before | POST offer | `@add_subscription` first |
201
+ | `@add_subscription_offer_10` | Before | POST subscription+offer ×10 | logged-in admin |
202
+ | `@add_client_paid` | Before | POST client (paid) | logged-in admin |
203
+ | `@add_client_unpaid` | Before | POST client (unpaid) | logged-in admin |
204
+ | `@add_client_paid_10` | Before | POST client ×10 | logged-in admin |
205
+ | `@add_user` | Before | POST user | logged-in admin |
206
+ | `@add_user_10` | Before | POST user ×10 | logged-in admin |
207
+ | `@add_technician` | Before | POST technician | logged-in admin |
208
+ | `@add_technician_10` | Before | POST technician ×10 | logged-in admin |
209
+ | `@add_facility_under_client` | Before | POST facility (fixed client) | logged-in admin |
210
+ | `@client_privilege_initial` | Before | PATCH privileges (reset) | logged-in admin |
211
+ | `@delete_facility_bdd_api` | After | DELETE facilities cleanup | — |
212
+ | `@full_privilege_client` | After | PATCH privileges (full) | — |
213
+
214
+ **Stacking order matters** — `@add_subscription` must always be above `@add_subscription_offer`.
215
+
216
+ ---
217
+
218
+ ## Quick Reference
219
+
220
+ | Content | Location |
221
+ |---------|----------|
222
+ | Business rule | `Rule:` in `.feature` |
223
+ | Persona/role variation | `Example:` block |
224
+ | Same flow, different data | `Scenario Outline` + `Examples:` |
225
+ | Multiple variations of same rule | Multiple `Example:` under one `Rule:` |
226
+ | API seeding tag | Above `Example:` / `Scenario Outline:` line |
227
+ | Locators discovered | Playwright MCP on live DOM |
228
+ | Locators written | POM constructor only |
229
+ | Actions | POM async methods |
230
+ | Assertions | POM `verify*` methods |
231
+ | Generated test data | `faker.js` — inside methods |
232
+ | `page.locator()` in step files | ❌ Never |
233
+ | Hardcoded waits | ❌ Never |
234
+ | XPath | ❌ Last resort only |
235
+ | `import`/`export` | ❌ Use `require`/`module.exports` |
236
+ | Gate 1 skipped | ❌ Never — always confirm Gherkin before writing code |
237
+ | Gate 2 skipped | ❌ Never — always confirm step defs before writing POM |
@@ -0,0 +1,169 @@
1
+ # Locator Patterns — Hard-Won Lessons
2
+
3
+ > Load this file before Phase 2 (Step Definitions) and Phase 3 (POM).
4
+ > These rules are learned from real QE-89 failures. Apply every one before writing any locator.
5
+
6
+ ---
7
+
8
+ ### 1. Floating-label UI — never use `getByPlaceholder()`
9
+
10
+ Floating-label inputs look like they have placeholder text but the visible text is a
11
+ **CSS `<label>` element** that moves upward on focus. There is **no actual HTML `placeholder`
12
+ attribute** on the input.
13
+
14
+ ```javascript
15
+ // ❌ WRONG — times out because there is no placeholder attr
16
+ this.emailInput = page.getByPlaceholder('Adres e-mail');
17
+
18
+ // ✅ CORRECT — positional selector, confirmed via Playwright MCP / screenshot
19
+ this.emailInput = page.locator('input').nth(0);
20
+ this.passwordInput = page.locator('input').nth(1);
21
+ ```
22
+
23
+ **How to detect:** Open DevTools → inspect input → if there is no `placeholder="..."` attr,
24
+ you are on a floating-label form.
25
+
26
+ ---
27
+
28
+ ### 2. Custom password field — never use `input[type="password"]`
29
+
30
+ Password inputs with a show/hide eye icon are often rendered with a **custom component**
31
+ that does not use `type="password"` on the native input element.
32
+
33
+ ```javascript
34
+ // ❌ WRONG — element never found
35
+ this.passwordInput = page.locator('input[type="password"]');
36
+
37
+ // ✅ CORRECT
38
+ this.passwordInput = page.locator('input').nth(1);
39
+ ```
40
+
41
+ ---
42
+
43
+ ### 3. `navigate()` must use an absolute URL
44
+
45
+ Playwright throws `"Cannot navigate to invalid URL"` when you pass a relative path without
46
+ a `baseURL` configured in `playwright.config`. Always compose the full URL:
47
+
48
+ ```javascript
49
+ // ❌ WRONG
50
+ await this.page.goto('/login');
51
+
52
+ // ✅ CORRECT
53
+ const baseUrl = process.env.BASE_URL || 'https://qa.loopay.com.pl';
54
+ await this.page.goto(`${baseUrl}/login`);
55
+ ```
56
+
57
+ ---
58
+
59
+ ### 4. Post-login redirect — don't assume `/dashboard` in the URL
60
+
61
+ After a successful login+OTP the app may redirect to `/home`, `/overview`, or any route.
62
+ Do **not** use `waitForURL(/dashboard/)` unless you have confirmed the exact redirect path.
63
+
64
+ ```javascript
65
+ // ❌ WRONG — times out if redirect URL is /home or /overview
66
+ await this.page.waitForURL(/dashboard/, { timeout: 30000 });
67
+
68
+ // ✅ CORRECT — generic "we left the login page" check
69
+ await this.page.waitForFunction(
70
+ () => !window.location.href.includes('/login'),
71
+ { timeout: 30000 }
72
+ );
73
+ ```
74
+
75
+ ---
76
+
77
+ ### 5. Invalid-credentials error is inline text, not `role="alert"`
78
+
79
+ Error messages shown below a form field (e.g. "Błędny e-mail lub hasło.") are plain
80
+ `<span>` or `<p>` elements. `getByRole('alert')` returns nothing and times out.
81
+
82
+ ```javascript
83
+ // ❌ WRONG
84
+ await expect(page.getByRole('alert')).toBeVisible();
85
+
86
+ // ✅ CORRECT — match the actual inline error text
87
+ this.credentialsErrorText = page.getByText(/błędny e-mail lub hasło/i);
88
+ await this.credentialsErrorText.waitFor({ state: 'visible', timeout: 10000 });
89
+ ```
90
+
91
+ ---
92
+
93
+ ### 6. Multiple toast alerts — strict-mode violation
94
+
95
+ When a wrong OTP is submitted the app may fire **two** alert toasts at once
96
+ (e.g. "Code sent" + "New code sent"). `getByRole('alert')` then throws a strict-mode
97
+ violation because it resolves to 2 elements.
98
+
99
+ ```javascript
100
+ // ❌ WRONG — strict mode violation when 2 alerts are present
101
+ await expect(page.getByRole('alert')).toBeVisible();
102
+
103
+ // ✅ CORRECT — use .first() or filter by hasText
104
+ await expect(page.getByRole('alert').first()).toBeVisible({ timeout: 10000 });
105
+ ```
106
+
107
+ ---
108
+
109
+ ### 7. 6-box OTP PIN component — use `keyboard.type()`, not `fill()`
110
+
111
+ The OTP screen uses a PIN component with 6 individual boxes. Calling `.fill()` on the
112
+ first box only populates one digit and the component does not auto-advance.
113
+
114
+ ```javascript
115
+ // ❌ WRONG — only fills box 1, leaves 5 empty
116
+ await this.otpInput.fill('999999');
117
+
118
+ // ✅ CORRECT — click first box to focus, then type digit-by-digit
119
+ await this.otpInput.waitFor({ state: 'visible', timeout: 15000 });
120
+ await this.otpInput.click();
121
+ await this.page.keyboard.type(otp); // component auto-advances on each digit
122
+ ```
123
+
124
+ ---
125
+
126
+ ### 8. Resend OTP state — inspect before assuming
127
+
128
+ The Resend OTP feature can behave differently across environments:
129
+ - **Some envs:** countdown text "Wróć ponownie po X:XX" shown for 60 s, then replaced by clickable link.
130
+ - **QA env (qa.loopay.com.pl):** the countdown is skipped; "Wyślij kod ponownie" is shown immediately.
131
+
132
+ **Always use Playwright MCP or failure screenshots** to confirm the exact text before writing the locator.
133
+
134
+ ```javascript
135
+ // ❌ FRAGILE — countdown may not appear in this environment
136
+ this.resendCountdown = page.getByText(/wróć ponownie po/i);
137
+
138
+ // ✅ CONFIRMED against QA env screenshot
139
+ this.resendOtpLink = page.getByText(/wyślij kod ponownie/i);
140
+ ```
141
+
142
+ ---
143
+
144
+ ### 9. Read failure screenshots before every locator decision
145
+
146
+ Every failed scenario saves a screenshot to:
147
+ ```
148
+ test/step-definations/failed_scenarios/<uuid>_<scenario-name>.png
149
+ ```
150
+
151
+ **Always read the latest screenshot for a failing scenario** using the `Read` tool.
152
+ The screenshot shows exactly what the browser sees and prevents guessing:
153
+ - What text is rendered
154
+ - Which elements are present
155
+ - Whether a toast/alert is shown
156
+
157
+ ---
158
+
159
+ ### 10. Submit-button locator — use the exact visible button text
160
+
161
+ The login submit button text is in the UI language (Polish: "Zaloguj się").
162
+ The OTP verify button is "Zweryfikuj". These are confirmed from screenshots.
163
+
164
+ ```javascript
165
+ this.submitButton = page.getByRole('button', { name: 'Zaloguj się' });
166
+ this.otpSubmit = page.getByRole('button', { name: /zweryfikuj|zatwierdź|verify|potwierdź/i });
167
+ ```
168
+
169
+ Use a regex when the exact label might vary by role or locale.